Skip to content

ref(cloudflare): Use the shared request handler wrapper - #25588

Open
isaacs wants to merge 14 commits into
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-bfrom
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
Open

isaacs wants to merge 14 commits into
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-bfrom
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c

Conversation

@isaacs

@isaacs isaacs commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Moves Cloudflare's wrapRequestHandler onto wrapServerRequestHandler, which Deno.serve and Bun.serve use since the previous PRs. Cloudflare keeps only what is unique to it: client init, the cf and cloud resource contexts, the protocol from cf.httpProtocol, the isStreamingResponse option, captureErrors, and the flush through waitUntil, which skips the dispose for a 101 response. The helpers that only the old code used (captureIncomingRequestBody, shouldCreateIncomingRequestSpan, getIncomingRequestSpanHooks and addRequest) are gone.

wrapRequestHandler must run on workers without the nodejs_compat flag, such as Shopify Oxygen, and every @sentry/server-utils entry loads node:async_hooks. So the wrapper moves to a new entry, @sentry/server-utils/request-handler, with no Node.js imports. Deno and Bun import it from there too. Cloudflare's module graph test allows this one entry, and a new test in server-utils walks the entry's module graph and fails on any Node.js import.

The wrapper gets three optional fields: isStreamingResponse, onDone (called once when the request is done, with the response if there is one), and a mechanism that can be left out to not capture errors.

For streamed responses, the shared stream monitor replaces Cloudflare's TransformStream. Workers with a compatibility date before 2022-11-30 cannot construct a ReadableStream without the streams_enable_constructors flag, so the wrapper then ends at handler return, as the TransformStream fallback did. The monitor now locks the source body only in start(), so the original response stays usable in that case. The wrapper builds the new response from the original one, which keeps encodeBody: 'manual'.

Visible changes for Cloudflare:

  • Request spans set client.address from CF-Connecting-IP (or another forwarding header), as on the other runtimes. This follows dataCollection.userInfo, which is true by default.
  • Request spans get the response header attributes, and events get the response context.
  • An error from the source of a streamed response is captured (unless captureErrors is false), and the span gets an error status.
  • If the user removed httpServerIntegration, request bodies follow dataCollection.httpBodies. Before, the SDK attached no body.

The minified withSentry bundle grows by about 2.4 KB (212,385 to 214,853 bytes), under the 211 KiB limit. Most of it is the request session and client address code, which Cloudflare now shares.

Fixes #25577
Fixes JS-3975

@linear-code

linear-code Bot commented Oct 20, 2026

Copy link
Copy Markdown

JS-3975

@isaacs
isaacs added this pull request to stack #25589 October 20, 2026 22:30

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread packages/cloudflare/src/wrapRequestHandlerWithInit.ts
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.88 kB - -
@sentry/browser - with treeshaking flags 28.02 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.92 kB - -
@sentry/browser (incl. Tracing) 52.07 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.07 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.02 kB - -
@sentry/browser (incl. Tracing, Replay) 91.82 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.68 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.55 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.49 kB - -
@sentry/browser (incl. Feedback) 47.4 kB - -
@sentry/browser (incl. sendFeedback) 34.94 kB - -
@sentry/browser (incl. FeedbackAsync) 40.05 kB - -
@sentry/browser (incl. Metrics) 30.9 kB - -
@sentry/browser (incl. Logs) 31.18 kB - -
@sentry/browser (incl. Metrics & Logs) 31.81 kB - -
@sentry/react 31.7 kB - -
@sentry/react (incl. Tracing) 54.39 kB - -
@sentry/vue 38.04 kB - -
@sentry/vue (incl. Tracing) 55.01 kB - -
@sentry/svelte 29.9 kB - -
@sentry/remix (Remix 3 client bundle) 57.08 kB - -
CDN Bundle 31.64 kB - -
CDN Bundle (incl. Tracing) 52.58 kB - -
CDN Bundle (incl. Logs, Metrics) 33.84 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.52 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.77 kB - -
CDN Bundle (incl. Tracing, Replay) 90.25 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.18 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.34 kB - -
CDN Bundle - uncompressed 93.18 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.03 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.72 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.76 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276.2 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 282.14 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.9 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.84 kB - -
@sentry/nextjs (client) 57.28 kB - -
@sentry/sveltekit (client) 52.45 kB - -
@sentry/core/server 41.75 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 155.45 kB +0.13% +193 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 84.06 kB - -
@sentry/node - without tracing 97.08 kB +0.17% +160 B 🔺
@sentry/node - without channel injection 133.42 kB +0.14% +182 B 🔺
@sentry/aws-serverless 105.28 kB +0.18% +180 B 🔺
@sentry/cloudflare (withSentry) - minified 215.07 kB +1.99% +4.18 kB 🔺
@sentry/cloudflare (withSentry) 533.27 kB +1.94% +10.1 kB 🔺
⛔️ @sentry/nextjs/cloudflare (withSentry) - minified (max: 234.5 kB) 234.61 kB +1.78% +4.1 kB 🔺

