-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add browser routing cache #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
94228be
ae9a739
d835f69
e730af8
2f12277
2082705
7030d96
0d9ddce
b09434e
2f16386
7a56ab6
fdd3adf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import Kernel from '@onkernel/sdk'; | ||
|
|
||
| async function main() { | ||
| const kernel = new Kernel({ | ||
| browserRouting: { | ||
| enabled: true, | ||
| subresources: ['computer'], | ||
| }, | ||
| }); | ||
|
|
||
| const browser = await kernel.browsers.create({}); | ||
| await kernel.browsers.computer.clickMouse(browser.session_id, { x: 10, y: 10 }); | ||
| const response = await kernel.browsers.fetch(browser.session_id, 'https://example.com', { method: 'GET' }); | ||
| console.log('status', response.status); | ||
|
|
||
| await kernel.browsers.deleteByID(browser.session_id); | ||
| } | ||
|
|
||
| void main(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| import type { RequestInfo, RequestInit } from '../internal/builtin-types'; | ||
| import { KernelError } from '../core/error'; | ||
| import { buildHeaders } from '../internal/headers'; | ||
| import type { FinalRequestOptions, RequestOptions } from '../internal/request-options'; | ||
| import type { HTTPMethod } from '../internal/types'; | ||
| import type { Kernel } from '../client'; | ||
|
|
||
| export interface BrowserFetchInit extends RequestInit { | ||
| timeout_ms?: number; | ||
| } | ||
|
|
||
| export async function browserFetch( | ||
| client: Kernel, | ||
| sessionId: string, | ||
| input: RequestInfo | URL, | ||
| init?: BrowserFetchInit, | ||
| ): Promise<Response> { | ||
| const route = client.browserRouteCache.get(sessionId); | ||
| if (!route) { | ||
| throw new KernelError( | ||
| `browser route cache does not contain session ${sessionId}; create, retrieve, or list the browser before calling browser.fetch`, | ||
| ); | ||
| } | ||
|
|
||
| const { url: targetURL, method, headers, body, signal, duplex, timeout_ms } = splitFetchArgs(input, init); | ||
| assertHTTPURL(targetURL); | ||
|
|
||
| const query: Record<string, unknown> = { url: targetURL, jwt: route.jwt }; | ||
| if (timeout_ms !== undefined) { | ||
| query['timeout_ms'] = timeout_ms; | ||
| } | ||
|
|
||
| const accept = headers.get('accept'); | ||
| const requestOptions: FinalRequestOptions = { | ||
| method: normalizeMethod(method), | ||
| path: joinURL(route.baseURL, '/curl/raw'), | ||
| query, | ||
| body: body as RequestOptions['body'], | ||
| headers: buildHeaders([ | ||
| { Authorization: null }, | ||
| accept ? { Accept: accept } : { Accept: '*/*' }, | ||
| headersToRequestOptionsHeaders(headers), | ||
| ]), | ||
| signal: signal ?? null, | ||
| __binaryResponse: true, | ||
| }; | ||
| if (duplex) { | ||
| requestOptions.fetchOptions = { duplex } as NonNullable<RequestOptions['fetchOptions']>; | ||
| } | ||
|
|
||
| return client.request(requestOptions).asResponse(); | ||
| } | ||
|
|
||
| function normalizeMethod(method: string): HTTPMethod { | ||
| const methodLower = method.toLowerCase(); | ||
| const allowed = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options']); | ||
| if (!allowed.has(methodLower)) { | ||
| throw new KernelError(`browser.fetch unsupported HTTP method: ${method}`); | ||
| } | ||
| return methodLower as HTTPMethod; | ||
| } | ||
|
|
||
| function splitFetchArgs( | ||
| input: RequestInfo | URL, | ||
| init?: BrowserFetchInit, | ||
| ): { | ||
| url: string; | ||
| method: string; | ||
| headers: Headers; | ||
| body?: RequestInit['body']; | ||
| signal?: AbortSignal | null; | ||
| duplex?: RequestInit['duplex']; | ||
| timeout_ms?: number; | ||
| } { | ||
| const timeoutFromInit = init && 'timeout_ms' in init ? init['timeout_ms'] : undefined; | ||
|
|
||
| if (input instanceof Request) { | ||
| const headers = new Headers(input.headers); | ||
| if (init?.headers) { | ||
| const extra = new Headers(init.headers); | ||
| extra.forEach((value, key) => { | ||
| headers.set(key, value); | ||
| }); | ||
| } | ||
|
|
||
| const out: { | ||
| url: string; | ||
| method: string; | ||
| headers: Headers; | ||
| body?: RequestInit['body']; | ||
| signal?: AbortSignal | null; | ||
| duplex?: RequestInit['duplex']; | ||
| timeout_ms?: number; | ||
| } = { | ||
| url: input.url, | ||
| method: (init?.method ?? input.method)?.toUpperCase() || 'GET', | ||
| headers, | ||
| }; | ||
| const body = init?.body ?? input.body; | ||
| if (body !== undefined && body !== null) { | ||
| out.body = body; | ||
| } | ||
| const signal = init?.signal ?? input.signal; | ||
| if (signal !== undefined) { | ||
| out.signal = signal; | ||
| } | ||
| if (init?.duplex !== undefined) { | ||
| out.duplex = init.duplex; | ||
| } | ||
| if (timeoutFromInit !== undefined) { | ||
| out.timeout_ms = timeoutFromInit; | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| const out: { | ||
| url: string; | ||
| method: string; | ||
| headers: Headers; | ||
| body?: RequestInit['body']; | ||
| signal?: AbortSignal | null; | ||
| duplex?: RequestInit['duplex']; | ||
| timeout_ms?: number; | ||
| } = { | ||
| url: input instanceof URL ? input.href : String(input), | ||
| method: (init?.method ?? 'GET').toUpperCase(), | ||
| headers: new Headers(init?.headers), | ||
| }; | ||
| if (init?.body !== undefined) { | ||
| out.body = init.body; | ||
| } | ||
| if (init?.signal !== undefined) { | ||
| out.signal = init.signal; | ||
| } | ||
| if (init?.duplex !== undefined) { | ||
| out.duplex = init.duplex; | ||
| } | ||
| if (timeoutFromInit !== undefined) { | ||
| out.timeout_ms = timeoutFromInit; | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| function assertHTTPURL(url: string): void { | ||
| let parsed: URL; | ||
| try { | ||
| parsed = new URL(url); | ||
| } catch { | ||
| throw new KernelError(`browser.fetch target must be an absolute URL; received: ${url}`); | ||
| } | ||
|
|
||
| if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { | ||
| throw new KernelError(`browser.fetch only supports http(s) URLs; received: ${parsed.protocol}`); | ||
| } | ||
| } | ||
|
|
||
| function headersToRequestOptionsHeaders(headers: Headers): Record<string, string | null | undefined> { | ||
| const out: Record<string, string | null | undefined> = {}; | ||
|
|
||
| headers.forEach((value, key) => { | ||
| switch (key.toLowerCase()) { | ||
| case 'accept': | ||
| case 'content-length': | ||
| case 'connection': | ||
| case 'keep-alive': | ||
| case 'proxy-authenticate': | ||
| case 'proxy-authorization': | ||
| case 'te': | ||
| case 'trailers': | ||
| case 'transfer-encoding': | ||
| case 'upgrade': | ||
| return; | ||
| default: | ||
| out[key] = value; | ||
| } | ||
| }); | ||
|
|
||
| return out; | ||
| } | ||
|
|
||
| function joinURL(baseURL: string, path: string): string { | ||
| return `${baseURL.replace(/\/+$/, '')}${path.startsWith('/') ? path : `/${path}`}`; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
normalizeMethodallows methods outsideHTTPMethodtype unionLow Severity
normalizeMethodvalidates and passes through'head'and'options', then casts the resultas HTTPMethod. However,HTTPMethodis defined as'get' | 'post' | 'put' | 'patch' | 'delete'— it does not include those two methods. Theascast silently lies to the type system, which could mask issues if any downstream code narrows onHTTPMethodvariants.Reviewed by Cursor Bugbot for commit fdd3adf. Configure here.