Skip to content

Commit 5d3ee29

Browse files
isaacsclaude
andcommitted
fix(cloudflare): Keep request scopes for streamed responses and the body opt-out
Two Cloudflare integration tests failed after the move to the shared request handler wrapper. Without `nodejs_compat`, Cloudflare has no `AsyncLocalStorage`. The wrapper ends the span of a streamed response from a stream callback, after the handler returns, and that callback ran outside the request's scopes. There `getClient()` found no client, so core sent the segment span as a static transaction instead of streaming it, and a captured stream error would not update the request session. The wrapper now runs these callbacks in the request's isolation scope and current scope. Before the move, removing `httpServerIntegration` turned off request body capture on Cloudflare. The wrapper used `dataCollection.httpBodies` instead, so bodies were attached. Cloudflare now passes `maxRequestBodySize: 'none'` when the integration is missing, and the CHANGELOG no longer lists that change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 6a844a0 commit 5d3ee29

3 files changed

Lines changed: 37 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ Work in this release was contributed by @adityareddy-dev, @jpdefo, @Jxxunnn, @ph
122122
- Request spans now set `client.address` from the `CF-Connecting-IP` header (or another forwarding header), as on the other runtimes. This follows `dataCollection.userInfo`, which is `true` by default. To leave it out, set `dataCollection: { userInfo: false }`.
123123
- Request spans now have the response header attributes, and events have the `response` context with the status code.
124124
- When the source of a streamed response fails, the SDK now captures the error and marks the span as failed. With `captureErrors: false`, it does not capture it.
125-
- If you removed `httpServerIntegration`, request bodies now follow `dataCollection.httpBodies`, as on the other runtimes. Before, the SDK attached no body.
126125

127126
## 11.8.0
128127

‎packages/cloudflare/src/wrapRequestHandlerWithInit.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ export function wrapRequestHandlerWithInit(
100100
request,
101101
client,
102102
isolationScope,
103-
options: client.getIntegrationByName<Integration & HttpServerIntegrationOptions>('HttpServer'),
103+
// Without the `HttpServer` integration, the SDK attaches no request body, so users can turn
104+
// body capture off by removing it.
105+
options: client.getIntegrationByName<Integration & HttpServerIntegrationOptions>('HttpServer') ?? {
106+
maxRequestBodySize: 'none',
107+
},
104108
origin: 'auto.http.cloudflare',
105109
mechanism: captureErrors ? { handled: false, type: getRequestErrorMechanismType(context) } : undefined,
106110
attributes,

‎packages/server-utils/src/utils/wrapServerRequestHandler.ts‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
captureBodyFromWinterCGRequest,
1414
captureException,
1515
continueTrace,
16+
getCurrentScope,
1617
getHttpSpanDetailsFromUrlObject,
1718
hasSpansEnabled,
1819
hasSpanStreamingEnabled,
@@ -25,6 +26,8 @@ import {
2526
startSpanManual,
2627
winterCGHeadersToDict,
2728
winterCGRequestToRequestData,
29+
withIsolationScope,
30+
withScope,
2831
} from '@sentry/core';
2932
import type { ServerRequestOptions } from '@sentry/core/server';
3033
import {
@@ -159,7 +162,13 @@ export async function wrapServerRequestHandler(
159162
throw e;
160163
}
161164
isolationScope.setContext('response', { status_code: response.status });
162-
return onResponseDone(response, isStreamingResponse(response), () => finish(response), captureError);
165+
const inRequestScope = bindToRequestScope(isolationScope);
166+
return onResponseDone(
167+
response,
168+
isStreamingResponse(response),
169+
inRequestScope(() => finish(response)),
170+
inRequestScope(captureError),
171+
);
163172
}
164173

165174
const [rawName, attributes] = getHttpSpanDetailsFromUrlObject(
@@ -235,10 +244,11 @@ export async function wrapServerRequestHandler(
235244
httpHeadersToSpanAttributes(responseHeadersToDict(response.headers), dataCollection, 'response'),
236245
);
237246

247+
const inRequestScope = bindToRequestScope(isolationScope);
238248
return onResponseDone(
239249
response,
240250
isStreamingResponse(response),
241-
() => {
251+
inRequestScope(() => {
242252
if (onSpanEnd) {
243253
safeCallback(
244254
DEBUG_BUILD ? 'Error in `onSpanEnd`:' : '',
@@ -248,17 +258,34 @@ export async function wrapServerRequestHandler(
248258
}
249259
span.end();
250260
finish(response);
251-
},
252-
error => {
261+
}),
262+
inRequestScope((error: unknown) => {
253263
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' });
254264
captureError(error);
255-
},
265+
}),
256266
);
257267
});
258268
},
259269
);
260270
}
261271

272+
/**
273+
* Returns a function that binds a callback to the request's isolation scope and the current scope.
274+
*
275+
* A streamed body finishes after the handler returns. Without `AsyncLocalStorage` (Cloudflare
276+
* Workers without `nodejs_compat`), its callbacks then run outside the request's scopes, where
277+
* `getClient()` finds no client: the segment span would be sent as a transaction, and a captured
278+
* error would not update the request session.
279+
*/
280+
function bindToRequestScope(
281+
isolationScope: Scope,
282+
): <A extends unknown[]>(fn: (...args: A) => void) => (...args: A) => void {
283+
const scope = getCurrentScope();
284+
return fn =>
285+
(...args) =>
286+
withIsolationScope(isolationScope, () => withScope(scope, () => fn(...args)));
287+
}
288+
262289
function shouldCreateSpan(
263290
client: Client,
264291
options: ServerRequestOptions<Request, Response> | undefined,

0 commit comments

Comments
 (0)