View base workflow run

@isaacs
isaacs force-pushed the isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c branch from fc30522 to 5d3ee29 Compare October 21, 2026 01:06
@isaacs
isaacs marked this pull request as ready for review October 21, 2026 01:07
@isaacs
isaacs requested a review from a team as a code owner October 21, 2026 01:07
@isaacs
isaacs requested review from JPeer264 and andreiborza and removed request for a team October 21, 2026 01:07
@isaacs
isaacs force-pushed the isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c branch from 5d3ee29 to 7a1874d Compare October 21, 2026 01:07
@isaacs
isaacs requested a review from a team as a code owner October 21, 2026 01:13
@isaacs
isaacs requested review from chargome and nicohrubec and removed request for a team October 21, 2026 01:13
@isaacs
isaacs force-pushed the isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c branch from 7a1874d to 9e86e46 Compare October 21, 2026 01:13
isaacs and others added 6 commits October 20, 2026 18:15
Node, Deno, Bun and Cloudflare each have their own options for incoming
HTTP requests, and the option sets drift. Bun.serve takes only
`maxRequestBodySize`, and Cloudflare has no span options. When we add
an option to one runtime, nothing reminds us to add it to the others.

`ServerRequestOptions` in `@sentry/core/server` lists the options that
every incoming request integration should support. Each runtime options
type is checked against it with `CheckServerRequestOptions`. The check
lists the options that the runtime does not support yet, with a comment
that says why. It fails to compile when a shared option is neither
declared nor listed, when a listed option is declared, or when a
callback written for the shared type does not fit the runtime option.

The runtime types do not extend the shared type. With `extends`, a new
shared option would appear in every runtime type, supported or not, and
nothing would fail to compile. Each runtime keeps its own declarations
and docs.

Node and Bun now type the request in `ignoreRequestBody` as
`RequestOptions & HttpIncomingMessage`. The value was always an
`IncomingMessage`. Callbacks typed with `RequestOptions` still compile.
The `RequestOptions` part is deprecated, for removal in v12.

A new BUGBOT rule flags an incoming request option that applies to more
than one runtime but is not in the shared type.

There is no runtime behavior change.

