summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fetch/api/response
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/fetch/api/response')
-rw-r--r--testing/web-platform/tests/fetch/api/response/json.any.js14
-rw-r--r--testing/web-platform/tests/fetch/api/response/many-empty-chunks-crash.html14
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/current/current.html3
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/incumbent/incumbent.html16
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/relevant/relevant.html2
-rw-r--r--testing/web-platform/tests/fetch/api/response/multi-globals/url-parsing.html27
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-body-read-task-handling.html86
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-cancel-stream.any.js64
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-clone-iframe.window.js32
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-clone.any.js140
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-consume-empty.any.js99
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-consume-stream.any.js80
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-consume.html317
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-error-from-stream.any.js59
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-error.any.js27
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-from-stream.any.js23
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-headers-guard.any.js8
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-init-001.any.js64
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-init-002.any.js61
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-init-contenttype.any.js125
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-static-error.any.js22
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-static-json.any.js96
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-static-redirect.any.js40
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-bad-chunk.any.js24
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-1.any.js44
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-2.any.js35
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-3.any.js36
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-4.any.js35
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-5.any.js19
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-6.any.js76
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-by-pipe.any.js17
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-disturbed-util.js17
-rw-r--r--testing/web-platform/tests/fetch/api/response/response-stream-with-broken-then.any.js117
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');