Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions crates/capabilities/src/http_transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,12 @@ module.exports.httpRequest = function (host, port, isHttps, socketPath, head_ptr
// wasm_memory.buffer is replaced each time WebAssembly.Memory grows, so
// the views must be recreated on every attempt against the current buffer.
const headView = new Uint8Array(wasm_memory.buffer, head_ptr, head_len)
const bodyView = new Uint8Array(wasm_memory.buffer, body_ptr, body_len)
// Copy the body into Node-owned memory before giving it to http.request.
// `body_ptr/body_len` points into wasm memory; if that memory grows while
// Node still has the write queued, the original ArrayBuffer detaches and
// ClientRequest can throw asynchronously from `_flushOutput`, outside the
// `req.write()` try/catch/retry path.
const body = Buffer.from(new Uint8Array(wasm_memory.buffer, body_ptr, body_len))

// The Rust side already rendered the full HTTP/1.1 request head (real
// method, `/v0.4/traces` path, Content-Type/Length, datadog-meta-*);
Expand Down Expand Up @@ -234,10 +239,10 @@ module.exports.httpRequest = function (host, port, isHttps, socketPath, head_ptr
req.on('error', reject)

// The request head (method/path/headers) was supplied via requestOptions
// above; just write the body. (No `req._header` injection — that Node
// internal is not honored by Bun.)
// above; just write the stable Node-owned body. (No `req._header`
// injection — that Node internal is not honored by Bun.)
try {
req.write(bodyView)
req.write(body)
req.end()
} catch (error) {
reject(error)
Expand Down
38 changes: 38 additions & 0 deletions test/http_transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,44 @@ describe('http_transport response header observer', () => {
assert.strictEqual(body.length, Buffer.byteLength(RESPONSE_BODY))
assert.strictEqual(Buffer.from(body).toString('utf8'), RESPONSE_BODY)
})

it('copies the request body before queued writes can observe detached wasm memory', async () => {
const received = []
const body = Buffer.from('wasm-backed request body')
const server = http.createServer((req, res) => {
req.on('data', chunk => received.push(chunk))
req.on('end', () => res.end(RESPONSE_BODY))
})

await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
try {
const port = server.address().port
const head = Buffer.from(
`POST /v0.4/traces HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\n`
+ `Content-Length: ${body.length}\r\nConnection: close\r\n\r\n`,
'utf8',
)
const memory = new WebAssembly.Memory({ initial: 1 })
const bytes = new Uint8Array(memory.buffer)
bytes.set(head, 0)
bytes.set(body, head.length)

const result = transport.httpRequest(
'127.0.0.1', port, false, '', 0, head.length, head.length, body.length, memory,
)
// Old behavior passed a live wasm-memory view to req.write(). If the memory
// grew before Node assigned a socket and flushed its queued output, Node
// threw `Cannot perform Construct on a detached ArrayBuffer` from
// ClientRequest._flushOutput, outside httpRequest's try/catch retry path.
memory.grow(1)

const [status] = await result
assert.strictEqual(status, 200)
assert.strictEqual(Buffer.concat(received).toString('utf8'), body.toString('utf8'))
} finally {
await new Promise(resolve => server.close(resolve))
}
})
})

// libdatadog derives the request host from the agent URI, which keeps the
Expand Down
Loading