Fixes JS-3968
Fixes #25570

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…egration`

`Bun.serve` took only `maxRequestBodySize`. A user could not skip body
capture or spans for one route, and release health showed no request
sessions or crash-free rate for `Bun.serve` apps. `denoServeIntegration`
got these in #25493 and #25496.

`bunServerIntegration` now accepts `ignoreRequestBody`,
`ignoreIncomingRequests`, `ignoreStaticAssets`, `spans`,
`onSpanCreated`, `onSpanEnd`, `sessions` and `sessionFlushingDelayMS`.
They use the same names and behavior as `denoServeIntegration`, and the
callbacks receive the web `Request`. The shared `ServerRequestOptions`
check for `Bun.serve` now lists no unsupported options.

When the SDK skips the span, it still continues the incoming trace,
attaches request data and the body, records the session and captures
errors. For a streamed response, `onSpanEnd` and the session end when
the stream finishes. A user callback that throws does not fail the
request. If `ignoreRequestBody` throws, the SDK skips the body, so it
cannot leak data that the filter was meant to hide.

Three changes in behavior:

- `ignoreStaticAssets` defaults to `true`, so `GET` requests for files
  such as `favicon.ico` or `*.js` no longer create spans.
- `spans` defaults to the client's tracing configuration, so the
  integration no longer starts spans when tracing is disabled.
- Errors thrown for `OPTIONS` and `HEAD` requests are now captured.
  These requests also record sessions, as on Node and Deno.

Fixes #25571
Fixes JS-3969

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`httpIntegration` takes `onSpanCreated` for incoming requests, but not
`onSpanEnd`. A user could not change the server span based on the
response, for example to rename it from data that is only known when
the response finishes. Core's `node:http` server code already supports
`onSpanEnd`, and `denoHttpIntegration` and `bunHttpServerIntegration`
pass it through.

`httpServerSpansIntegration` now accepts `onSpanEnd` and calls it as
core does: when the response finishes, after the SDK sets the response
attributes and the span status, before the span ends. `httpIntegration`
passes it through. The shared `ServerRequestOptions` check for Node now
lists no unsupported options.

Fixes #25572
Fixes JS-3970

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…egration`

Cloudflare's `httpServerIntegration` took only the body capture
options. A user could not skip spans for one route, such as a health
check, or change the request span from the request or the response.
Node, Deno and Bun all allow this.

`httpServerIntegration` now accepts `ignoreIncomingRequests`,
`ignoreStaticAssets`, `spans`, `onSpanCreated` and `onSpanEnd`. They
use the same names and behavior as `denoServeIntegration` and
`bunServerIntegration`, and the callbacks receive the web `Request`.
The shared `ServerRequestOptions` check for Cloudflare now lists only
the session options, which a worker cannot support because it has no
long-lived process to run the flush timer.

When the SDK skips the span, it still continues the incoming trace,
attaches request data and the body, and captures errors. It also still
waits for a streamed response to finish before it flushes. For a
streamed response, `onSpanEnd` runs when the stream finishes. It does
not run when the handler throws. A callback that throws does not fail
the request.

`ignoreRequestBody` also no longer fails the request when it throws.
The SDK skips the body instead, so it cannot leak data that the filter
was meant to hide. Deno and Bun already do this.

Two defaults change behavior:

- `ignoreStaticAssets` defaults to `true`, so `GET` requests for files
  such as `favicon.ico` or `*.js` no longer create spans.
- `spans` defaults to the client's tracing configuration, so the SDK no
  longer starts request spans when tracing is disabled.

Fixes #25573
Fixes JS-3971

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er SDKs

The server SDKs export different sets of helpers from `@sentry/core`
and `@sentry/server-utils`, although the helpers work on each runtime.
A user who moves between runtimes, or follows docs written for another
one, finds a helper missing. Two of the gaps also hide default
integrations: Node and Bun add `awsIntegration` to their defaults, and
Deno adds `fastifyIntegration`, but they did not export them. A user who
sets `defaultIntegrations` could not add them back.

- Node and Bun now export `moduleMetadataIntegration`,
  `instrumentPostgresJsSql`, `instrumentCreateReactAgent`,
  `awsIntegration` and the `FlueOptions` type. `@sentry/aws-serverless`
  and `@sentry/google-cloud-serverless` re-export the new Node values,
  as the consistent exports check requires.
- Deno and Cloudflare now export `parameterize` and
  `instrumentStateGraphCompile`, so all four runtimes export the same
  LangGraph helpers.
- Bun and Cloudflare now export the `Log` and `LogSeverityLevel` types.
- Deno now exports `fastifyIntegration`.

Cloudflare's channel integrations (express, pg and so on) are separate
work, because they need a warning when the vite plugin is missing.

