diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/fetch/api/response | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fetch/api/response')
33 files changed, 1839 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/api/response/json.any.js b/testing/web-platform/tests/fetch/api/response/json.any.js new file mode 100644 index 0000000000..15f050e632 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/json.any.js @@ -0,0 +1,14 @@ +// See also /xhr/json.any.js + +promise_test(async t => { + const response = await fetch(`data:,\uFEFF{ "b": 1, "a": 2, "b": 3 }`); + const json = await response.json(); + assert_array_equals(Object.keys(json), ["b", "a"]); + assert_equals(json.a, 2); + assert_equals(json.b, 3); +}, "Ensure the correct JSON parser is used"); + +promise_test(async t => { + const response = await fetch("/xhr/resources/utf16-bom.json"); + return promise_rejects_js(t, SyntaxError, response.json()); +}, "Ensure UTF-16 results in an error"); diff --git a/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html b/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html new file mode 100644 index 0000000000..fe5e7d4c07 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> + new Response(new ReadableStream({ + start(c) { + + for (const i of new Array(40000).fill()) { + c.enqueue(new Uint8Array(0)); + } + c.close(); + + } + })).text(); +</script> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html b/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html new file mode 100644 index 0000000000..9bb6e0bbf3 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>Current page used as a test helper</title> +<base href="success/"> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html b/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html new file mode 100644 index 0000000000..f63372e64c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>Incumbent page used as a test helper</title> + +<iframe src="../current/current.html" id="c"></iframe> +<iframe src="../relevant/relevant.html" id="r"></iframe> + +<script> +'use strict'; + +window.createRedirectResponse = (...args) => { + const current = document.querySelector('#c').contentWindow; + const relevant = document.querySelector('#r').contentWindow; + return current.Response.redirect.call(relevant.Response, ...args); +}; + +</script> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html b/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html new file mode 100644 index 0000000000..44f42eda49 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<title>Relevant page used as a test helper</title> diff --git a/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html b/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html new file mode 100644 index 0000000000..5f2f42a1ce --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Response.redirect URL parsing, with multiple globals in play</title> +<link rel="help" href="https://fetch.spec.whatwg.org/#dom-response-redirect"> +<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<!-- This is the entry global --> + +<iframe src="incumbent/incumbent.html"></iframe> + +<script> +'use strict'; + +const loadPromise = new Promise(resolve => { + window.addEventListener("load", () => resolve()); +}); + +promise_test(() => { + return loadPromise.then(() => { + const res = document.querySelector('iframe').contentWindow.createRedirectResponse("url"); + + assert_equals(res.headers.get("Location"), new URL("current/success/url", location.href).href); + }); +}, "should parse the redirect Location URL relative to the current settings object"); + +</script> diff --git a/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html b/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html new file mode 100644 index 0000000000..64b0755666 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title></title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> +<body> + <script> +function performMicrotaskCheckpoint() { + document.createNodeIterator(document, -1, { + acceptNode() { + return NodeFilter.FILTER_ACCEPT; + } + }).nextNode(); +} + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + // Add a getter for "then" that will incidentally be invoked + // during promise resolution. + Object.prototype.__defineGetter__('then', () => { + // Clean up behind ourselves. + delete Object.prototype.then; + + // This promise should (like all promises) be resolved + // asynchronously. + var executed = false; + Promise.resolve().then(_ => { executed = true; }); + + // This shouldn't run microtasks! They should only run + // after the fetch is resolved. + performMicrotaskCheckpoint(); + + // The fulfill handler above shouldn't have run yet. If it has run, + // throw to reject this promise and fail the test. + assert_false(executed, "shouldn't have run microtasks yet"); + + // Otherwise act as if there's no "then" property so the promise + // fulfills and the test passes. + return undefined; + }); + + // Create a read request, incidentally resolving a promise with an + // object value, thereby invoking the getter installed above. + return response.body.getReader().read(); + }); +}, "reading from a body stream should occur in a microtask scope"); + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + // Add a getter for "then" that will incidentally be invoked + // during promise resolution. + Object.prototype.__defineGetter__('then', () => { + // Clean up behind ourselves. + delete Object.prototype.then; + + // This promise should (like all promises) be resolved + // asynchronously. + var executed = false; + Promise.resolve().then(_ => { executed = true; }); + + // This shouldn't run microtasks! They should only run + // after the fetch is resolved. + performMicrotaskCheckpoint(); + + // The fulfill handler above shouldn't have run yet. If it has run, + // throw to reject this promise and fail the test. + assert_false(executed, "shouldn't have run microtasks yet"); + + // Otherwise act as if there's no "then" property so the promise + // fulfills and the test passes. + return undefined; + }); + + // Create a read request, incidentally resolving a promise with an + // object value, thereby invoking the getter installed above. + return response.body.pipeTo(new WritableStream({ + write(chunk) {} + })) + }); +}, "piping from a body stream to a JS-written WritableStream should occur in a microtask scope"); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js new file mode 100644 index 0000000000..91140d1afd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker +// META: title=Response consume blob and http bodies +// META: script=../resources/utils.js + +promise_test(function(test) { + return new Response(new Blob([], { "type" : "text/plain" })).body.cancel(); +}, "Cancelling a starting blob Response stream"); + +promise_test(function(test) { + var response = new Response(new Blob(["This is data"], { "type" : "text/plain" })); + var reader = response.body.getReader(); + reader.read(); + return reader.cancel(); +}, "Cancelling a loading blob Response stream"); + +promise_test(function(test) { + var response = new Response(new Blob(["T"], { "type" : "text/plain" })); + var reader = response.body.getReader(); + + var closedPromise = reader.closed.then(function() { + return reader.cancel(); + }); + reader.read().then(function readMore({done, value}) { + if (!done) return reader.read().then(readMore); + }); + return closedPromise; +}, "Cancelling a closed blob Response stream"); + +promise_test(function(test) { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { + return response.body.cancel(); + }); +}, "Cancelling a starting Response stream"); + +promise_test(function() { + return fetch(RESOURCES_DIR + "trickle.py?ms=30&count=100").then(function(response) { + var reader = response.body.getReader(); + return reader.read().then(function() { + return reader.cancel(); + }); + }); +}, "Cancelling a loading Response stream"); + +promise_test(function() { + async function readAll(reader) { + while (true) { + const {value, done} = await reader.read(); + if (done) + return; + } + } + + return fetch(RESOURCES_DIR + "top.txt").then(function(response) { + var reader = response.body.getReader(); + return readAll(reader).then(() => reader.cancel()); + }); +}, "Cancelling a closed Response stream"); + +promise_test(async () => { + const response = await fetch(RESOURCES_DIR + "top.txt"); + const { body } = response; + await body.cancel(); + assert_equals(body, response.body, ".body should not change after cancellation"); +}, "Accessing .body after canceling it"); diff --git a/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js b/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js new file mode 100644 index 0000000000..da54616c37 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js @@ -0,0 +1,32 @@ +// Verify that calling Response clone() in a detached iframe doesn't crash. +// Regression test for https://crbug.com/1082688. + +'use strict'; + +promise_test(async () => { + // Wait for the document body to be available. + await new Promise(resolve => { + onload = resolve; + }); + + window.iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.srcdoc = `<!doctype html> +<script> +const response = new Response('body'); +window.parent.postMessage('okay', '*'); +window.parent.iframe.remove(); +response.clone(); +</script> +`; + + await new Promise(resolve => { + onmessage = evt => { + if (evt.data === 'okay') { + resolve(); + } + }; + }); + + // If it got here without crashing, the test passed. +}, 'clone within removed iframe should not crash'); diff --git a/testing/web-platform/tests/fetch/api/response/response-clone.any.js b/testing/web-platform/tests/fetch/api/response/response-clone.any.js new file mode 100644 index 0000000000..f5cda75149 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-clone.any.js @@ -0,0 +1,140 @@ +// META: global=window,worker +// META: title=Response clone +// META: script=../resources/utils.js + +var defaultValues = { "type" : "default", + "url" : "", + "ok" : true, + "status" : 200, + "statusText" : "" +}; + +var response = new Response(); +var clonedResponse = response.clone(); +test(function() { + for (var attributeName in defaultValues) { + var expectedValue = defaultValues[attributeName]; + assert_equals(clonedResponse[attributeName], expectedValue, + "Expect default response." + attributeName + " is " + expectedValue); + } +}, "Check Response's clone with default values, without body"); + +var body = "This is response body"; +var headersInit = { "name" : "value" }; +var responseInit = { "status" : 200, + "statusText" : "GOOD", + "headers" : headersInit +}; +var response = new Response(body, responseInit); +var clonedResponse = response.clone(); +test(function() { + assert_equals(clonedResponse.status, responseInit["status"], + "Expect response.status is " + responseInit["status"]); + assert_equals(clonedResponse.statusText, responseInit["statusText"], + "Expect response.statusText is " + responseInit["statusText"]); + assert_equals(clonedResponse.headers.get("name"), "value", + "Expect response.headers has name:value header"); +}, "Check Response's clone has the expected attribute values"); + +promise_test(function(test) { + return validateStreamFromString(response.body.getReader(), body); +}, "Check orginal response's body after cloning"); + +promise_test(function(test) { + return validateStreamFromString(clonedResponse.body.getReader(), body); +}, "Check cloned response's body"); + +promise_test(function(test) { + var disturbedResponse = new Response("data"); + return disturbedResponse.text().then(function() { + assert_true(disturbedResponse.bodyUsed, "response is disturbed"); + assert_throws_js(TypeError, function() { disturbedResponse.clone(); }, + "Expect TypeError exception"); + }); +}, "Cannot clone a disturbed response"); + +promise_test(function(t) { + var clone; + var result; + var response; + return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { + clone = res.clone(); + response = res; + return clone.text(); + }).then(function(r) { + assert_equals(r.length, 26); + result = r; + return response.text(); + }).then(function(r) { + assert_equals(r, result, "cloned responses should provide the same data"); + }); + }, 'Cloned responses should provide the same data'); + +promise_test(function(t) { + var clone; + return fetch('../resources/trickle.py?count=2&delay=100').then(function(res) { + clone = res.clone(); + res.body.cancel(); + assert_true(res.bodyUsed); + assert_false(clone.bodyUsed); + return clone.arrayBuffer(); + }).then(function(r) { + assert_equals(r.byteLength, 26); + assert_true(clone.bodyUsed); + }); +}, 'Cancelling stream should not affect cloned one'); + +function testReadableStreamClone(initialBuffer, bufferType) +{ + promise_test(function(test) { + var response = new Response(new ReadableStream({start : function(controller) { + controller.enqueue(initialBuffer); + controller.close(); + }})); + + var clone = response.clone(); + var stream1 = response.body; + var stream2 = clone.body; + + var buffer; + return stream1.getReader().read().then(function(data) { + assert_false(data.done); + assert_equals(data.value, initialBuffer, "Buffer of being-cloned response stream is the same as the original buffer"); + return stream2.getReader().read(); + }).then(function(data) { + assert_false(data.done); + if (initialBuffer instanceof ArrayBuffer) { + assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBufer"); + assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal"); + assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content"); + } else if (initialBuffer instanceof DataView) { + assert_true(data.value instanceof DataView, "Cloned buffer is DataView"); + assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal"); + assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal"); + for (let i = 0; i < initialBuffer.byteLength; ++i) { + assert_equals( + data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}"); + } + } else { + assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content"); + } + assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type"); + assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer"); + }); + }, "Check response clone use structureClone for teed ReadableStreams (" + bufferType + "chunk)"); +} + +var arrayBuffer = new ArrayBuffer(16); +testReadableStreamClone(new Int8Array(arrayBuffer, 1), "Int8Array"); +testReadableStreamClone(new Int16Array(arrayBuffer, 2, 2), "Int16Array"); +testReadableStreamClone(new Int32Array(arrayBuffer), "Int32Array"); +testReadableStreamClone(arrayBuffer, "ArrayBuffer"); +testReadableStreamClone(new Uint8Array(arrayBuffer), "Uint8Array"); +testReadableStreamClone(new Uint8ClampedArray(arrayBuffer), "Uint8ClampedArray"); +testReadableStreamClone(new Uint16Array(arrayBuffer, 2), "Uint16Array"); +testReadableStreamClone(new Uint32Array(arrayBuffer), "Uint32Array"); +testReadableStreamClone(typeof BigInt64Array === "function" ? new BigInt64Array(arrayBuffer) : undefined, "BigInt64Array"); +testReadableStreamClone(typeof BigUint64Array === "function" ? new BigUint64Array(arrayBuffer) : undefined, "BigUint64Array"); +testReadableStreamClone(new Float32Array(arrayBuffer), "Float32Array"); +testReadableStreamClone(new Float64Array(arrayBuffer), "Float64Array"); +testReadableStreamClone(new DataView(arrayBuffer, 2, 8), "DataView"); diff --git a/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js b/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js new file mode 100644 index 0000000000..0fa85ecbcb --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js @@ -0,0 +1,99 @@ +// META: global=window,worker +// META: title=Response consume empty bodies + +function checkBodyText(test, response) { + return response.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyBlob(test, response) { + return response.blob().then(function(bodyAsBlob) { + var promise = new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function() { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + return promise.then(function(body) { + assert_equals(body, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); + }); +} + +function checkBodyArrayBuffer(test, response) { + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyJSON(test, response) { + return response.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormData(test, response) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormDataError(test, response) { + return promise_rejects_js(test, TypeError, response.formData()).then(function() { + assert_false(response.bodyUsed); + }); +} + +function checkResponseWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var response = new Response(undefined, { "headers": headers }); + assert_false(response.bodyUsed); + return checkFunction(test, response); + }, "Consume response's body as " + bodyType); +} + +checkResponseWithNoBody("text", checkBodyText); +checkResponseWithNoBody("blob", checkBodyBlob); +checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkResponseWithNoBody("json (error case)", checkBodyJSON); +checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkResponseWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var response = new Response(body); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return response.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer")); +} + +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkResponseWithEmptyBody("text", "", false); +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkResponseWithEmptyBody("text", "", true); +checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +checkResponseWithEmptyBody("FormData", new FormData(), true); +checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js new file mode 100644 index 0000000000..f89d7341ac --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js @@ -0,0 +1,80 @@ +// META: global=window,worker +// META: title=Response consume +// META: script=../resources/utils.js + +promise_test(function(test) { + var body = ""; + var response = new Response(""); + return validateStreamFromString(response.body.getReader(), ""); +}, "Read empty text response's body as readableStream"); + +promise_test(function(test) { + var response = new Response(new Blob([], { "type" : "text/plain" })); + return validateStreamFromString(response.body.getReader(), ""); +}, "Read empty blob response's body as readableStream"); + +var formData = new FormData(); +formData.append("name", "value"); +var textData = JSON.stringify("This is response's body"); +var blob = new Blob([textData], { "type" : "text/plain" }); +var urlSearchParamsData = "name=value"; +var urlSearchParams = new URLSearchParams(urlSearchParamsData); + +for (const mode of [undefined, "byob"]) { + promise_test(function(test) { + var response = new Response(blob); + return validateStreamFromString(response.body.getReader({ mode }), textData); + }, `Read blob response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(textData); + return validateStreamFromString(response.body.getReader({ mode }), textData); + }, `Read text response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(urlSearchParams); + return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData); + }, `Read URLSearchParams response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var arrayBuffer = new ArrayBuffer(textData.length); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < textData.length; cptr++) + int8Array[cptr] = textData.charCodeAt(cptr); + + return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData); + }, `Read array buffer response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(formData); + return validateStreamFromPartialString(response.body.getReader({ mode }), + "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue"); + }, `Read form data response's body as readableStream with mode=${mode}`); +} + +test(function() { + assert_equals(Response.error().body, null); +}, "Getting an error Response stream"); + +test(function() { + assert_equals(Response.redirect("/").body, null); +}, "Getting a redirect Response stream"); + +promise_test(async function(test) { + var buffer = new ArrayBuffer(textData.length); + + var body = new Response(textData).body; + const reader = body.getReader( {mode: 'byob'} ); + + let offset = 3; + while (offset < textData.length) { + const {done, value} = await reader.read(new Uint8Array(buffer, offset)); + if (done) { + break; + } + buffer = value.buffer; + offset += value.byteLength; + } + + validateBufferFromString(buffer, `\0\0\0\"This is response's bo`, 'Buffer should be validated'); +}, `Reading with offset from Response stream`); diff --git a/testing/web-platform/tests/fetch/api/response/response-consume.html b/testing/web-platform/tests/fetch/api/response/response-consume.html new file mode 100644 index 0000000000..89fc49fd3c --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-consume.html @@ -0,0 +1,317 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>Response consume</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#response"> + <meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin"> + <meta name="author" title="Canon Research France" href="https://www.crf.canon.fr"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="../resources/utils.js"></script> + </head> + <body> + <script> + function blobToFormDataResponse(name, blob) { + var formData = new FormData(); + formData.append(name, blob); + return new Response(formData); + } + + function readBlobAsArrayBuffer(blob) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result); + }; + reader.onerror = function(evt) { + reject("Blob's reader failed"); + }; + reader.readAsArrayBuffer(blob); + }); + } + + function blobToTypeViaFetch(blob) { + var url = URL.createObjectURL(blob); + return fetch(url).then(function(response) { + return response.headers.get('Content-Type'); + }); + } + + function responsePromise(body, responseInit) { + return new Promise(function(resolve, reject) { + resolve(new Response(body, responseInit)); + }); + } + + function responseStringToMultipartFormTextData(response, name, value) { + assert_true(response.headers.has("Content-Type"), "Response contains Content-Type header"); + var boundaryMatches = response.headers.get("Content-Type").match(/;\s*boundary=("?)([^";\s]*)\1/); + assert_true(!!boundaryMatches, "Response contains boundary parameter"); + return stringToMultipartFormTextData(boundaryMatches[2], name, value); + } + + function streamResponsePromise(streamData, responseInit) { + return new Promise(function(resolve, reject) { + var stream = new ReadableStream({ + start: function(controller) { + controller.enqueue(stringToArray(streamData)); + controller.close(); + } + }); + resolve(new Response(stream, responseInit)); + }); + } + + function stringToMultipartFormTextData(multipartBoundary, name, value) { + return ('--' + multipartBoundary + '\r\n' + + 'Content-Disposition: form-data;name="' + name + '"\r\n' + + '\r\n' + + value + '\r\n' + + '--' + multipartBoundary + '--\r\n'); + } + + function checkBodyText(test, response, expectedBody) { + return response.text().then( function(bodyAsText) { + assert_equals(bodyAsText, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as text: bodyUsed turned true"); + }); + } + + function checkBodyBlob(test, response, expectedBody, expectedType) { + return response.blob().then(function(bodyAsBlob) { + assert_equals(bodyAsBlob.type, expectedType || "text/plain", "Blob body type should be computed from the response Content-Type"); + + var promise = blobToTypeViaFetch(bodyAsBlob).then(function(type) { + assert_equals(type, expectedType || "text/plain", 'Type via blob URL'); + return new Promise( function (resolve, reject) { + var reader = new FileReader(); + reader.onload = function(evt) { + resolve(reader.result) + }; + reader.onerror = function () { + reject("Blob's reader failed"); + }; + reader.readAsText(bodyAsBlob); + }); + }); + return promise.then(function(body) { + assert_equals(body, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as blob: bodyUsed turned true"); + }); + }); + } + + function checkBodyArrayBuffer(test, response, expectedBody) { + return response.arrayBuffer().then( function(bodyAsArrayBuffer) { + validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as arrayBuffer: bodyUsed turned true"); + }); + } + + function checkBodyJSON(test, response, expectedBody) { + return response.json().then(function(bodyAsJSON) { + var strBody = JSON.stringify(bodyAsJSON) + assert_equals(strBody, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as json: bodyUsed turned true"); + }); + } + + function checkBodyFormDataMultipart(test, response, expectedBody) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + var entryName = "name"; + var strBody = responseStringToMultipartFormTextData(response, entryName, bodyAsFormData.get(entryName)); + assert_equals(strBody, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as formData: bodyUsed turned true"); + }); + } + + function checkBodyFormDataUrlencoded(test, response, expectedBody) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + var entryName = "name"; + var strBody = entryName + "=" + bodyAsFormData.get(entryName); + assert_equals(strBody, expectedBody, "Retrieve and verify response's body"); + assert_true(response.bodyUsed, "body as formData: bodyUsed turned true"); + }); + } + + function checkBodyFormDataError(test, response, expectedBody) { + return promise_rejects_js(test, TypeError, response.formData()).then(function() { + assert_true(response.bodyUsed, "body as formData: bodyUsed turned true"); + }); + } + + function checkResponseBody(responsePromise, expectedBody, checkFunction, bodyTypes) { + promise_test(function(test) { + return responsePromise.then(function(response) { + assert_false(response.bodyUsed, "bodyUsed is false at init"); + return checkFunction(test, response, expectedBody); + }); + }, "Consume response's body: " + bodyTypes); + } + + var textData = JSON.stringify("This is response's body"); + var textResponseInit = { "headers": [["Content-Type", "text/PLAIN"]] }; + var blob = new Blob([textData], { "type": "application/octet-stream" }); + var multipartBoundary = "boundary-" + Math.random(); + var formData = new FormData(); + var formTextResponseInit = { "headers": [["Content-Type", 'multipart/FORM-data; boundary="' + multipartBoundary + '"']] }; + var formTextData = stringToMultipartFormTextData(multipartBoundary, "name", textData); + var formBlob = new Blob([formTextData]); + var urlSearchParamsData = "name=value"; + var urlSearchParams = new URLSearchParams(urlSearchParamsData); + var urlSearchParamsType = "application/x-www-form-urlencoded;charset=UTF-8"; + var urlSearchParamsResponseInit = { "headers": [["Content-Type", urlSearchParamsType]] }; + var urlSearchParamsBlob = new Blob([urlSearchParamsData], { "type": urlSearchParamsType }); + formData.append("name", textData); + + // https://fetch.spec.whatwg.org/#concept-body-package-data + // "UTF-8 decoded without BOM" is used for formData(), either in + // "multipart/form-data" and "application/x-www-form-urlencoded" cases, + // so BOMs in the values should be kept. + // (The "application/x-www-form-urlencoded" cases are tested in + // url/urlencoded-parser.any.js) + var textDataWithBom = "\uFEFFquick\uFEFFfox\uFEFF"; + var formTextDataWithBom = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom); + var formTextDataWithBomExpectedForMultipartFormData = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom); + + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyText, "from text to text"); + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyBlob, "from text to blob"); + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyArrayBuffer, "from text to arrayBuffer"); + checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyJSON, "from text to json"); + checkResponseBody(responsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from text with correct multipart type to formData"); + checkResponseBody(responsePromise(formTextDataWithBom, formTextResponseInit), formTextDataWithBomExpectedForMultipartFormData, checkBodyFormDataMultipart, "from text with correct multipart type to formData with BOM"); + checkResponseBody(responsePromise(formTextData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct multipart type to formData (error case)"); + checkResponseBody(responsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from text with correct urlencoded type to formData"); + checkResponseBody(responsePromise(urlSearchParamsData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct urlencoded type to formData (error case)"); + + checkResponseBody(responsePromise(blob, textResponseInit), textData, checkBodyBlob, "from blob to blob"); + checkResponseBody(responsePromise(blob), textData, checkBodyText, "from blob to text"); + checkResponseBody(responsePromise(blob), textData, checkBodyArrayBuffer, "from blob to arrayBuffer"); + checkResponseBody(responsePromise(blob), textData, checkBodyJSON, "from blob to json"); + checkResponseBody(responsePromise(formBlob, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from blob with correct multipart type to formData"); + checkResponseBody(responsePromise(formBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct multipart type to formData (error case)"); + checkResponseBody(responsePromise(urlSearchParamsBlob, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from blob with correct urlencoded type to formData"); + checkResponseBody(responsePromise(urlSearchParamsBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct urlencoded type to formData (error case)"); + + function checkFormDataResponseBody(responsePromise, expectedName, expectedValue, checkFunction, bodyTypes) { + promise_test(function(test) { + return responsePromise.then(function(response) { + assert_false(response.bodyUsed, "bodyUsed is false at init"); + var expectedBody = responseStringToMultipartFormTextData(response, expectedName, expectedValue); + return Promise.resolve().then(function() { + if (checkFunction === checkBodyFormDataMultipart) + return expectedBody; + // Modify expectedBody to use the same spacing for + // Content-Disposition parameters as Response and FormData does. + var response2 = new Response(formData); + return response2.text().then(function(formDataAsText) { + var reName = /[ \t]*;[ \t]*name=/; + var nameMatches = formDataAsText.match(reName); + return expectedBody.replace(reName, nameMatches[0]); + }); + }).then(function(expectedBody) { + return checkFunction(test, response, expectedBody); + }); + }); + }, "Consume response's body: " + bodyTypes); + } + + checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyFormDataMultipart, "from FormData to formData"); + checkResponseBody(responsePromise(formData, textResponseInit), undefined, checkBodyFormDataError, "from FormData without correct type to formData (error case)"); + checkFormDataResponseBody(responsePromise(formData), "name", textData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, response.headers.get('Content-Type').toLowerCase()); }, "from FormData to blob"); + checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyText, "from FormData to text"); + checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyArrayBuffer, "from FormData to arrayBuffer"); + + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyFormDataUrlencoded, "from URLSearchParams to formData"); + checkResponseBody(responsePromise(urlSearchParams, textResponseInit), urlSearchParamsData, checkBodyFormDataError, "from URLSearchParams without correct type to formData (error case)"); + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, "application/x-www-form-urlencoded;charset=utf-8"); }, "from URLSearchParams to blob"); + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyText, "from URLSearchParams to text"); + checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyArrayBuffer, "from URLSearchParams to arrayBuffer"); + + checkResponseBody(streamResponsePromise(textData, textResponseInit), textData, checkBodyBlob, "from stream to blob"); + checkResponseBody(streamResponsePromise(textData), textData, checkBodyText, "from stream to text"); + checkResponseBody(streamResponsePromise(textData), textData, checkBodyArrayBuffer, "from stream to arrayBuffer"); + checkResponseBody(streamResponsePromise(textData), textData, checkBodyJSON, "from stream to json"); + checkResponseBody(streamResponsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from stream with correct multipart type to formData"); + checkResponseBody(streamResponsePromise(formTextData), formTextData, checkBodyFormDataError, "from stream without correct multipart type to formData (error case)"); + checkResponseBody(streamResponsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from stream with correct urlencoded type to formData"); + checkResponseBody(streamResponsePromise(urlSearchParamsData), urlSearchParamsData, checkBodyFormDataError, "from stream without correct urlencoded type to formData (error case)"); + + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyBlob, "from fetch to blob"); + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyText, "from fetch to text"); + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyArrayBuffer, "from fetch to arrayBuffer"); + checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyFormDataError, "from fetch without correct type to formData (error case)"); + + promise_test(function(test) { + var response = new Response(new Blob([ + "--boundary\r\n", + "Content-Disposition: form-data; name=string\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "1\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=string-with-default-charset\r\n", + "Content-Type: text/plain; charset=utf-8\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "2\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=string-with-non-default-charset\r\n", + "Content-Type: text/plain; charset=iso-8859-1\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "3\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=string-with-non-default-type\r\n", + "Content-Type: application/octet-stream\r\n", + "\r\nvalue", new Uint8Array([0xC2, 0xA0]), "4\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=file; filename=file1\r\n", + "Content-Type: application/octet-stream; x-param=x-value\r\n", + "\r\n", new Uint8Array([5, 0x0, 0xFF]), "\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=\"file-without-type\"; filename=\"file2\"\r\n", + "\r\n", new Uint8Array([6, 0x0, 0x7F, 0xFF]), "\r\n", + "--boundary--\r\n" + ]), { "headers": [["Content-Type", 'multipart/form-data; boundary="boundary"']] }); + return response.formData().then(function(bodyAsFormData) { + // Non-file parts must always be decoded using utf-8 encoding. + assert_equals(bodyAsFormData.get("string"), "value\u00A01", "Retrieve and verify response's 1st entry value"); + assert_equals(bodyAsFormData.get("string-with-default-charset"), "value\u00A02", "Retrieve and verify response's 2nd entry value"); + assert_equals(bodyAsFormData.get("string-with-non-default-charset"), "value\u00A03", "Retrieve and verify response's 3rd entry value"); + assert_equals(bodyAsFormData.get("string-with-non-default-type"), "value\u00A04", "Retrieve and verify response's 4th entry value"); + // The name of a File must be taken from the filename parameter in + // the Content-Disposition header field. + assert_equals(bodyAsFormData.get("file").name, "file1", "Retrieve and verify response's 5th entry name property"); + assert_equals(bodyAsFormData.get("file-without-type").name, "file2", "Retrieve and verify response's 6th entry name property"); + // The type of a File must be taken from the Content-Type header field + // which defaults to "text/plain". + assert_equals(bodyAsFormData.get("file").type, "application/octet-stream; x-param=x-value", "Retrieve and verify response's 5th entry type property"); + assert_equals(bodyAsFormData.get("file-without-type").type, "text/plain", "Retrieve and verify response's 6th entry type property"); + + return Promise.resolve().then(function() { + return blobToFormDataResponse("file", bodyAsFormData.get("file")).text().then(function(bodyAsText) { + // Verify that filename, name and type are preserved. + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file1\2[;\r]/i, "Retrieve and verify response's 5th entry filename parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file\2[;\r]/i, "Retrieve and verify response's 5th entry name parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Type: *application\/octet-stream; x-param=x-value\r\n/i, "Retrieve and verify response's 5th entry type field"); + // Verify that the content is preserved. + return readBlobAsArrayBuffer(bodyAsFormData.get("file")).then(function(arrayBuffer) { + assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([5, 0x0, 0xFF]), "Retrieve and verify response's 5th entry content"); + }); + }); + }).then(function() { + return blobToFormDataResponse("file-without-type", bodyAsFormData.get("file-without-type")).text().then(function(bodyAsText) { + // Verify that filename, name and type are preserved. + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file2\2[;\r]/i, "Retrieve and verify response's 6th entry filename parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file-without-type\2[;\r]/i, "Retrieve and verify response's 6th entry name parameter"); + assert_regexp_match(bodyAsText, /\r\nContent-Type: *text\/plain\r\n/i, "Retrieve and verify response's 6th entry type field"); + // Verify that the content is preserved. + return readBlobAsArrayBuffer(bodyAsFormData.get("file-without-type")).then(function(arrayBuffer) { + assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([6, 0x0, 0x7F, 0xFF]), "Retrieve and verify response's 6th entry content"); + }); + }); + }); + }); + }, "Consume response's body: from multipart form data blob to formData"); + + </script> + </body> +</html> diff --git a/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js new file mode 100644 index 0000000000..118eb7d5cb --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker +// META: title=Response Receives Propagated Error from ReadableStream + +function newStreamWithStartError() { + var err = new Error("Start error"); + return [new ReadableStream({ + start(controller) { + controller.error(err); + } + }), + err] +} + +function newStreamWithPullError() { + var err = new Error("Pull error"); + return [new ReadableStream({ + pull(controller) { + controller.error(err); + } + }), + err] +} + +function runRequestPromiseTest([stream, err], responseReaderMethod, testDescription) { + promise_test(test => { + return promise_rejects_exactly( + test, + err, + new Response(stream)[responseReaderMethod](), + 'CustomTestError should propagate' + ) + }, testDescription) +} + + +promise_test(test => { + var [stream, err] = newStreamWithStartError(); + return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') +}, "ReadableStreamDefaultReader Promise receives ReadableStream start() Error") + +promise_test(test => { + var [stream, err] = newStreamWithPullError(); + return promise_rejects_exactly(test, err, stream.getReader().read(), 'CustomTestError should propagate') +}, "ReadableStreamDefaultReader Promise receives ReadableStream pull() Error") + + +// test start() errors for all Body reader methods +runRequestPromiseTest(newStreamWithStartError(), 'arrayBuffer', 'ReadableStream start() Error propagates to Response.arrayBuffer() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'blob', 'ReadableStream start() Error propagates to Response.blob() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'formData', 'ReadableStream start() Error propagates to Response.formData() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'json', 'ReadableStream start() Error propagates to Response.json() Promise'); +runRequestPromiseTest(newStreamWithStartError(), 'text', 'ReadableStream start() Error propagates to Response.text() Promise'); + +// test pull() errors for all Body reader methods +runRequestPromiseTest(newStreamWithPullError(), 'arrayBuffer', 'ReadableStream pull() Error propagates to Response.arrayBuffer() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'blob', 'ReadableStream pull() Error propagates to Response.blob() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'formData', 'ReadableStream pull() Error propagates to Response.formData() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'json', 'ReadableStream pull() Error propagates to Response.json() Promise'); +runRequestPromiseTest(newStreamWithPullError(), 'text', 'ReadableStream pull() Error propagates to Response.text() Promise'); diff --git a/testing/web-platform/tests/fetch/api/response/response-error.any.js b/testing/web-platform/tests/fetch/api/response/response-error.any.js new file mode 100644 index 0000000000..a76bc43802 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-error.any.js @@ -0,0 +1,27 @@ +// META: global=window,worker +// META: title=Response error + +var invalidStatus = [0, 100, 199, 600, 1000]; +invalidStatus.forEach(function(status) { + test(function() { + assert_throws_js(RangeError, function() { new Response("", { "status" : status }); }, + "Expect RangeError exception when status is " + status); + },"Throws RangeError when responseInit's status is " + status); +}); + +var invalidStatusText = ["\n", "Ā"]; +invalidStatusText.forEach(function(statusText) { + test(function() { + assert_throws_js(TypeError, function() { new Response("", { "statusText" : statusText }); }, + "Expect TypeError exception " + statusText); + },"Throws TypeError when responseInit's statusText is " + statusText); +}); + +var nullBodyStatus = [204, 205, 304]; +nullBodyStatus.forEach(function(status) { + test(function() { + assert_throws_js(TypeError, + function() { new Response("body", {"status" : status }); }, + "Expect TypeError exception "); + },"Throws TypeError when building a response with body and a body status of " + status); +}); diff --git a/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js b/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js new file mode 100644 index 0000000000..ea5192bfb1 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-from-stream.any.js @@ -0,0 +1,23 @@ +// META: global=window,worker + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + stream.getReader(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which getReader() is called"); + +test(() => { + const stream = new ReadableStream(); + stream.getReader().read(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which read() is called"); + +promise_test(async () => { + const stream = new ReadableStream({ pull: c => c.enqueue(new Uint8Array()) }), + reader = stream.getReader(); + await reader.read(); + reader.releaseLock(); + assert_throws_js(TypeError, () => new Response(stream)); +}, "Constructing a Response with a stream on which read() and releaseLock() are called"); diff --git a/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js b/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js new file mode 100644 index 0000000000..4a67d067a7 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js @@ -0,0 +1,8 @@ +// META: global=window,worker +// META: title=Response: error static method + +promise_test (async () => { + const response = await fetch("../resources/data.json"); + assert_throws_js(TypeError, () => { response.headers.append("name", "value"); }); + assert_not_equals(response.headers.get("name"), "value", "response headers should be immutable"); +}, "Ensure response headers are immutable"); diff --git a/testing/web-platform/tests/fetch/api/response/response-init-001.any.js b/testing/web-platform/tests/fetch/api/response/response-init-001.any.js new file mode 100644 index 0000000000..559e49ad11 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-init-001.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker +// META: title=Response init: simple cases + +var defaultValues = { "type" : "default", + "url" : "", + "ok" : true, + "status" : 200, + "statusText" : "", + "body" : null +}; + +var statusCodes = { "givenValues" : [200, 300, 400, 500, 599], + "expectedValues" : [200, 300, 400, 500, 599] +}; +var statusTexts = { "givenValues" : ["", "OK", "with space", String.fromCharCode(0x80)], + "expectedValues" : ["", "OK", "with space", String.fromCharCode(0x80)] +}; +var initValuesDict = { "status" : statusCodes, + "statusText" : statusTexts +}; + +function isOkStatus(status) { + return 200 <= status && 299 >= status; +} + +var response = new Response(); +for (var attributeName in defaultValues) { + test(function() { + var expectedValue = defaultValues[attributeName]; + assert_equals(response[attributeName], expectedValue, + "Expect default response." + attributeName + " is " + expectedValue); + }, "Check default value for " + attributeName + " attribute"); +} + +for (var attributeName in initValuesDict) { + test(function() { + var valuesToTest = initValuesDict[attributeName]; + for (var valueIdx in valuesToTest["givenValues"]) { + var givenValue = valuesToTest["givenValues"][valueIdx]; + var expectedValue = valuesToTest["expectedValues"][valueIdx]; + var responseInit = {}; + responseInit[attributeName] = givenValue; + var response = new Response("", responseInit); + assert_equals(response[attributeName], expectedValue, + "Expect response." + attributeName + " is " + expectedValue + + " when initialized with " + givenValue); + assert_equals(response.ok, isOkStatus(response.status), + "Expect response.ok is " + isOkStatus(response.status)); + } + }, "Check " + attributeName + " init values and associated getter"); +} + +test(function() { + const response1 = new Response(""); + assert_equals(response1.headers, response1.headers); + + const response2 = new Response("", {"headers": {"X-Foo": "bar"}}); + assert_equals(response2.headers, response2.headers); + const headers = response2.headers; + response2.headers.set("X-Foo", "quux"); + assert_equals(headers, response2.headers); + headers.set("X-Other-Header", "baz"); + assert_equals(headers, response2.headers); +}, "Test that Response.headers has the [SameObject] extended attribute"); diff --git a/testing/web-platform/tests/fetch/api/response/response-init-002.any.js b/testing/web-platform/tests/fetch/api/response/response-init-002.any.js new file mode 100644 index 0000000000..6c0a46e480 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-init-002.any.js @@ -0,0 +1,61 @@ +// META: global=window,worker +// META: title=Response init: body and headers +// META: script=../resources/utils.js + +test(function() { + var headerDict = {"name1": "value1", + "name2": "value2", + "name3": "value3" + }; + var headers = new Headers(headerDict); + var response = new Response("", { "headers" : headers }) + for (var name in headerDict) { + assert_equals(response.headers.get(name), headerDict[name], + "response's headers has " + name + " : " + headerDict[name]); + } +}, "Initialize Response with headers values"); + +function checkResponseInit(body, bodyType, expectedTextBody) { + promise_test(function(test) { + var response = new Response(body); + var resHeaders = response.headers; + var mime = resHeaders.get("Content-Type"); + assert_true(mime && mime.search(bodyType) > -1, "Content-Type header should be \"" + bodyType + "\" "); + return response.text().then(function(bodyAsText) { + //not equals: cannot guess formData exact value + assert_true(bodyAsText.search(expectedTextBody) > -1, "Retrieve and verify response body"); + }); + }, "Initialize Response's body with " + bodyType); +} + +var blob = new Blob(["This is a blob"], {type: "application/octet-binary"}); +var formaData = new FormData(); +formaData.append("name", "value"); +var urlSearchParams = "URLSearchParams are not supported"; +//avoid test timeout if not implemented +if (self.URLSearchParams) + urlSearchParams = new URLSearchParams("name=value"); +var usvString = "This is a USVString" + +checkResponseInit(blob, "application/octet-binary", "This is a blob"); +checkResponseInit(formaData, "multipart/form-data", "name=\"name\"\r\n\r\nvalue"); +checkResponseInit(urlSearchParams, "application/x-www-form-urlencoded;charset=UTF-8", "name=value"); +checkResponseInit(usvString, "text/plain;charset=UTF-8", "This is a USVString"); + +promise_test(function(test) { + var body = "This is response body"; + var response = new Response(body); + return validateStreamFromString(response.body.getReader(), body); +}, "Read Response's body as readableStream"); + +promise_test(function(test) { + var response = new Response("This is my fork", {"headers" : [["Content-Type", ""]]}); + return response.blob().then(function(blob) { + assert_equals(blob.type, "", "Blob type should be the empty string"); + }); +}, "Testing empty Response Content-Type header"); + +test(function() { + var response = new Response(null, {status: 204}); + assert_equals(response.body, null); +}, "Testing null Response body"); diff --git a/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js b/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js new file mode 100644 index 0000000000..3a7744c287 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js @@ -0,0 +1,125 @@ +test(() => { + const response = new Response(); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with empty body"); + +test(() => { + const blob = new Blob([]); + const response = new Response(blob); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with Blob body (no type set)"); + +test(() => { + const blob = new Blob([], { type: "" }); + const response = new Response(blob); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with Blob body (empty type)"); + +test(() => { + const blob = new Blob([], { type: "a/b; c=d" }); + const response = new Response(blob); + assert_equals(response.headers.get("Content-Type"), "a/b; c=d"); +}, "Default Content-Type for Response with Blob body (set type)"); + +test(() => { + const buffer = new Uint8Array(); + const response = new Response(buffer); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with buffer source body"); + +promise_test(async () => { + const formData = new FormData(); + formData.append("a", "b"); + const response = new Response(formData); + const boundary = (await response.text()).split("\r\n")[0].slice(2); + assert_equals( + response.headers.get("Content-Type"), + `multipart/form-data; boundary=${boundary}`, + ); +}, "Default Content-Type for Response with FormData body"); + +test(() => { + const usp = new URLSearchParams(); + const response = new Response(usp); + assert_equals( + response.headers.get("Content-Type"), + "application/x-www-form-urlencoded;charset=UTF-8", + ); +}, "Default Content-Type for Response with URLSearchParams body"); + +test(() => { + const response = new Response(""); + assert_equals( + response.headers.get("Content-Type"), + "text/plain;charset=UTF-8", + ); +}, "Default Content-Type for Response with string body"); + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_equals(response.headers.get("Content-Type"), null); +}, "Default Content-Type for Response with ReadableStream body"); + +// ----------------------------------------------------------------------------- + +const OVERRIDE_MIME = "test/only; mime=type"; + +function responseWithOverrideMime(body) { + return new Response( + body, + { headers: { "Content-Type": OVERRIDE_MIME } }, + ); +} + +test(() => { + const response = responseWithOverrideMime(undefined); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with empty body"); + +test(() => { + const blob = new Blob([]); + const response = responseWithOverrideMime(blob); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with Blob body (no type set)"); + +test(() => { + const blob = new Blob([], { type: "" }); + const response = responseWithOverrideMime(blob); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with Blob body (empty type)"); + +test(() => { + const blob = new Blob([], { type: "a/b; c=d" }); + const response = responseWithOverrideMime(blob); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with Blob body (set type)"); + +test(() => { + const buffer = new Uint8Array(); + const response = responseWithOverrideMime(buffer); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with buffer source body"); + +test(() => { + const formData = new FormData(); + const response = responseWithOverrideMime(formData); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with FormData body"); + +test(() => { + const usp = new URLSearchParams(); + const response = responseWithOverrideMime(usp); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with URLSearchParams body"); + +test(() => { + const response = responseWithOverrideMime(""); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with string body"); + +test(() => { + const stream = new ReadableStream(); + const response = responseWithOverrideMime(stream); + assert_equals(response.headers.get("Content-Type"), OVERRIDE_MIME); +}, "Can override Content-Type for Response with ReadableStream body"); diff --git a/testing/web-platform/tests/fetch/api/response/response-static-error.any.js b/testing/web-platform/tests/fetch/api/response/response-static-error.any.js new file mode 100644 index 0000000000..4097eab37b --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-static-error.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +// META: title=Response: error static method + +test(function() { + var responseError = Response.error(); + assert_equals(responseError.type, "error", "Network error response's type is error"); + assert_equals(responseError.status, 0, "Network error response's status is 0"); + assert_equals(responseError.statusText, "", "Network error response's statusText is empty"); + assert_equals(responseError.body, null, "Network error response's body is null"); + + assert_true(responseError.headers.entries().next().done, "Headers should be empty"); +}, "Check response returned by static method error()"); + +test(function() { + const headers = Response.error().headers; + + // Avoid false positives if expected API is not available + assert_true(!!headers); + assert_equals(typeof headers.append, 'function'); + + assert_throws_js(TypeError, function () { headers.append('name', 'value'); }); +}, "the 'guard' of the Headers instance should be immutable"); diff --git a/testing/web-platform/tests/fetch/api/response/response-static-json.any.js b/testing/web-platform/tests/fetch/api/response/response-static-json.any.js new file mode 100644 index 0000000000..5ec79e69aa --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-static-json.any.js @@ -0,0 +1,96 @@ +// META: global=window,worker +// META: title=Response: json static method + +const APPLICATION_JSON = "application/json"; +const FOO_BAR = "foo/bar"; + +const INIT_TESTS = [ + [undefined, 200, "", APPLICATION_JSON, {}], + [{ status: 400 }, 400, "", APPLICATION_JSON, {}], + [{ statusText: "foo" }, 200, "foo", APPLICATION_JSON, {}], + [{ headers: {} }, 200, "", APPLICATION_JSON, {}], + [{ headers: { "content-type": FOO_BAR } }, 200, "", FOO_BAR, {}], + [{ headers: { "x-foo": "bar" } }, 200, "", APPLICATION_JSON, { "x-foo": "bar" }], +]; + +for (const [init, expectedStatus, expectedStatusText, expectedContentType, expectedHeaders] of INIT_TESTS) { + promise_test(async function () { + const response = Response.json("hello world", init); + assert_equals(response.type, "default", "Response's type is default"); + assert_equals(response.status, expectedStatus, "Response's status is " + expectedStatus); + assert_equals(response.statusText, expectedStatusText, "Response's statusText is " + JSON.stringify(expectedStatusText)); + assert_equals(response.headers.get("content-type"), expectedContentType, "Response's content-type is " + expectedContentType); + for (const key in expectedHeaders) { + assert_equals(response.headers.get(key), expectedHeaders[key], "Response's header " + key + " is " + JSON.stringify(expectedHeaders[key])); + } + + const data = await response.json(); + assert_equals(data, "hello world", "Response's body is 'hello world'"); + }, `Check response returned by static json() with init ${JSON.stringify(init)}`); +} + +const nullBodyStatus = [204, 205, 304]; +for (const status of nullBodyStatus) { + test(function () { + assert_throws_js( + TypeError, + function () { + Response.json("hello world", { status: status }); + }, + ); + }, `Throws TypeError when calling static json() with a status of ${status}`); +} + +promise_test(async function () { + const response = Response.json({ foo: "bar" }); + const data = await response.json(); + assert_equals(typeof data, "object", "Response's json body is an object"); + assert_equals(data.foo, "bar", "Response's json body is { foo: 'bar' }"); +}, "Check static json() encodes JSON objects correctly"); + +test(function () { + assert_throws_js( + TypeError, + function () { + Response.json(Symbol("foo")); + }, + ); +}, "Check static json() throws when data is not encodable"); + +test(function () { + const a = { b: 1 }; + a.a = a; + assert_throws_js( + TypeError, + function () { + Response.json(a); + }, + ); +}, "Check static json() throws when data is circular"); + +promise_test(async function () { + class CustomError extends Error { + name = "CustomError"; + } + assert_throws_js( + CustomError, + function () { + Response.json({ get foo() { throw new CustomError("bar") }}); + } + ) +}, "Check static json() propagates JSON serializer errors"); + +const encodingChecks = [ + ["𝌆", [34, 240, 157, 140, 134, 34]], + ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]], + ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]], +]; + +for (const [input, expected] of encodingChecks) { + promise_test(async function () { + const response = Response.json(input); + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + assert_array_equals(data, expected); + }, `Check response returned by static json() with input ${input}`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js b/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js new file mode 100644 index 0000000000..b16c56d830 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js @@ -0,0 +1,40 @@ +// META: global=window,worker +// META: title=Response: redirect static method + +var url = "http://test.url:1234/"; +test(function() { + const redirectResponse = Response.redirect(url); + assert_equals(redirectResponse.type, "default"); + assert_false(redirectResponse.redirected); + assert_false(redirectResponse.ok); + assert_equals(redirectResponse.status, 302, "Default redirect status is 302"); + assert_equals(redirectResponse.headers.get("Location"), url, + "redirected response has Location header with the correct url"); + assert_equals(redirectResponse.statusText, ""); +}, "Check default redirect response"); + +[301, 302, 303, 307, 308].forEach(function(status) { + test(function() { + const redirectResponse = Response.redirect(url, status); + assert_equals(redirectResponse.type, "default"); + assert_false(redirectResponse.redirected); + assert_false(redirectResponse.ok); + assert_equals(redirectResponse.status, status, "Redirect status is " + status); + assert_equals(redirectResponse.headers.get("Location"), url); + assert_equals(redirectResponse.statusText, ""); + }, "Check response returned by static method redirect(), status = " + status); +}); + +test(function() { + var invalidUrl = "http://:This is not an url"; + assert_throws_js(TypeError, function() { Response.redirect(invalidUrl); }, + "Expect TypeError exception"); +}, "Check error returned when giving invalid url to redirect()"); + +var invalidRedirectStatus = [200, 309, 400, 500]; +invalidRedirectStatus.forEach(function(invalidStatus) { + test(function() { + assert_throws_js(RangeError, function() { Response.redirect(url, invalidStatus); }, + "Expect RangeError exception"); + }, "Check error returned when giving invalid status to redirect(), status = " + invalidStatus); +}); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js new file mode 100644 index 0000000000..d3d92e1677 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: title=Response causes TypeError from bad chunk type + +function runChunkTest(responseReaderMethod, testDescription) { + promise_test(test => { + let stream = new ReadableStream({ + start(controller) { + controller.enqueue("not Uint8Array"); + controller.close(); + } + }); + + return promise_rejects_js(test, TypeError, + new Response(stream)[responseReaderMethod](), + 'TypeError should propagate' + ) + }, testDescription) +} + +runChunkTest('arrayBuffer', 'ReadableStream with non-Uint8Array chunk passed to Response.arrayBuffer() causes TypeError'); +runChunkTest('blob', 'ReadableStream with non-Uint8Array chunk passed to Response.blob() causes TypeError'); +runChunkTest('formData', 'ReadableStream with non-Uint8Array chunk passed to Response.formData() causes TypeError'); +runChunkTest('json', 'ReadableStream with non-Uint8Array chunk passed to Response.json() causes TypeError'); +runChunkTest('text', 'ReadableStream with non-Uint8Array chunk passed to Response.text() causes TypeError'); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js new file mode 100644 index 0000000000..64f65f16f2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js @@ -0,0 +1,44 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + const reader = response.body.getReader(); + reader.releaseLock(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.blob().then(function(blob) { + assert_true(blob instanceof Blob); + }); + }); + }, `Getting blob after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); + + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.text().then(function(text) { + assert_true(text.length > 0); + }); + }); + }, `Getting text after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); + + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.json().then(function(json) { + assert_equals(typeof json, "object"); + }); + }); + }, `Getting json after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); + + promise_test(function() { + return createResponseWithReadableStream(bodySource, function(response) { + return response.arrayBuffer().then(function(arrayBuffer) { + assert_true(arrayBuffer.byteLength > 0); + }); + }); + }, `Getting arrayBuffer after getting the Response body - not disturbed, not locked (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js new file mode 100644 index 0000000000..c46a180a18 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js @@ -0,0 +1,35 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithLockedReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + response.body.getReader(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); + }, `Getting blob after getting a locked Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); + }, `Getting text after getting a locked Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); + }, `Getting json after getting a locked Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithLockedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); + }, `Getting arrayBuffer after getting a locked Response body (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js new file mode 100644 index 0000000000..35fb086469 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js @@ -0,0 +1,36 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithDisturbedReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + const reader = response.body.getReader(); + reader.read(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); + }, `Getting blob after reading the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); + }, `Getting text after reading the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); + }, `Getting json after reading the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithDisturbedReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); + }, `Getting arrayBuffer after reading the Response body (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js new file mode 100644 index 0000000000..490672febd --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js @@ -0,0 +1,35 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +async function createResponseWithCancelledReadableStream(bodySource, callback) { + const response = await responseFromBodySource(bodySource); + response.body.cancel(); + return callback(response); +} + +for (const bodySource of ["fetch", "stream", "string"]) { + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.blob()); + }); + }, `Getting blob after cancelling the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.text()); + }); + }, `Getting text after cancelling the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.json()); + }); + }, `Getting json after cancelling the Response body (body source: ${bodySource})`); + + promise_test(function(test) { + return createResponseWithCancelledReadableStream(bodySource, function(response) { + return promise_rejects_js(test, TypeError, response.arrayBuffer()); + }); + }, `Getting arrayBuffer after cancelling the Response body (body source: ${bodySource})`); +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js new file mode 100644 index 0000000000..348fc39383 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js @@ -0,0 +1,19 @@ +// META: global=window,worker +// META: title=Consuming Response body after getting a ReadableStream +// META: script=./response-stream-disturbed-util.js + +for (const bodySource of ["fetch", "stream", "string"]) { + for (const consumeAs of ["blob", "text", "json", "arrayBuffer"]) { + promise_test( + async () => { + const response = await responseFromBodySource(bodySource); + response[consumeAs](); + assert_not_equals(response.body, null); + assert_throws_js(TypeError, function () { + response.body.getReader(); + }); + }, + `Getting a body reader after consuming as ${consumeAs} (body source: ${bodySource})`, + ); + } +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js new file mode 100644 index 0000000000..61d8544f07 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js @@ -0,0 +1,76 @@ +// META: global=window,worker +// META: title=ReadableStream disturbed tests, via Response's bodyUsed property + +"use strict"; + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read(); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "A non-closed stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream(); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.cancel(); + assert_true(response.bodyUsed, "After calling stream.cancel()"); +}, "A non-closed stream on which cancel() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.close(); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read(); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "A closed stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.error(new Error("some error")); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.read().then(() => { }, () => { }); + assert_true(response.bodyUsed, "After calling stream.read()"); +}, "An errored stream on which read() has been called"); + +test(() => { + const stream = new ReadableStream({ + start(c) { + c.error(new Error("some error")); + } + }); + const response = new Response(stream); + assert_false(response.bodyUsed, "On construction"); + + const reader = stream.getReader(); + assert_false(response.bodyUsed, "After getting a reader"); + + reader.cancel().then(() => { }, () => { }); + assert_true(response.bodyUsed, "After calling stream.cancel()"); +}, "An errored stream on which cancel() has been called"); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js new file mode 100644 index 0000000000..5341b75271 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +test(() => { + const r = new Response(new ReadableStream()); + // highWaterMark: 0 means that nothing will actually be read from the body. + r.body.pipeTo(new WritableStream({}, {highWaterMark: 0})); + assert_true(r.bodyUsed, 'bodyUsed should be true'); +}, 'using pipeTo on Response body should disturb it synchronously'); + +test(() => { + const r = new Response(new ReadableStream()); + r.body.pipeThrough({ + writable: new WritableStream({}, {highWaterMark: 0}), + readable: new ReadableStream() + }); + assert_true(r.bodyUsed, 'bodyUsed should be true'); +}, 'using pipeThrough on Response body should disturb it synchronously'); diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js new file mode 100644 index 0000000000..50bb586aa0 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js @@ -0,0 +1,17 @@ +const BODY = '{"key": "value"}'; + +function responseFromBodySource(bodySource) { + if (bodySource === "fetch") { + return fetch("../resources/data.json"); + } else if (bodySource === "stream") { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(BODY)); + controller.close(); + }, + }); + return new Response(stream); + } else { + return new Response(BODY); + } +} diff --git a/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js b/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js new file mode 100644 index 0000000000..8fef66c8a2 --- /dev/null +++ b/testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js @@ -0,0 +1,117 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +promise_test(async () => { + // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so + // these tests use add_completion_callback for cleanup instead. + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: undefined}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject value: undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(undefined); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(8.2); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject 8.2 via Object.prototype.then.'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const resp = new Response(hello); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' + + 'should not be possible'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const u8a123 = new Uint8Array([1, 2, 3]); + const u8a456 = new Uint8Array([4, 5, 6]); + const resp = new Response(u8a123); + const writtenBytes = []; + const ws = new WritableStream({ + write(chunk) { + writtenBytes.push(...Array.from(chunk)); + } + }); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: u8a456}); + }; + await resp.body.pipeTo(ws); + delete Object.prototype.then; + assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]'); +}, 'intercepting arraybuffer to body readable stream conversion via ' + + 'Object.prototype.then should not be possible'); |