Skip to content

Commit 7d907b1

Browse files
dsp-antclaude
andcommitted
fix: Pass RequestInit options to auth requests
Fixes an issue where custom headers (like user-agent) and other RequestInit options set when creating transports were not being passed through to authorization requests (.well-known/ discovery, token exchange, DCR, etc.). Changes: - Created createFetchWithInit() utility in shared/transport.ts to wrap fetch with base RequestInit options - Transports now wrap their fetch function before passing to auth module - All RequestInit options (headers, credentials, mode, etc.) are now preserved - Auth-specific headers properly override base headers when needed - Extracted normalizeHeaders() to shared/transport.ts for reuse 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a7e525a commit 7d907b1

File tree

3 files changed

+64
-20
lines changed

3 files changed

+64
-20
lines changed

src/client/sse.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EventSource, type ErrorEvent, type EventSourceInit } from 'eventsource';
2-
import { Transport, FetchLike } from '../shared/transport.js';
2+
import { Transport, FetchLike, createFetchWithInit } from '../shared/transport.js';
33
import { JSONRPCMessage, JSONRPCMessageSchema } from '../types.js';
44
import { auth, AuthResult, extractWWWAuthenticateParams, OAuthClientProvider, UnauthorizedError } from './auth.js';
55

@@ -93,11 +93,14 @@ export class SSEClientTransport implements Transport {
9393

9494
let result: AuthResult;
9595
try {
96+
// Wrap fetch to automatically include base RequestInit options
97+
const fetchFn = createFetchWithInit(this._fetch, this._requestInit);
98+
9699
result = await auth(this._authProvider, {
97100
serverUrl: this._url,
98101
resourceMetadataUrl: this._resourceMetadataUrl,
99102
scope: this._scope,
100-
fetchFn: this._fetch
103+
fetchFn
101104
});
102105
} catch (error) {
103106
this.onerror?.(error as Error);
@@ -215,12 +218,15 @@ export class SSEClientTransport implements Transport {
215218
throw new UnauthorizedError('No auth provider');
216219
}
217220

221+
// Wrap fetch to automatically include base RequestInit options
222+
const fetchFn = createFetchWithInit(this._fetch, this._requestInit);
223+
218224
const result = await auth(this._authProvider, {
219225
serverUrl: this._url,
220226
authorizationCode,
221227
resourceMetadataUrl: this._resourceMetadataUrl,
222228
scope: this._scope,
223-
fetchFn: this._fetch
229+
fetchFn
224230
});
225231
if (result !== 'AUTHORIZED') {
226232
throw new UnauthorizedError('Failed to authorize');
@@ -256,11 +262,14 @@ export class SSEClientTransport implements Transport {
256262
this._resourceMetadataUrl = resourceMetadataUrl;
257263
this._scope = scope;
258264

265+
// Wrap fetch to automatically include base RequestInit options
266+
const fetchFn = createFetchWithInit(this._fetch, this._requestInit);
267+
259268
const result = await auth(this._authProvider, {
260269
serverUrl: this._url,
261270
resourceMetadataUrl: this._resourceMetadataUrl,
262271
scope: this._scope,
263-
fetchFn: this._fetch
272+
fetchFn
264273
});
265274
if (result !== 'AUTHORIZED') {
266275
throw new UnauthorizedError();

src/client/streamableHttp.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Transport, FetchLike } from '../shared/transport.js';
1+
import { Transport, FetchLike, createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
22
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from '../types.js';
33
import { auth, AuthResult, extractWWWAuthenticateParams, OAuthClientProvider, UnauthorizedError } from './auth.js';
44
import { EventSourceParserStream } from 'eventsource-parser/stream';
@@ -156,11 +156,14 @@ export class StreamableHTTPClientTransport implements Transport {
156156

157157
let result: AuthResult;
158158
try {
159+
// Wrap fetch to automatically include base RequestInit options
160+
const fetchFn = createFetchWithInit(this._fetch, this._requestInit);
161+
159162
result = await auth(this._authProvider, {
160163
serverUrl: this._url,
161164
resourceMetadataUrl: this._resourceMetadataUrl,
162165
scope: this._scope,
163-
fetchFn: this._fetch
166+
fetchFn
164167
});
165168
} catch (error) {
166169
this.onerror?.(error as Error);
@@ -190,7 +193,7 @@ export class StreamableHTTPClientTransport implements Transport {
190193
headers['mcp-protocol-version'] = this._protocolVersion;
191194
}
192195

193-
const extraHeaders = this._normalizeHeaders(this._requestInit?.headers);
196+
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
194197

195198
return new Headers({
196199
...headers,
@@ -255,19 +258,6 @@ export class StreamableHTTPClientTransport implements Transport {
255258
return Math.min(initialDelay * Math.pow(growFactor, attempt), maxDelay);
256259
}
257260

258-
private _normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
259-
if (!headers) return {};
260-
261-
if (headers instanceof Headers) {
262-
return Object.fromEntries(headers.entries());
263-
}
264-
265-
if (Array.isArray(headers)) {
266-
return Object.fromEntries(headers);
267-
}
268-
269-
return { ...(headers as Record<string, string>) };
270-
}
271261

272262
/**
273263
* Schedule a reconnection attempt with exponential backoff

src/shared/transport.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@ import { JSONRPCMessage, MessageExtraInfo, RequestId } from '../types.js';
22

33
export type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
44

5+
/**
6+
* Normalizes HeadersInit to a plain Record<string, string> for manipulation.
7+
* Handles Headers objects, arrays of tuples, and plain objects.
8+
*/
9+
export function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> {
10+
if (!headers) return {};
11+
12+
if (headers instanceof Headers) {
13+
return Object.fromEntries(headers.entries());
14+
}
15+
16+
if (Array.isArray(headers)) {
17+
return Object.fromEntries(headers);
18+
}
19+
20+
return { ...(headers as Record<string, string>) };
21+
}
22+
23+
/**
24+
* Creates a fetch function that includes base RequestInit options.
25+
* This ensures requests inherit settings like credentials, mode, headers, etc. from the base init.
26+
*
27+
* @param baseFetch - The base fetch function to wrap (defaults to global fetch)
28+
* @param baseInit - The base RequestInit to merge with each request
29+
* @returns A wrapped fetch function that merges base options with call-specific options
30+
*/
31+
export function createFetchWithInit(baseFetch: FetchLike = fetch, baseInit?: RequestInit): FetchLike {
32+
if (!baseInit) {
33+
return baseFetch;
34+
}
35+
36+
// Return a wrapped fetch that merges base RequestInit with call-specific init
37+
return async (url: string | URL, init?: RequestInit): Promise<Response> => {
38+
const mergedInit: RequestInit = {
39+
...baseInit,
40+
...init,
41+
// Headers need special handling - merge instead of replace
42+
headers: init?.headers
43+
? { ...normalizeHeaders(baseInit.headers), ...normalizeHeaders(init.headers) }
44+
: baseInit.headers
45+
};
46+
return baseFetch(url, mergedInit);
47+
};
48+
}
49+
550
/**
651
* Options for sending a JSON-RPC message.
752
*/

0 commit comments

Comments
 (0)