Fixes #25574
Fixes JS-3972

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the `@sentry/cloudflare/vite` plugin bundles an instrumented
package, such as `pg` or `openai`, the SDK adds the matching channel
integration. It calls each factory with no arguments, so a Cloudflare
user always got the default options. They could not, for example, turn
off prompt and response capture with
`openAIIntegration({ recordInputs: false })`, as Node, Bun and Deno
users can.

`@sentry/cloudflare` now exports the channel integrations that Node
exports. These are the integrations that the vite plugin can register,
so their code already runs on workerd. A user instance passed in
`integrations` replaces the registered default with the same name, at
`init()` and also when the module loads after `init()`. Both packages
set `"sideEffects": false`, so a worker bundles only the integrations
it imports. The `withSentry` bundle does not change in size.

These integrations get data only when the vite plugin injected the
channels. Without the plugin, a configured integration records nothing.
On Cloudflare, `invokeOrchestrionInstrumentation` now logs a debug
warning, once for each integration, when the plugin did not run.

Fixes #25575
Fixes JS-3973

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@isaacs
isaacs force-pushed the isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c branch from 9e86e46 to a650894 Compare October 21, 2026 01:16
@isaacs
isaacs force-pushed the isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c branch from a650894 to 66b9312 Compare October 21, 2026 01:20

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 66b9312. Configure here.

isaacs and others added 7 commits October 20, 2026 21:13
The `@sentry/cloudflare/vite` plugin stores an integration factory for
each instrumented package when the package first runs. At `init()`, the
SDK adds the stored factories to the defaults. A package that runs after
`init()`, for example a driver loaded with a dynamic `import()`, emits
`orchestrion.module-injected`, and the SDK then called
`client.addIntegration(factory())`.

That handler did not check the user's options, so it ignored each way
to opt out: an `integrations` function that filtered the integration
out, `defaultIntegrations: false`, and a custom `defaultIntegrations`
list. The user got the integration back. For the AI integrations, that
can turn prompt and response capture back on. Whether it happened
depended on module load order.

The handler now applies the rules that `init()` applies to the defaults:

- If the user set `defaultIntegrations`, the SDK defaults are off, so
  the handler adds nothing.
- If `integrations` is a function, the handler passes the late
  integration through it with `getIntegrationsToSetup` and installs the
  result with the same name, if there is one. This calls the function
  again, once for each late module. If the function throws, the handler
  skips the integration, so the module load that emitted the event
  still works.
- If `integrations` is an array, nothing changes. An array cannot
  remove a default, and `addIntegration` already skips a name the user
  configured.

Fixes #25576
Fixes JS-3974

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deno.serve, Bun.serve and Cloudflare each wrap a web `Request` handler
with their own copy of the same steps: request data and body, the
`http.server` span, streamed responses, errors and sessions. The copies
drift, so a fix in one runtime does not reach the others.

This adds `wrapServerRequestHandler` to `@sentry/server-utils` and
moves `Deno.serve` onto it. Bun.serve and Cloudflare follow in separate
PRs, so each runtime's changes can be reviewed alone. The wrapper has
only what Deno needs today, and the next PRs extend it. It is exported
from the shared entry, so the Node-free
`@sentry/server-utils/no-diagnostic-channels` entry can use it too.

The rules follow core's `node:http` server code. `OPTIONS` and `HEAD`
requests get no span and no body capture, but still get request data,
a session and error capture. The static asset filter applies only to
`GET`. For streamed responses, the wrapper uses the stream monitor from
the Bun SDK, which reads only when the client pulls and reports errors
from the stream source. It replaces Deno's own copy, which read ahead
of the client and ignored source errors.

The existing Deno tests pass with no change. Visible changes for Deno:

- An error from the source of a streamed response is now captured, and
  the span gets an error status.
- The SDK no longer reads a streamed response ahead of the client.
- Errors in `OPTIONS` and `HEAD` requests now carry the request data.
- `client.address` is no longer set to the path of a Unix socket.

Part of #25577
Part of JS-3975

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves `Bun.serve` onto `wrapServerRequestHandler`, which `Deno.serve`
uses since the previous PR. Bun keeps only what is unique to it: the
patches for `fetch`, `routes` and `server.reload`, the route template,
the route parameters, and the socket address from `server.requestIP()`.
Its copy of the stream monitor moves out, because the wrapper uses the
same code from `@sentry/server-utils`.

