Skip to content

Commit 13c70df

Browse files
committed
fix(fetch): preserve Content-Type header for SDK requests on Node.js
The @sentry/api SDK constructs a Request object with Content-Type set, then calls fetch(request) with no init argument. Our authenticatedFetch was reading headers only from init?.headers, which is undefined in this case — resulting in new Headers(undefined) (empty), losing the SDK's Content-Type. On Node.js, fetch(request, {headers}) replaces the Request's headers per spec, so the server received an empty Content-Type and returned HTTP 415. Fix prepareHeaders to fall back to the Request object's headers when init is undefined. Also correct method extraction for span tracing when called with only a Request object. Fixes CLI-65
1 parent 24fe62c commit 13c70df

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)