diff --git a/src/lib/seam-http-request.ts b/src/lib/seam-http-request.ts index 719de08d..5a16ee12 100644 --- a/src/lib/seam-http-request.ts +++ b/src/lib/seam-http-request.ts @@ -223,7 +223,7 @@ export class SeamHttpRequest< } const getUrlPrefix = (input: string): string => { - if (canParseUrl(input)) { + if (isAbsoluteHttpUrl(input)) { const url = new URL(input).toString() if (url.endsWith('/')) return url.slice(0, -1) return url @@ -239,11 +239,13 @@ const getUrlPrefix = (input: string): string => { ) } -// UPSTREAM: Prefer URL.canParse when it has wider support. -// https://caniuse.com/mdn-api_url_canparse_static -const canParseUrl = (input: string): boolean => { +// An input without an http or https scheme, e.g., localhost:3000, +// may still parse as a URL with an unintended scheme, e.g., localhost:, +// and must not be treated as an absolute URL. +const isAbsoluteHttpUrl = (input: string): boolean => { try { - return new URL(input) != null + const { protocol } = new URL(input) + return protocol === 'http:' || protocol === 'https:' } catch { return false } diff --git a/test/seam/connect/seam-http-request.test.ts b/test/seam/connect/seam-http-request.test.ts index 00e69b5d..a84722a3 100644 --- a/test/seam/connect/seam-http-request.test.ts +++ b/test/seam/connect/seam-http-request.test.ts @@ -158,6 +158,39 @@ test.serial( }, ) +test('SeamHttpRequest: url is a URL when endpoint has an explicit scheme and port', async (t) => { + const { seed } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { + endpoint: 'http://localhost:3000', + }) + + const { url } = seam.devices.get({ device_id: 'abc123' }) + + t.true(url instanceof URL) + t.deepEqual( + toPlainUrlObject(url), + toPlainUrlObject( + new URL( + 'http://localhost:3000/devices/get?device_id=abc123&_strict=true', + ), + ), + ) +}) + +test.serial( + 'SeamHttpRequest: url throws for a scheme-less host in a non-browser environment', + async (t) => { + const { seed } = await getTestServer(t) + const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { + endpoint: 'localhost:3000', + }) + + const request = seam.devices.get({ device_id: 'abc123' }) + + t.throws(() => request.url, { message: /Cannot resolve origin/ }) + }, +) + test.serial( 'SeamHttpRequest: url throws if unable to resolve origin', async (t) => {