Skip to content

fix(node): Don't mark fetch spans as errors when the caller aborts - #25129

Merged
andreiborza merged 3 commits into
getsentry:developfrom
4ktLuffy:fix/node-fetch-abort-not-error
Oct 12, 2026
Merged

andreiborza merged 3 commits into
getsentry:developfrom
4ktLuffy:fix/node-fetch-abort-not-error

Conversation

@4ktLuffy

@4ktLuffy 4ktLuffy commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

A fetch() span that already got a 200 response ends up with status: error when the caller cancels the body stream (reader.cancel()) or calls AbortController.abort() after the headers arrived. undici reports that as undici:request:error with an AbortError instead of undici:request:trailers, and onError in the undici instrumentation sets SPAN_STATUS_ERROR for every error.

This ports the check from upstream OpenTelemetry (open-telemetry/opentelemetry-js-contrib#3488, "do not record aborted requests as errors"): if the error is an AbortError, or a DOMException with ABORT_ERR, the span is ended without touching its status. Other errors (connection reset, TypeError: fetch failed, ...) still set the error status as before. I updated the NOTE comment above onError to say so.

This matters for clients that read a stream and then close it as part of normal operation. For example the MCP TypeScript SDK closes its event stream on client close, so every healthy session showed http.client error spans.

Repro

import * as Sentry from "@sentry/node";
import { createServer } from "node:http";

Sentry.init({ dsn: "http://key@127.0.0.1:9/1", tracesSampleRate: 1,
  beforeSendSpan(s) { console.log(s.name ?? s.description, s.status); return s; } });

const srv = createServer((req, res) => {
  if (req.url === "/stream") { res.writeHead(200, { "content-type": "text/event-stream" }); res.write("data: 1\n\n"); return; }
  res.writeHead(200); res.end("ok");
});
await new Promise((r) => srv.listen(0, "127.0.0.1", r));
const base = `http://127.0.0.1:${srv.address().port}`;

await Sentry.startSpan({ name: "root" }, async () => {
  await (await fetch(base + "/plain")).text();                    // ok
  const r = (await fetch(base + "/stream")).body.getReader();
  await r.read(); await r.cancel();                                // error, although the response was 200
});
await Sentry.flush(2000); srv.closeAllConnections(); srv.close();

Before (11.4.0): the fully read request is ok, the cancelled one is error with http.response.status_code 200. Same with AbortController.abort() after the headers arrive.
After (this branch, built with yarn build:dev:filter @sentry/node): both are ok.

Tests

Added unit tests to packages/node/test/integrations/undici.test.ts that publish the undici:request:create, undici:request:headers (200) and undici:request:error diagnostics channel messages:

  • abort errors (AbortError, DOMException named AbortError, DOMException with only the ABORT_ERR code) end the span without an error status
  • control: a TypeError and an ECONNRESET error still set the error status

cd packages/node && yarn vitest run test/integrations/undici.test.ts: without the fix 3 of 10 fail (the abort cases), with the fix 10 of 10 pass. oxfmt --check and oxlint --type-aware are clean on both files.

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test). (Ran the changed test file, oxfmt and oxlint on the changed files; not the full suite.)
  • Link an issue if there is one related to your pull request. (There is none; this PR has the repro.)

@4ktLuffy
4ktLuffy requested a review from a team as a code owner October 7, 2026 10:35
@4ktLuffy
4ktLuffy requested review from andreiborza and mydea and removed request for a team October 7, 2026 10:35
@github-actions github-actions Bot added the external PR from an external contributor label Oct 7, 2026

@andreiborza andreiborza 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.

Hi @4ktLuffy, thanks for fixing this. LGTM

andreiborza added a commit that referenced this pull request Oct 12, 2026
…rts (#25260)

## What

Outgoing `node:http` spans no longer get an error status when the caller
aborts the request (for example with `AbortController.abort()`). If a
response already arrived, the span keeps the status of that response.

## Why

A caller abort is not a request failure, and it showed false
`http.client` errors (for example for axios cancellation). This matches
the fetch fix in #25129.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@andreiborza
andreiborza merged commit 20c26f2 into getsentry:develop Oct 12, 2026
415 of 418 checks passed
andreiborza added a commit that referenced this pull request Oct 12, 2026
This PR adds the external contributor to the CHANGELOG.md file, so that
they are credited for their contribution. See #25129

Co-authored-by: andreiborza <168741329+andreiborza@users.noreply.github.com>
andreiborza pushed a commit that referenced this pull request Oct 12, 2026
#25273)

Backport of: #25129

## Differences to the original PR

- The fix applies to
`packages/node-core/src/integrations/node-fetch/undici-instrumentation.ts`.
On v10 the vendored undici instrumentation is in `@sentry/node-core`,
not `@sentry/node`. The `onError` change itself is the same.
- Added `// eslint-disable-next-line typescript/no-deprecated` above the
`error.code === DOMException.ABORT_ERR` check. Without it, v10's
`typescript/no-deprecated` lint rule fails on `DOMException.code`.
Runtime behavior does not change.
- The tests are in a new file,
`packages/node-core/test/integrations/undici.test.ts`. v10 has no
`undici.test.ts` to extend. The new file uses the same mock setup as the
develop test file (`@sentry/core` `startInactiveSpan`/`getClient`,
`outgoingFetchRequest`) and contains only the new `request errors`
cases. It leaves out the existing develop-only QUERY-method/span-naming
tests.

<!-- junior-request-attribution:start -->
via Junior system actor `junior`.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!--
junior-conversation-id:agent-dispatch%3Adispatch_c21e073eaf1d6b2a655e346024498ce1
-->

--

[View Junior
Session](https://junior-prod.sentry.dev/conversations/agent-dispatch%3Adispatch_c21e073eaf1d6b2a655e346024498ce1)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent-dispatch%3Adispatch_c21e073eaf1d6b2a655e346024498ce1/?project=4510944073809921)

<!-- junior-session-footer:end -->

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
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.

2 participants