diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index a98917610fd5..77632c9e3d29 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -1593,19 +1593,17 @@ fn fetch_impl( )); } - if !url.protocol.is_empty() { - if !(url.is_http() || url.is_https() || url.is_s3()) { - let err = global_this.to_type_error( - jsc::ErrorCode::INVALID_ARG_VALUE, - format_args!("protocol must be http:, https: or s3:"), - ); - return Ok( - JSPromise::dangerously_create_rejected_promise_value_without_notifying_vm( - global_this, - err, - ), - ); - } + if !(url.is_http() || url.is_https() || url.is_s3()) { + let err = global_this.to_type_error( + jsc::ErrorCode::INVALID_ARG_VALUE, + format_args!("protocol must be http:, https: or s3:"), + ); + return Ok( + JSPromise::dangerously_create_rejected_promise_value_without_notifying_vm( + global_this, + err, + ), + ); } if !ALLOW_GET_BODY && !method.has_request_body() && body.has_body() && !upgraded_connection { diff --git a/test/js/web/fetch/fetch-args.test.ts b/test/js/web/fetch/fetch-args.test.ts index 3d245e817493..ae2ce83b7932 100644 --- a/test/js/web/fetch/fetch-args.test.ts +++ b/test/js/web/fetch/fetch-args.test.ts @@ -16,6 +16,51 @@ afterAll(() => { server!.stop(true); }); +describe("non-HTTP(S) URL scheme rejection", () => { + // WHATWG URL parses `localhost:3000/x` as scheme "localhost:" with an empty + // host. fetch() must reject these without any network activity rather than + // treating the scheme name as a hostname. + test.each(["about:blank", "javascript:alert(1)", "chrome:flags", "foo:bar"])( + "fetch(%j) rejects with TypeError", + async input => { + const prevCount = requestCount; + const err = await fetch(input).then( + () => null, + e => e, + ); + expect(err).toBeInstanceOf(TypeError); + expect(requestCount).toBe(prevCount); + }, + ); + + test("fetch('localhost:/path') does not reach the network", async () => { + const prevCount = requestCount; + const input = `localhost:${server!.port}/api/x?q=1`; + expect(new URL(input).host).toBe(""); + const err = await fetch(input).then( + () => null, + e => e, + ); + expect(err).toBeInstanceOf(TypeError); + expect(requestCount).toBe(prevCount); + }); + + test("fetch(new Request('about:blank')) rejects with TypeError", async () => { + const err = await fetch(new Request("about:blank")).then( + () => null, + e => e, + ); + expect(err).toBeInstanceOf(TypeError); + }); + + test("http:// and data: still work", async () => { + const res = await fetch(server!.url); + expect(res.status).toBe(200); + const dataRes = await fetch("data:text/plain,hello"); + expect(await dataRes.text()).toBe("hello"); + }); +}); + test("fetch(request subclass with headers)", async () => { class MyRequest extends Request { constructor(input: RequestInfo, init?: RequestInit) {