Skip to content

fix(core): End node:http client spans when the response is destroyed - #25503

Open
4ktLuffy wants to merge 2 commits into
getsentry:developfrom
4ktLuffy:fix/node-http-client-span-on-response-destroy
Open

4ktLuffy wants to merge 2 commits into
getsentry:developfrom
4ktLuffy:fix/node-http-client-span-on-response-destroy

Conversation

@4ktLuffy

Copy link
Copy Markdown
Contributor

When the caller destroys an http.get() response mid-body with res.destroy() (no error), the http.client span is never ended, so it is never sent. The response emits only close, not end or error, and the request close fallback is removed once the response arrives.

This adds a response close listener that ends the span. Following #25260, a destroy by the caller is not treated as a failure: the span keeps the status of the response (ok for a 200, error for a 404). res.destroy(err) and a server closing the connection mid-body still set an error status, because they emit error before close. After a normal end or an error, the new listener is a no-op.

Tests: caller destroy (200 and 404), caller destroy with an error, and a new server-abort case. The caller-destroy test times out without the fix. All 121 http tests pass. Reproduced on @sentry/node 11.8.0.

Closes #25469

@github-actions github-actions Bot added the external PR from an external contributor label Oct 19, 2026
@Lms24
Lms24 requested review from andreiborza and isaacs October 19, 2026 08:18

@s1gr1d s1gr1d left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution, I have some comments on it.

btw, also linking this PR for reference: #25260

// A response destroyed by the caller without an error (e.g. `res.destroy()`) emits neither
// 'end' nor 'error', only 'close'. Like a caller abort, this is not a failure of the request,
// so end the span with the status of the response. After 'end' or an error this is a no-op.
response.on('close', () => finishWithResponse(undefined, true));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

close is not only emitted when the caller destroys the response, but also when the connection dies/resets (server reset, req.destroy()).

Node calls res.destroy(ECONNRESET) and IncomingMessage only emits error if there is an 'error' listener (but it does not count errorMonitor).

If user code does not listen for error on the response, the span ends as ok (common with http.get(url, res => res.pipe(...))).

response.errored separates the two cases. It is null after res.destroy() and holds the error after a reset, even when Node did not emit that error:

response.on('close', () => finishWithResponse(response.errored));

With this change, you can also remove the destroyedByCaller flag. Could you check that?


let finished = false;
function finishWithResponse(error?: unknown): void {
function finishWithResponse(error?: unknown, destroyedByCaller = false): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use response.errored, you can remove the destroyedByCaller parameter.

Comment on lines +199 to +201
// A response destroyed by the caller without an error (e.g. `res.destroy()`) emits neither
// 'end' nor 'error', only 'close'. Like a caller abort, this is not a failure of the request,
// so end the span with the status of the response. After 'end' or an error this is a no-op.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the comment shorter and correct it after the change, for example: "res.destroy() emits only close. errored separates a caller destroy from a connection error that was not emitted because nothing listens for error." Right now it says that close always means a caller destroy, which is not true.

@4ktLuffy

Copy link
Copy Markdown
Contributor Author

Thanks, you were right. I reproduced it: with no error listener, a reset only emits close with errored set, and the span ended as ok. Pushed the fix:

  • close now calls finishWithResponse(response.errored), and destroyedByCaller is gone.
  • Also removed the aborted check, since Node sets aborted on a plain res.destroy() too.
  • Added errored to HttpIncomingMessage for the type check.
  • Tests now use the real errorMonitor (the old default added an error listener and hid the bug), plus a new test for a reset with no listener.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external PR from an external contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node:http client span never ends when the response is destroyed without an error (res.destroy())

2 participants