Description
@sentry/astro auto-registers an Astro middleware that wraps every dynamic SSR response via injectMetaTagsInResponse(originalResponse, traceMetaTags) to inject <meta name="sentry-trace"> tags. The wrap uses the well-known broken new Response(body, originalResponse) clone pattern, which silently drops X-Frame-Options, Permissions-Policy, Cross-Origin-Opener-Policy, and (some observers report) Content-Security-Policy in the Cloudflare Workers runtime when copying from a Worker-originating response.
Source reference
@sentry/astro v10.49 (and current 10.53):
build/esm/server/middleware.js — injectMetaTagsInResponse:
function injectMetaTagsInResponse(originalResponse, metaTagsStr) {
try {
// ...stream transform setup...
return new Response(newResponseStream, originalResponse); // <-- broken pattern
} catch (e) {
sendErrorToSentry(e);
throw e;
}
}
This function is called from handleStaticRoute, enhanceHttpServerSpan, and instrumentRequestStartHttpServerSpan — every Astro SSR route in environments where @sentry/astro registered itself via addMiddleware() in astro:config:setup.
Reproduction conditions
- Astro
output: "server" with @astrojs/cloudflare v13 adapter
@sentry/astro v10.40+ in integrations: [sentry()]
PUBLIC_SENTRY_DSN env var set at build time so the integration is active
- A downstream layer (e.g. a Pages Functions middleware, or in our case
apps/<app>/functions/_middleware.ts) that stamps response headers expecting them to survive
Empirical evidence
In our setup (mcluckie-marketing-astro / Riney Electric) the applySecurityHeaders and runtime-CSP middleware in apps/<app>/functions/_middleware.ts stamp the BASE security floor onto every response after context.next() returns. The four headers above are correctly set when Sentry is NOT enabled (build without PUBLIC_SENTRY_DSN) but are silently dropped when Sentry IS enabled.
Suggested fix
Replace the broken clone pattern with the explicit init form:
function injectMetaTagsInResponse(originalResponse, metaTagsStr) {
try {
// ...stream transform setup...
return new Response(newResponseStream, {
status: originalResponse.status,
statusText: originalResponse.statusText,
headers: new Headers(originalResponse.headers),
});
} catch (e) {
sendErrorToSentry(e);
throw e;
}
}
This is the same pattern Sentry has already adopted elsewhere — e.g., @sentry/cloudflare's wrapRequestHandler (build/esm/request.js) uses { status, statusText, headers: res.headers }. Applying it here aligns @sentry/astro with that prior art and avoids the silent-drop in Workers runtimes.
Workaround currently deployed
We've added a defense-in-depth final-pass that materializes upstream headers via .entries() iteration (bypassing any guarded-Headers semantics) and re-stamps the security floor after Sentry's wrap:
export function assertSecurityFloor(response: Response, headers = NON_CSP_SECURITY_FLOOR): Response {
const entries: [string, string][] = [];
for (const [name, value] of response.headers.entries()) {
entries.push([name, value]);
}
const next = new Headers(entries);
for (const [name, value] of Object.entries(headers)) {
next.set(name, value);
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: next,
});
}
This is paranoid and works, but should not be necessary if injectMetaTagsInResponse is fixed upstream.
Severity
Medium. Affects security headers (defense-in-depth) but not core functionality. Easy to miss in CI because Node's undici doesn't drop headers across new Response(body, response) — only the workerd runtime does.
Description
@sentry/astroauto-registers an Astro middleware that wraps every dynamic SSR response viainjectMetaTagsInResponse(originalResponse, traceMetaTags)to inject<meta name="sentry-trace">tags. The wrap uses the well-known brokennew Response(body, originalResponse)clone pattern, which silently dropsX-Frame-Options,Permissions-Policy,Cross-Origin-Opener-Policy, and (some observers report)Content-Security-Policyin the Cloudflare Workers runtime when copying from a Worker-originating response.Source reference
@sentry/astrov10.49 (and current 10.53):build/esm/server/middleware.js—injectMetaTagsInResponse:This function is called from
handleStaticRoute,enhanceHttpServerSpan, andinstrumentRequestStartHttpServerSpan— every Astro SSR route in environments where@sentry/astroregistered itself viaaddMiddleware()inastro:config:setup.Reproduction conditions
output: "server"with@astrojs/cloudflarev13 adapter@sentry/astrov10.40+ inintegrations: [sentry()]PUBLIC_SENTRY_DSNenv var set at build time so the integration is activeapps/<app>/functions/_middleware.ts) that stamps response headers expecting them to surviveEmpirical evidence
In our setup (mcluckie-marketing-astro / Riney Electric) the
applySecurityHeadersand runtime-CSP middleware inapps/<app>/functions/_middleware.tsstamp the BASE security floor onto every response aftercontext.next()returns. The four headers above are correctly set when Sentry is NOT enabled (build withoutPUBLIC_SENTRY_DSN) but are silently dropped when Sentry IS enabled.Suggested fix
Replace the broken clone pattern with the explicit init form:
This is the same pattern Sentry has already adopted elsewhere — e.g.,
@sentry/cloudflare'swrapRequestHandler(build/esm/request.js) uses{ status, statusText, headers: res.headers }. Applying it here aligns@sentry/astrowith that prior art and avoids the silent-drop in Workers runtimes.Workaround currently deployed
We've added a defense-in-depth final-pass that materializes upstream headers via
.entries()iteration (bypassing any guarded-Headers semantics) and re-stamps the security floor after Sentry's wrap:This is paranoid and works, but should not be necessary if
injectMetaTagsInResponseis fixed upstream.Severity
Medium. Affects security headers (defense-in-depth) but not core functionality. Easy to miss in CI because Node's undici doesn't drop headers across
new Response(body, response)— only the workerd runtime does.