Skip to content

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

Open

Description

@isaacs

Deno.serve, Bun.serve and Cloudflare each wrap a web-standard (Request) => Response handler. Each has its own copy of the same steps:

  • isolation scope and request data
  • client IP address
  • request body capture
  • continueTrace and the http.server span
  • span name rules for span streaming
  • response status and headers on the span
  • end the span when a streamed response finishes
  • capture errors that the handler throws
  • request sessions (Deno only)

The files are packages/deno/src/wrap-deno-request-handler.ts,packages/bun/src/integrations/bunserver.ts andpackages/cloudflare/src/wrapRequestHandlerWithInit.ts, about 900 lines together.

The copies drift. A fix or feature in one runtime does not reach the others. Examples today:

  • Bun.serve returns early for OPTIONS and HEAD, with no error capture. Cloudflare captures errors for them. Deno records sessions for them.
  • Deno and Bun skip body capture for GET. Cloudflare skips GET, HEAD and OPTIONS.
  • Deno and Bun each have a utils/streaming.ts with a different monitorStream. Bun's reads only when the client pulls and captures errors from the stream source. Deno's reads ahead of the client, with no backpressure, and does not capture stream errors. Cloudflare has a third version inline.
  • Deno.serve has sessions and the full option set (feat(deno): Add incoming request options to denoServeIntegration #25493, feat(deno): Record request sessions for Deno.serve #25496). Bun.serve and Cloudflare do not.

The shared ServerRequestOptions type (#25538) keeps option names in sync, but not behavior.

Proposed solution

Add a wrapper to @sentry/server-utils. That package is internal, so the wrapper can change as runtimes move onto it. Every runtime SDK already depends on it.

export function wrapServerRequestHandler(
  config: {
    request: Request;
    options: ServerRequestOptions<Request, Response>;
    origin: string; // e.g. 'auto.http.deno.serve'
    getClientAddress?: () => { address: string; port?: number } | undefined;
    getRouteAttributes?: () => SpanAttributes | undefined; // Bun route templates
    isStreamingResponse?: (response: Response) => boolean | undefined;
    onFinish?: () => void; // Cloudflare flush via waitUntil
  },
  handler: () => Response | Promise<Response>,
): Promise<Response>;

The wrapper owns every shared step in the list above, and it reads options for the shared rules. Each runtime keeps only what is unique to it:

  • Deno: reads the address from Deno.ServeHandlerInfo, and finds the handler in the Deno.servecall forms.
  • Bun: patches fetch, routes and server.reload. Gives route templates and params, and calls server.requestIP().
  • Cloudflare: client init and caching, cf context, cloud resource context, the isStreamingResponse option, flush through waitUntil. It passes sessions: false.

One behavior for all three, taken from core's node:http server code (packages/core/src/integrations/http/server-subscription.ts), so web and node:http match:

  • OPTIONS and HEAD: no span. Still isolate the request, attach request data, record a session and capture errors.
  • Static asset filter: GET only.
  • Body capture: skip GET, HEAD and OPTIONS, then apply ignoreRequestBody.
  • Streaming: keep Bun's monitorStream (no read-ahead, captures source errors) and delete the other two.

Plan for the PR: move one runtime at a time, in separate commits, Deno first because it has the full option set. The existing tests of each runtime must pass with no change, except tests that assert one of the behavior differences above. List those changes in the PR description, because users can see them.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions