Repository navigation
ref(cloudflare): Use the shared request handler wrapper - #25588
Conversation
isaacs
added this pull request to stack #25589
October 20, 2026 22:30
Contributor
size-limit report 📦
|
isaacs
force-pushed
the
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
branch
from
October 21, 2026 01:06
fc30522 to
5d3ee29
Compare
isaacs
marked this pull request as ready for review
October 21, 2026 01:07
isaacs
requested review from
JPeer264 and
andreiborza
and removed request for
a team
October 21, 2026 01:07
isaacs
force-pushed
the
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
branch
from
October 21, 2026 01:07
5d3ee29 to
7a1874d
Compare
isaacs
requested review from
chargome and
nicohrubec
and removed request for
a team
October 21, 2026 01:13
isaacs
force-pushed
the
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
branch
from
October 21, 2026 01:13
7a1874d to
9e86e46
Compare
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
force-pushed
the
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
branch
from
October 21, 2026 01:16
9e86e46 to
a650894
Compare
isaacs
force-pushed
the
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
branch
from
October 21, 2026 01:20
a650894 to
66b9312
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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
force-pushed
the
isaacschlueter/js-3975-share-one-request-handler-wrapper-across-denoserve-bunserve-c
branch
from
October 21, 2026 04:14
a1a478b to
f0da1bc
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Moves Cloudflare's
wrapRequestHandlerontowrapServerRequestHandler, whichDeno.serveandBun.serveuse since the previous PRs. Cloudflare keeps only what is unique to it: client init, thecfand cloud resource contexts, the protocol fromcf.httpProtocol, theisStreamingResponseoption,captureErrors, and the flush throughwaitUntil, which skips the dispose for a 101 response. The helpers that only the old code used (captureIncomingRequestBody,shouldCreateIncomingRequestSpan,getIncomingRequestSpanHooksandaddRequest) are gone.wrapRequestHandlermust run on workers without thenodejs_compatflag, such as Shopify Oxygen, and every@sentry/server-utilsentry loadsnode: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 amechanismthat 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 aReadableStreamwithout thestreams_enable_constructorsflag, so the wrapper then ends at handler return, as theTransformStreamfallback did. The monitor now locks the source body only instart(), so the original response stays usable in that case. The wrapper builds the new response from the original one, which keepsencodeBody: 'manual'.Visible changes for Cloudflare:
client.addressfromCF-Connecting-IP(or another forwarding header), as on the other runtimes. This followsdataCollection.userInfo, which istrueby default.responsecontext.captureErrorsisfalse), and the span gets an error status.httpServerIntegration, request bodies followdataCollection.httpBodies. Before, the SDK attached no body.The minified
withSentrybundle 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