Skip to content

Commit

Permalink
feat: add response WPTs (nodejs#1684)
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev authored and metcoder95 committed Dec 26, 2022
1 parent 0e6a52d commit cb6853a
Show file tree
Hide file tree
Showing 27 changed files with 1,086 additions and 1 deletion.
12 changes: 11 additions & 1 deletion lib/fetch/body.js
Expand Up @@ -204,7 +204,17 @@ function extractBody (object, keepalive = false) {
typeof source === 'string' ? new TextEncoder().encode(source) : source
)
queueMicrotask(() => {
controller.close()
try {
controller.close()
} catch (err) {
// TODO(@KhafraDev): this error is thrown in
// response-stream-disturbed-4.any.js - investigate
// why it does so.

if (!/Controller is already closed/.test(err)) {
throw err
}
}
})
}
})
Expand Down
27 changes: 27 additions & 0 deletions test/wpt/server/server.mjs
Expand Up @@ -30,6 +30,7 @@ const server = createServer(async (req, res) => {
const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`)

switch (fullUrl.pathname) {
case '/xhr/resources/utf16-bom.json':
case '/fetch/data-urls/resources/base64.json':
case '/fetch/data-urls/resources/data-urls.json':
case '/fetch/api/resources/empty.txt':
Expand All @@ -39,6 +40,32 @@ const server = createServer(async (req, res) => {
.on('end', () => res.end())
.pipe(res)
}
case '/fetch/api/resources/trickle.py': {
// Note: python's time.sleep(...) takes seconds, while setTimeout
// takes ms.
const delay = parseFloat(fullUrl.searchParams.get('ms') ?? 500)
const count = parseInt(fullUrl.searchParams.get('count') ?? 50)

// eslint-disable-next-line no-unused-vars
for await (const chunk of req); // read request body

await sleep(delay)

if (!fullUrl.searchParams.has('notype')) {
res.setHeader('Content-type', 'text/plain')
}

res.statusCode = 200
await sleep(delay)

for (let i = 0; i < count; i++) {
res.write('TEST_TRICKLE\n')
await sleep(delay)
}

res.end()
break
}
case '/fetch/api/resources/infinite-slow-response.py': {
// https://github.com/web-platform-tests/wpt/blob/master/fetch/api/resources/infinite-slow-response.py
const stateKey = fullUrl.searchParams.get('stateKey') ?? ''
Expand Down
13 changes: 13 additions & 0 deletions test/wpt/status/fetch.status.json
Expand Up @@ -15,5 +15,18 @@
"fail": [
"Input request used for creating new request became disturbed even if body is not used"
]
},
"response-error-from-stream.any.js": {
"fail": [
"ReadableStream start() Error propagates to Response.formData() Promise",
"ReadableStream pull() Error propagates to Response.formData() Promise"
]
},
"response-consume-empty.any.js": {
"fail": [
"Consume response's body as blob",
"Consume response's body as formData with correct multipart type (error case)",
"Consume empty FormData response body as text"
]
}
}
1 change: 1 addition & 0 deletions test/wpt/tests/fetch/api/resources/top.txt
@@ -0,0 +1 @@
top
14 changes: 14 additions & 0 deletions test/wpt/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");
57 changes: 57 additions & 0 deletions test/wpt/tests/fetch/api/response/response-cancel-stream.any.js
@@ -0,0 +1,57 @@
// 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");
99 changes: 99 additions & 0 deletions test/wpt/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);
59 changes: 59 additions & 0 deletions test/wpt/tests/fetch/api/response/response-consume-stream.any.js
@@ -0,0 +1,59 @@
// 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);

promise_test(function(test) {
var response = new Response(blob);
return validateStreamFromString(response.body.getReader(), textData);
}, "Read blob response's body as readableStream");

promise_test(function(test) {
var response = new Response(textData);
return validateStreamFromString(response.body.getReader(), textData);
}, "Read text response's body as readableStream");

promise_test(function(test) {
var response = new Response(urlSearchParams);
return validateStreamFromString(response.body.getReader(), urlSearchParamsData);
}, "Read URLSearchParams response's body as readableStream");

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(), textData);
}, "Read array buffer response's body as readableStream");

promise_test(function(test) {
var response = new Response(formData);
return validateStreamFromPartialString(response.body.getReader(),
"Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue");
}, "Read form data response's body as readableStream");

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");
@@ -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');

0 comments on commit cb6853a

Please sign in to comment.