The wrapper gets two optional fields for this. `routeName` passes the
matched route to core's span helper, which sets `http.route`, the
`route` source and the span name. `attributes` adds span attributes
that only the runtime knows, here `url.path.parameter.*`.

Bun built its own span attributes, which did not follow the Sentry
conventions for `http.server` spans. The conventions say that server
instrumentation sets the matched route in `http.route`, and that
`url.template` is for `http.client` spans and client-side routing. The
host goes in `server.address`, as on Node, Deno and Cloudflare. So Bun
spans change:

- `url.template` becomes `http.route`. Routes without parameters now
  also get `http.route` and the `route` source.
- `url.domain` becomes `server.address`.
- A request to `/` gets the `route` source and the name `GET /`, as on
  the other runtimes, because the path is already low cardinality.
- Spans also get `user_agent.original` and `http.request.body.size`.
- Errors in `OPTIONS` and `HEAD` requests now carry the request data.

The wrapper now also keeps every `Set-Cookie` header of a response.
`Headers.forEach` lists each one on its own, so a plain dict kept only
the last one. This fixes Deno too, and Cloudflare once it moves.

Part of #25577
Part of JS-3975

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves Cloudflare's `wrapRequestHandler` onto `wrapServerRequestHandler`,
which `Deno.serve` and `Bun.serve` use since the previous PRs. Cloudflare
keeps only what is unique to it: client init, the `cf` and cloud
resource contexts, the protocol from `cf.httpProtocol`, the
`isStreamingResponse` option, `captureErrors`, and the flush through
`waitUntil`, which skips the dispose for a 101 response. The helpers
that only the old code used (`captureIncomingRequestBody`,
`shouldCreateIncomingRequestSpan`, `getIncomingRequestSpanHooks` and
`addRequest`) are gone.

`wrapRequestHandler` must run on workers without the `nodejs_compat`
flag, such as Shopify Oxygen, and every `@sentry/server-utils` entry
loads `node:async_hooks`. So the wrapper moves to a new entry,
`@sentry/server-utils/request-handler`, with no Node.js imports. Deno
and Bun import it from there too. Cloudflare's module graph test allows
this one entry, and a new test in server-utils walks the entry's module
graph and fails on any Node.js import.

The wrapper gets three optional fields: `isStreamingResponse`, `onDone`
(called once when the request is done, with the response if there is
one), and a `mechanism` that can be left out to not capture errors.

For streamed responses, the shared stream monitor replaces Cloudflare's
`TransformStream`. Workers with a compatibility date before 2022-11-30
cannot construct a `ReadableStream` without the
`streams_enable_constructors` flag, so the wrapper then ends at handler
return, as the `TransformStream` fallback did. The monitor now locks the
source body only in `start()`, so the original response stays usable in
that case. The wrapper builds the new response from the original one,
which keeps `encodeBody: 'manual'`.

Visible changes for Cloudflare:

- Request spans set `client.address` from `CF-Connecting-IP` (or
  another forwarding header), as on the other runtimes. This follows
  `dataCollection.userInfo`, which is `true` by default.
- Request spans get the response header attributes, and events get the
  `response` context.
- An error from the source of a streamed response is captured (unless
  `captureErrors` is `false`), and the span gets an error status.
- If the user removed `httpServerIntegration`, request bodies follow
  `dataCollection.httpBodies`. Before, the SDK attached no body.

The minified `withSentry` bundle grows by about 2.4 KB (212,385 to
214,853 bytes), under the 211 KiB limit. Most of it is the request
session and client address code, which Cloudflare now shares.

Fixes #25577
Fixes JS-3975

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@isaacs
isaacs force-pushed the isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c branch from a1a478b to f0da1bc Compare October 21, 2026 04:14

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Share one  Request  handler wrapper across Deno.serve, Bun.serve and Cloudflare

1 participant