Skip to content

Commit fdd3adf

Browse files
committed
fix: simplify node browser routing helpers
Split browser.fetch into its own helper, remove unused browser transport code, and simplify withOptions cache sharing so the routing layer stays easier to reason about. Made-with: Cursor
1 parent 7a56ab6 commit fdd3adf

7 files changed

Lines changed: 199 additions & 262 deletions

File tree

src/client.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -342,21 +342,14 @@ export class Kernel {
342342
*/
343343
withOptions(options: Partial<ClientOptions>): this {
344344
const currentRouting = this._options.browserRouting;
345-
const nextBrowserRouting =
346-
options.browserRouting === undefined ?
345+
const nextBrowserRouting = options.browserRouting === undefined ? currentRouting : options.browserRouting;
346+
const sharedBrowserRouting =
347+
nextBrowserRouting ?
347348
{
348-
...(currentRouting ?? {}),
349-
cache: currentRouting?.cache ?? this.browserRouteCache,
349+
...nextBrowserRouting,
350+
cache: nextBrowserRouting.cache ?? this.browserRouteCache,
350351
}
351-
: options.browserRouting.enabled ?
352-
{
353-
...options.browserRouting,
354-
cache: options.browserRouting.cache ?? this.browserRouteCache,
355-
}
356-
: {
357-
...options.browserRouting,
358-
cache: options.browserRouting.cache ?? this.browserRouteCache,
359-
};
352+
: undefined;
360353

361354
const client = new (this.constructor as any as new (props: ClientOptions) => typeof this)({
362355
...this._options,
@@ -370,7 +363,7 @@ export class Kernel {
370363
fetchOptions: this.fetchOptions,
371364
apiKey: this.apiKey,
372365
...options,
373-
browserRouting: nextBrowserRouting,
366+
browserRouting: sharedBrowserRouting,
374367
});
375368
return client;
376369
}

src/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@ export { Kernel as default } from './client';
55
export { type Uploadable, toFile } from './core/uploads';
66
export { APIPromise } from './core/api-promise';
77
export { Kernel, type ClientOptions } from './client';
8-
export {
9-
BrowserRouteCache,
10-
type BrowserFetchInit,
11-
type BrowserRoute,
12-
type BrowserRoutingOptions,
13-
} from './lib/browser-routing';
8+
export { type BrowserFetchInit } from './lib/browser-fetch';
9+
export { BrowserRouteCache, type BrowserRoute, type BrowserRoutingOptions } from './lib/browser-routing';
1410
export { PagePromise } from './core/pagination';
1511
export {
1612
KernelError,

src/lib/browser-fetch.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import type { RequestInfo, RequestInit } from '../internal/builtin-types';
2+
import { KernelError } from '../core/error';
3+
import { buildHeaders } from '../internal/headers';
4+
import type { FinalRequestOptions, RequestOptions } from '../internal/request-options';
5+
import type { HTTPMethod } from '../internal/types';
6+
import type { Kernel } from '../client';
7+
8+
export interface BrowserFetchInit extends RequestInit {
9+
timeout_ms?: number;
10+
}
11+
12+
export async function browserFetch(
13+
client: Kernel,
14+
sessionId: string,
15+
input: RequestInfo | URL,
16+
init?: BrowserFetchInit,
17+
): Promise<Response> {
18+
const route = client.browserRouteCache.get(sessionId);
19+
if (!route) {
20+
throw new KernelError(
21+
`browser route cache does not contain session ${sessionId}; create, retrieve, or list the browser before calling browser.fetch`,
22+
);
23+
}
24+
25+
const { url: targetURL, method, headers, body, signal, duplex, timeout_ms } = splitFetchArgs(input, init);
26+
assertHTTPURL(targetURL);
27+
28+
const query: Record<string, unknown> = { url: targetURL, jwt: route.jwt };
29+
if (timeout_ms !== undefined) {
30+
query['timeout_ms'] = timeout_ms;
31+
}
32+
33+
const accept = headers.get('accept');
34+
const requestOptions: FinalRequestOptions = {
35+
method: normalizeMethod(method),
36+
path: joinURL(route.baseURL, '/curl/raw'),
37+
query,
38+
body: body as RequestOptions['body'],
39+
headers: buildHeaders([
40+
{ Authorization: null },
41+
accept ? { Accept: accept } : { Accept: '*/*' },
42+
headersToRequestOptionsHeaders(headers),
43+
]),
44+
signal: signal ?? null,
45+
__binaryResponse: true,
46+
};
47+
if (duplex) {
48+
requestOptions.fetchOptions = { duplex } as NonNullable<RequestOptions['fetchOptions']>;
49+
}
50+
51+
return client.request(requestOptions).asResponse();
52+
}
53+
54+
function normalizeMethod(method: string): HTTPMethod {
55+
const methodLower = method.toLowerCase();
56+
const allowed = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options']);
57+
if (!allowed.has(methodLower)) {
58+
throw new KernelError(`browser.fetch unsupported HTTP method: ${method}`);
59+
}
60+
return methodLower as HTTPMethod;
61+
}
62+
63+
function splitFetchArgs(
64+
input: RequestInfo | URL,
65+
init?: BrowserFetchInit,
66+
): {
67+
url: string;
68+
method: string;
69+
headers: Headers;
70+
body?: RequestInit['body'];
71+
signal?: AbortSignal | null;
72+
duplex?: RequestInit['duplex'];
73+
timeout_ms?: number;
74+
} {
75+
const timeoutFromInit = init && 'timeout_ms' in init ? init['timeout_ms'] : undefined;
76+
77+
if (input instanceof Request) {
78+
const headers = new Headers(input.headers);
79+
if (init?.headers) {
80+
const extra = new Headers(init.headers);
81+
extra.forEach((value, key) => {
82+
headers.set(key, value);
83+
});
84+
}
85+
86+
const out: {
87+
url: string;
88+
method: string;
89+
headers: Headers;
90+
body?: RequestInit['body'];
91+
signal?: AbortSignal | null;
92+
duplex?: RequestInit['duplex'];
93+
timeout_ms?: number;
94+
} = {
95+
url: input.url,
96+
method: (init?.method ?? input.method)?.toUpperCase() || 'GET',
97+
headers,
98+
};
99+
const body = init?.body ?? input.body;
100+
if (body !== undefined && body !== null) {
101+
out.body = body;
102+
}
103+
const signal = init?.signal ?? input.signal;
104+
if (signal !== undefined) {
105+
out.signal = signal;
106+
}
107+
if (init?.duplex !== undefined) {
108+
out.duplex = init.duplex;
109+
}
110+
if (timeoutFromInit !== undefined) {
111+
out.timeout_ms = timeoutFromInit;
112+
}
113+
return out;
114+
}
115+
116+
const out: {
117+
url: string;
118+
method: string;
119+
headers: Headers;
120+
body?: RequestInit['body'];
121+
signal?: AbortSignal | null;
122+
duplex?: RequestInit['duplex'];
123+
timeout_ms?: number;
124+
} = {
125+
url: input instanceof URL ? input.href : String(input),
126+
method: (init?.method ?? 'GET').toUpperCase(),
127+
headers: new Headers(init?.headers),
128+
};
129+
if (init?.body !== undefined) {
130+
out.body = init.body;
131+
}
132+
if (init?.signal !== undefined) {
133+
out.signal = init.signal;
134+
}
135+
if (init?.duplex !== undefined) {
136+
out.duplex = init.duplex;
137+
}
138+
if (timeoutFromInit !== undefined) {
139+
out.timeout_ms = timeoutFromInit;
140+
}
141+
return out;
142+
}
143+
144+
function assertHTTPURL(url: string): void {
145+
let parsed: URL;
146+
try {
147+
parsed = new URL(url);
148+
} catch {
149+
throw new KernelError(`browser.fetch target must be an absolute URL; received: ${url}`);
150+
}
151+
152+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
153+
throw new KernelError(`browser.fetch only supports http(s) URLs; received: ${parsed.protocol}`);
154+
}
155+
}
156+
157+
function headersToRequestOptionsHeaders(headers: Headers): Record<string, string | null | undefined> {
158+
const out: Record<string, string | null | undefined> = {};
159+
160+
headers.forEach((value, key) => {
161+
switch (key.toLowerCase()) {
162+
case 'accept':
163+
case 'content-length':
164+
case 'connection':
165+
case 'keep-alive':
166+
case 'proxy-authenticate':
167+
case 'proxy-authorization':
168+
case 'te':
169+
case 'trailers':
170+
case 'transfer-encoding':
171+
case 'upgrade':
172+
return;
173+
default:
174+
out[key] = value;
175+
}
176+
});
177+
178+
return out;
179+
}
180+
181+
function joinURL(baseURL: string, path: string): string {
182+
return `${baseURL.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`;
183+
}

0 commit comments

Comments
 (0)