Skip to content

Commit 1ff4115

Browse files
authored
fix(fetch): preserve Content-Type header for SDK requests on Node.js (#276)
## Problem `sentry issue explain` and `sentry issue plan` were failing with `Error: Unsupported media type "" in request.` (HTTP 415) exclusively on the Node.js runtime (the npm package). Bun builds were unaffected. Tracked as CLI-65, affecting 2 users across 9 occurrences. ## Root Cause The `@sentry/api` SDK constructs a `Request` object with `Content-Type: application/json` in its headers, then calls `_fetch(request2)` with a single argument (no `init`). Our `authenticatedFetch` received `init = undefined`, so `prepareHeaders` created `new Headers(undefined)` — empty headers — losing the SDK's `Content-Type`. `fetchWithTimeout` then called `fetch(request, { ...init, headers, signal })`. Per the Fetch spec, passing an `init` with `headers` to `fetch(Request, init)` **replaces** the Request's headers entirely. The server received a POST with no `Content-Type` and returned HTTP 415. Bun merges headers from the Request object in this scenario rather than replacing them, masking the bug there. ## Fix In `prepareHeaders`, fall back to the `Request` object's headers when `init` is undefined: ```ts const sourceHeaders = init?.headers ?? (input instanceof Request ? input.headers : undefined); ``` Also corrects method extraction for HTTP span tracing when the SDK passes only a `Request` object (was always recording `GET`, now correctly records the Request's method).
1 parent 0a8e649 commit 1ff4115

File tree

1 file changed

+20
-4
lines changed

1 file changed

+20
-4
lines changed

src/lib/sentry-client.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,27 @@ function isUserAbort(error: unknown, signal?: AbortSignal | null): boolean {
5050
* - apiRequestToRegion always sends JSON and sets it explicitly
5151
* - rawApiRequest may or may not want Content-Type (e.g., string bodies)
5252
*
53+
* When `init` is undefined (the SDK passes only a Request object), headers are
54+
* read from the Request object to preserve Content-Type and other headers set
55+
* by the SDK. Without this, fetch(Request, {headers}) would override the
56+
* Request's headers with our empty headers, stripping Content-Type and causing
57+
* HTTP 415 errors on Node.js (which strictly follows the spec).
58+
*
5359
* The returned Headers instance is intentionally shared and mutated across
5460
* retry attempts (e.g., handleUnauthorized updates the Authorization header
5561
* and sets the retry marker). Do not clone before passing to retry logic.
5662
*/
57-
function prepareHeaders(init: RequestInit | undefined, token: string): Headers {
58-
const headers = new Headers(init?.headers);
63+
function prepareHeaders(
64+
input: Request | string | URL,
65+
init: RequestInit | undefined,
66+
token: string
67+
): Headers {
68+
// When the SDK calls fetch(request) with no init, read headers from the Request
69+
// object to preserve Content-Type. On Node.js, fetch(request, {headers}) replaces
70+
// the Request's headers entirely per spec, so we must carry them forward explicitly.
71+
const sourceHeaders =
72+
init?.headers ?? (input instanceof Request ? input.headers : undefined);
73+
const headers = new Headers(sourceHeaders);
5974
headers.set("Authorization", `Bearer ${token}`);
6075
if (!headers.has("User-Agent")) {
6176
headers.set("User-Agent", getUserAgent());
@@ -211,12 +226,13 @@ function createAuthenticatedFetch(): (
211226
input: Request | string | URL,
212227
init?: RequestInit
213228
): Promise<Response> {
214-
const method = init?.method ?? "GET";
229+
const method =
230+
init?.method ?? (input instanceof Request ? input.method : "GET");
215231
const urlPath = extractUrlPath(input);
216232

217233
return withHttpSpan(method, urlPath, async () => {
218234
const { token } = await refreshToken();
219-
const headers = prepareHeaders(init, token);
235+
const headers = prepareHeaders(input, init, token);
220236

221237
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
222238
const isLastAttempt = attempt === MAX_RETRIES;

0 commit comments

Comments
 (0)