diff options
-rw-r--r-- | cli/tests/unit/fetch_test.ts | 134 | ||||
-rw-r--r-- | ext/fetch/22_body.js | 29 | ||||
-rw-r--r-- | ext/fetch/26_fetch.js | 1 | ||||
-rw-r--r-- | ext/fetch/lib.rs | 4 |
4 files changed, 164 insertions, 4 deletions
diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts index b3e097cae..5375457bc 100644 --- a/cli/tests/unit/fetch_test.ts +++ b/cli/tests/unit/fetch_test.ts @@ -1639,3 +1639,137 @@ Deno.test(async function staticResponseJson() { const res = await resp.json(); assertEquals(res, data); }); + +function invalidServer(addr: string, body: Uint8Array): Deno.Listener { + const [hostname, port] = addr.split(":"); + const listener = Deno.listen({ + hostname, + port: Number(port), + }) as Deno.Listener; + + (async () => { + for await (const conn of listener) { + const p1 = conn.read(new Uint8Array(2 ** 14)); + const p2 = conn.write(body); + + await Promise.all([p1, p2]); + conn.close(); + } + })(); + + return listener; +} + +Deno.test( + { permissions: { net: true } }, + async function fetchWithInvalidContentLengthAndTransferEncoding(): Promise< + void + > { + const addr = "127.0.0.1:4516"; + const data = "a".repeat(10 << 10); + + const body = new TextEncoder().encode( + `HTTP/1.1 200 OK\r\nContent-Length: ${ + Math.round(data.length * 2) + }\r\nTransfer-Encoding: chunked\r\n\r\n${ + data.length.toString(16) + }\r\n${data}\r\n0\r\n\r\n`, + ); + + // if transfer-encoding is sent, content-length is ignored + // even if it has an invalid value (content-length > totalLength) + const listener = invalidServer(addr, body); + const response = await fetch(`http://${addr}/`); + + const res = await response.arrayBuffer(); + const buf = new TextEncoder().encode(data); + assertEquals(res.byteLength, buf.byteLength); + assertEquals(new Uint8Array(res), buf); + + listener.close(); + }, +); + +Deno.test( + { permissions: { net: true } }, + async function fetchWithInvalidContentLength(): Promise< + void + > { + const addr = "127.0.0.1:4517"; + const data = "a".repeat(10 << 10); + + const body = new TextEncoder().encode( + `HTTP/1.1 200 OK\r\nContent-Length: ${ + Math.round(data.length / 2) + }\r\nContent-Length: ${data.length}\r\n\r\n${data}`, + ); + + // It should fail if multiple content-length headers with different values are sent + const listener = invalidServer(addr, body); + await assertRejects( + async () => { + await fetch(`http://${addr}/`); + }, + TypeError, + "invalid content-length parsed", + ); + + listener.close(); + }, +); + +Deno.test( + { permissions: { net: true } }, + async function fetchWithInvalidContentLength(): Promise< + void + > { + const addr = "127.0.0.1:4518"; + const data = "a".repeat(10 << 10); + + const contentLength = data.length / 2; + const body = new TextEncoder().encode( + `HTTP/1.1 200 OK\r\nContent-Length: ${contentLength}\r\n\r\n${data}`, + ); + + const listener = invalidServer(addr, body); + const response = await fetch(`http://${addr}/`); + + // If content-length < totalLength, a maximum of content-length bytes + // should be returned. + const res = await response.arrayBuffer(); + const buf = new TextEncoder().encode(data); + assertEquals(res.byteLength, contentLength); + assertEquals(new Uint8Array(res), buf.subarray(contentLength)); + + listener.close(); + }, +); + +Deno.test( + { permissions: { net: true } }, + async function fetchWithInvalidContentLength(): Promise< + void + > { + const addr = "127.0.0.1:4519"; + const data = "a".repeat(10 << 10); + + const contentLength = data.length * 2; + const body = new TextEncoder().encode( + `HTTP/1.1 200 OK\r\nContent-Length: ${contentLength}\r\n\r\n${data}`, + ); + + const listener = invalidServer(addr, body); + const response = await fetch(`http://${addr}/`); + // If content-length > totalLength, a maximum of content-length bytes + // should be returned. + await assertRejects( + async () => { + await response.arrayBuffer(); + }, + Error, + "end of file before message length reached", + ); + + listener.close(); + }, +); diff --git a/ext/fetch/22_body.js b/ext/fetch/22_body.js index a51cdc184..97a8a8db1 100644 --- a/ext/fetch/22_body.js +++ b/ext/fetch/22_body.js @@ -64,10 +64,12 @@ } class InnerBody { + #knownExactLength = null; + /** * @param {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} stream */ - constructor(stream) { + constructor(stream, knownExactLength) { /** @type {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} */ this.streamOrStatic = stream ?? { body: new Uint8Array(), consumed: false }; @@ -75,6 +77,8 @@ this.source = null; /** @type {null | number} */ this.length = null; + + this.#knownExactLength = knownExactLength; } get stream() { @@ -147,14 +151,31 @@ const reader = this.stream.getReader(); /** @type {Uint8Array[]} */ const chunks = []; + + let finalBuffer = this.#knownExactLength + ? new Uint8Array(this.#knownExactLength) + : null; + let totalLength = 0; while (true) { const { value: chunk, done } = await reader.read(); if (done) break; - ArrayPrototypePush(chunks, chunk); + + if (finalBuffer) { + // fast path, content-length is present + TypedArrayPrototypeSet(finalBuffer, chunk, totalLength); + } else { + // slow path, content-length is not present + ArrayPrototypePush(chunks, chunk); + } totalLength += chunk.byteLength; } - const finalBuffer = new Uint8Array(totalLength); + + if (finalBuffer) { + return finalBuffer; + } + + finalBuffer = new Uint8Array(totalLength); let i = 0; for (const chunk of chunks) { TypedArrayPrototypeSet(finalBuffer, chunk, i); @@ -199,7 +220,7 @@ clone() { const [out1, out2] = this.stream.tee(); this.streamOrStatic = out1; - const second = new InnerBody(out2); + const second = new InnerBody(out2, this.#knownExactLength); second.source = core.deserialize(core.serialize(this.source)); second.length = this.length; return second; diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js index c980bc9b8..13c34f534 100644 --- a/ext/fetch/26_fetch.js +++ b/ext/fetch/26_fetch.js @@ -335,6 +335,7 @@ } else { response.body = new InnerBody( createResponseBodyStream(resp.responseRid, terminator), + resp.contentLength, ); } } diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 20db7abbc..3988acf9e 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -361,6 +361,7 @@ pub struct FetchResponse { headers: Vec<(ByteString, ByteString)>, url: String, response_rid: ResourceId, + content_length: Option<u64>, } #[op] @@ -391,6 +392,8 @@ pub async fn op_fetch_send( res_headers.push((key.as_str().into(), val.as_bytes().into())); } + let content_length = res.content_length(); + let stream: BytesStream = Box::pin(res.bytes_stream().map(|r| { r.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) })); @@ -409,6 +412,7 @@ pub async fn op_fetch_send( headers: res_headers, url, response_rid: rid, + content_length, }) } |