1- import type { Fetch } from '../internal/builtin-types' ;
1+ import type { RequestInfo , RequestInit , Fetch } 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' ;
26import { parseJwtFromCdpWsUrl } from './browser-transport' ;
7+ import type { Kernel } from '../client' ;
38
49export type BrowserRoute = {
510 sessionId : string ;
@@ -13,6 +18,10 @@ export interface BrowserRoutingOptions {
1318 cache ?: BrowserRouteCache | undefined ;
1419}
1520
21+ export interface BrowserFetchInit extends RequestInit {
22+ timeout_ms ?: number ;
23+ }
24+
1625export class BrowserRouteCache {
1726 private entries = new Map < string , BrowserRoute > ( ) ;
1827
@@ -60,6 +69,51 @@ export function createRoutingFetch(
6069 } ;
6170}
6271
72+ export async function browserFetch (
73+ client : Kernel ,
74+ sessionId : string ,
75+ input : RequestInfo | URL ,
76+ init ?: BrowserFetchInit ,
77+ ) : Promise < Response > {
78+ const route = client . browserRouteCache . get ( sessionId ) ;
79+ if ( ! route ) {
80+ throw new KernelError (
81+ `browser route cache does not contain session ${ sessionId } ; create, retrieve, or list the browser before calling browser.fetch` ,
82+ ) ;
83+ }
84+
85+ const { url : targetURL , method, headers, body, signal, duplex, timeout_ms } = splitFetchArgs ( input , init ) ;
86+ assertHTTPURL ( targetURL ) ;
87+
88+ const query : Record < string , unknown > = {
89+ url : targetURL ,
90+ jwt : route . jwt ,
91+ } ;
92+ if ( timeout_ms !== undefined ) {
93+ query [ 'timeout_ms' ] = timeout_ms ;
94+ }
95+
96+ const accept = headers . get ( 'accept' ) ;
97+ const requestOptions : FinalRequestOptions = {
98+ method : normalizeMethod ( method ) ,
99+ path : joinURL ( route . baseURL , '/curl/raw' ) ,
100+ query,
101+ body : body as RequestOptions [ 'body' ] ,
102+ headers : buildHeaders ( [
103+ { Authorization : null } ,
104+ accept ? { Accept : accept } : { Accept : '*/*' } ,
105+ headersToRequestOptionsHeaders ( headers ) ,
106+ ] ) ,
107+ signal : signal ?? null ,
108+ __binaryResponse : true ,
109+ } ;
110+ if ( duplex ) {
111+ requestOptions . fetchOptions = { duplex } as NonNullable < RequestOptions [ 'fetchOptions' ] > ;
112+ }
113+
114+ return client . request ( requestOptions ) . asResponse ( ) ;
115+ }
116+
63117function browserRouteFromValue ( value : unknown ) : BrowserRoute | undefined {
64118 if ( ! value || typeof value !== 'object' ) {
65119 return undefined ;
@@ -180,3 +234,126 @@ function joinURL(baseURL: string, path: string): string {
180234 return `${ baseURL . replace ( / \/ + $ / , '' ) } ${ path . startsWith ( '/' ) ? path : `/${ path } ` } ` ;
181235}
182236
237+ function normalizeMethod ( method : string ) : HTTPMethod {
238+ const methodLower = method . toLowerCase ( ) ;
239+ const allowed = new Set ( [ 'get' , 'post' , 'put' , 'patch' , 'delete' , 'head' , 'options' ] ) ;
240+ if ( ! allowed . has ( methodLower ) ) {
241+ throw new KernelError ( `browser.fetch unsupported HTTP method: ${ method } ` ) ;
242+ }
243+ return methodLower as HTTPMethod ;
244+ }
245+
246+ function splitFetchArgs (
247+ input : RequestInfo | URL ,
248+ init ?: BrowserFetchInit ,
249+ ) : {
250+ url : string ;
251+ method : string ;
252+ headers : Headers ;
253+ body ?: RequestInit [ 'body' ] ;
254+ signal ?: AbortSignal | null ;
255+ duplex ?: RequestInit [ 'duplex' ] ;
256+ timeout_ms ?: number ;
257+ } {
258+ const timeoutFromInit = init && 'timeout_ms' in init ? init [ 'timeout_ms' ] : undefined ;
259+
260+ if ( input instanceof Request ) {
261+ const merged = new Headers ( input . headers ) ;
262+ if ( init ?. headers ) {
263+ const extra = new Headers ( init . headers ) ;
264+ extra . forEach ( ( value , key ) => {
265+ merged . set ( key , value ) ;
266+ } ) ;
267+ }
268+ const out : {
269+ url : string ;
270+ method : string ;
271+ headers : Headers ;
272+ body ?: RequestInit [ 'body' ] ;
273+ signal ?: AbortSignal | null ;
274+ duplex ?: RequestInit [ 'duplex' ] ;
275+ timeout_ms ?: number ;
276+ } = {
277+ url : input . url ,
278+ method : ( init ?. method ?? input . method ) ?. toUpperCase ( ) || 'GET' ,
279+ headers : merged ,
280+ } ;
281+ const mergedBody = init ?. body ?? input . body ;
282+ if ( mergedBody !== undefined && mergedBody !== null ) {
283+ out . body = mergedBody ;
284+ }
285+ const mergedSignal = init ?. signal ?? input . signal ;
286+ if ( mergedSignal !== undefined ) {
287+ out . signal = mergedSignal ;
288+ }
289+ if ( init ?. duplex !== undefined ) {
290+ out . duplex = init . duplex ;
291+ }
292+ if ( timeoutFromInit !== undefined ) {
293+ out . timeout_ms = timeoutFromInit ;
294+ }
295+ return out ;
296+ }
297+
298+ const url = input instanceof URL ? input . href : String ( input ) ;
299+ const method = ( init ?. method ?? 'GET' ) . toUpperCase ( ) ;
300+ const headers = new Headers ( init ?. headers ) ;
301+ const out : {
302+ url : string ;
303+ method : string ;
304+ headers : Headers ;
305+ body ?: RequestInit [ 'body' ] ;
306+ signal ?: AbortSignal | null ;
307+ duplex ?: RequestInit [ 'duplex' ] ;
308+ timeout_ms ?: number ;
309+ } = { url, method, headers } ;
310+ if ( init ?. body !== undefined ) {
311+ out . body = init . body ;
312+ }
313+ if ( init ?. signal !== undefined ) {
314+ out . signal = init . signal ;
315+ }
316+ if ( init ?. duplex !== undefined ) {
317+ out . duplex = init . duplex ;
318+ }
319+ if ( timeoutFromInit !== undefined ) {
320+ out . timeout_ms = timeoutFromInit ;
321+ }
322+ return out ;
323+ }
324+
325+ function assertHTTPURL ( url : string ) : void {
326+ let parsed : URL ;
327+ try {
328+ parsed = new URL ( url ) ;
329+ } catch {
330+ throw new KernelError ( `browser.fetch target must be an absolute URL; received: ${ url } ` ) ;
331+ }
332+ if ( parsed . protocol !== 'http:' && parsed . protocol !== 'https:' ) {
333+ throw new KernelError ( `browser.fetch only supports http(s) URLs; received: ${ parsed . protocol } ` ) ;
334+ }
335+ }
336+
337+ function headersToRequestOptionsHeaders ( headers : Headers ) : Record < string , string | null | undefined > {
338+ const out : Record < string , string | null | undefined > = { } ;
339+ headers . forEach ( ( value , key ) => {
340+ const lower = key . toLowerCase ( ) ;
341+ if (
342+ lower === 'accept' ||
343+ lower === 'content-length' ||
344+ lower === 'connection' ||
345+ lower === 'keep-alive' ||
346+ lower === 'proxy-authenticate' ||
347+ lower === 'proxy-authorization' ||
348+ lower === 'te' ||
349+ lower === 'trailers' ||
350+ lower === 'transfer-encoding' ||
351+ lower === 'upgrade'
352+ ) {
353+ return ;
354+ }
355+ out [ key ] = value ;
356+ } ) ;
357+ return out ;
358+ }
359+
0 commit comments