mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 00:29:09 -05:00
feat(fetch): implement abort (#10863)
This commit introduces fetch aborting via an AbortSignal.
This commit is contained in:
parent
3f9187c366
commit
1fb2e23a67
10 changed files with 325 additions and 63 deletions
|
@ -3227,6 +3227,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function errorReadableStream(stream, e) {
|
||||||
|
readableStreamDefaultControllerError(stream[_controller], e);
|
||||||
|
}
|
||||||
|
|
||||||
/** @template R */
|
/** @template R */
|
||||||
class ReadableStreamGenericReader {
|
class ReadableStreamGenericReader {
|
||||||
/** @type {Deferred<void>} */
|
/** @type {Deferred<void>} */
|
||||||
|
@ -3873,6 +3877,7 @@
|
||||||
window.__bootstrap.streams = {
|
window.__bootstrap.streams = {
|
||||||
// Non-Public
|
// Non-Public
|
||||||
isReadableStreamDisturbed,
|
isReadableStreamDisturbed,
|
||||||
|
errorReadableStream,
|
||||||
// Exposed in global runtime scope
|
// Exposed in global runtime scope
|
||||||
ByteLengthQueuingStrategy,
|
ByteLengthQueuingStrategy,
|
||||||
CountQueuingStrategy,
|
CountQueuingStrategy,
|
||||||
|
|
|
@ -20,7 +20,8 @@
|
||||||
const { parseFormData, formDataFromEntries, encodeFormData } =
|
const { parseFormData, formDataFromEntries, encodeFormData } =
|
||||||
globalThis.__bootstrap.formData;
|
globalThis.__bootstrap.formData;
|
||||||
const mimesniff = globalThis.__bootstrap.mimesniff;
|
const mimesniff = globalThis.__bootstrap.mimesniff;
|
||||||
const { isReadableStreamDisturbed } = globalThis.__bootstrap.streams;
|
const { isReadableStreamDisturbed, errorReadableStream } =
|
||||||
|
globalThis.__bootstrap.streams;
|
||||||
|
|
||||||
class InnerBody {
|
class InnerBody {
|
||||||
/** @type {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} */
|
/** @type {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} */
|
||||||
|
@ -106,6 +107,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancel(error) {
|
||||||
|
if (this.streamOrStatic instanceof ReadableStream) {
|
||||||
|
this.streamOrStatic.cancel(error);
|
||||||
|
} else {
|
||||||
|
this.streamOrStatic.consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(error) {
|
||||||
|
if (this.streamOrStatic instanceof ReadableStream) {
|
||||||
|
errorReadableStream(this.streamOrStatic, error);
|
||||||
|
} else {
|
||||||
|
this.streamOrStatic.consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {InnerBody}
|
* @returns {InnerBody}
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,9 +26,11 @@
|
||||||
getDecodeSplitHeader,
|
getDecodeSplitHeader,
|
||||||
} = window.__bootstrap.headers;
|
} = window.__bootstrap.headers;
|
||||||
const { HttpClient } = window.__bootstrap.fetch;
|
const { HttpClient } = window.__bootstrap.fetch;
|
||||||
|
const abortSignal = window.__bootstrap.abortSignal;
|
||||||
|
|
||||||
const _request = Symbol("request");
|
const _request = Symbol("request");
|
||||||
const _headers = Symbol("headers");
|
const _headers = Symbol("headers");
|
||||||
|
const _signal = Symbol("signal");
|
||||||
const _mimeType = Symbol("mime type");
|
const _mimeType = Symbol("mime type");
|
||||||
const _body = Symbol("body");
|
const _body = Symbol("body");
|
||||||
|
|
||||||
|
@ -145,6 +147,8 @@
|
||||||
[_request];
|
[_request];
|
||||||
/** @type {Headers} */
|
/** @type {Headers} */
|
||||||
[_headers];
|
[_headers];
|
||||||
|
/** @type {AbortSignal} */
|
||||||
|
[_signal];
|
||||||
get [_mimeType]() {
|
get [_mimeType]() {
|
||||||
let charset = null;
|
let charset = null;
|
||||||
let essence = null;
|
let essence = null;
|
||||||
|
@ -206,6 +210,9 @@
|
||||||
let request;
|
let request;
|
||||||
const baseURL = getLocationHref();
|
const baseURL = getLocationHref();
|
||||||
|
|
||||||
|
// 4.
|
||||||
|
let signal = null;
|
||||||
|
|
||||||
// 5.
|
// 5.
|
||||||
if (typeof input === "string") {
|
if (typeof input === "string") {
|
||||||
const parsedURL = new URL(input, baseURL);
|
const parsedURL = new URL(input, baseURL);
|
||||||
|
@ -213,8 +220,12 @@
|
||||||
} else { // 6.
|
} else { // 6.
|
||||||
if (!(input instanceof Request)) throw new TypeError("Unreachable");
|
if (!(input instanceof Request)) throw new TypeError("Unreachable");
|
||||||
request = input[_request];
|
request = input[_request];
|
||||||
|
signal = input[_signal];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 12.
|
||||||
|
// TODO(lucacasonato): create a copy of `request`
|
||||||
|
|
||||||
// 22.
|
// 22.
|
||||||
if (init.redirect !== undefined) {
|
if (init.redirect !== undefined) {
|
||||||
request.redirectMode = init.redirect;
|
request.redirectMode = init.redirect;
|
||||||
|
@ -227,6 +238,11 @@
|
||||||
request.method = method;
|
request.method = method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 26.
|
||||||
|
if (init.signal !== undefined) {
|
||||||
|
signal = init.signal;
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: non standard extension. This handles Deno.HttpClient parameter
|
// NOTE: non standard extension. This handles Deno.HttpClient parameter
|
||||||
if (init.client !== undefined) {
|
if (init.client !== undefined) {
|
||||||
if (init.client !== null && !(init.client instanceof HttpClient)) {
|
if (init.client !== null && !(init.client instanceof HttpClient)) {
|
||||||
|
@ -242,6 +258,12 @@
|
||||||
// 27.
|
// 27.
|
||||||
this[_request] = request;
|
this[_request] = request;
|
||||||
|
|
||||||
|
// 28.
|
||||||
|
this[_signal] = abortSignal.newSignal();
|
||||||
|
if (signal !== null) {
|
||||||
|
abortSignal.follow(this[_signal], signal);
|
||||||
|
}
|
||||||
|
|
||||||
// 29.
|
// 29.
|
||||||
this[_headers] = headersFromHeaderList(request.headerList, "request");
|
this[_headers] = headersFromHeaderList(request.headerList, "request");
|
||||||
|
|
||||||
|
@ -299,6 +321,9 @@
|
||||||
|
|
||||||
// 40.
|
// 40.
|
||||||
request.body = finalBody;
|
request.body = finalBody;
|
||||||
|
|
||||||
|
// 41.
|
||||||
|
// TODO(lucacasonato): Extranious? https://github.com/whatwg/fetch/issues/1249
|
||||||
}
|
}
|
||||||
|
|
||||||
get method() {
|
get method() {
|
||||||
|
@ -321,13 +346,24 @@
|
||||||
return this[_request].redirectMode;
|
return this[_request].redirectMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get signal() {
|
||||||
|
webidl.assertBranded(this, Request);
|
||||||
|
return this[_signal];
|
||||||
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
webidl.assertBranded(this, Request);
|
webidl.assertBranded(this, Request);
|
||||||
if (this[_body] && this[_body].unusable()) {
|
if (this[_body] && this[_body].unusable()) {
|
||||||
throw new TypeError("Body is unusable.");
|
throw new TypeError("Body is unusable.");
|
||||||
}
|
}
|
||||||
const newReq = cloneInnerRequest(this[_request]);
|
const newReq = cloneInnerRequest(this[_request]);
|
||||||
return fromInnerRequest(newReq, guardFromHeaders(this[_headers]));
|
const newSignal = abortSignal.newSignal();
|
||||||
|
abortSignal.follow(newSignal, this[_signal]);
|
||||||
|
return fromInnerRequest(
|
||||||
|
newReq,
|
||||||
|
newSignal,
|
||||||
|
guardFromHeaders(this[_headers]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get [Symbol.toStringTag]() {
|
get [Symbol.toStringTag]() {
|
||||||
|
@ -364,6 +400,10 @@
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(Request.prototype, "signal", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
Object.defineProperty(Request.prototype, "clone", {
|
Object.defineProperty(Request.prototype, "clone", {
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
writable: true,
|
writable: true,
|
||||||
|
@ -403,6 +443,12 @@
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ key: "redirect", converter: webidl.converters["RequestRedirect"] },
|
{ key: "redirect", converter: webidl.converters["RequestRedirect"] },
|
||||||
|
{
|
||||||
|
key: "signal",
|
||||||
|
converter: webidl.createNullableConverter(
|
||||||
|
webidl.converters["AbortSignal"],
|
||||||
|
),
|
||||||
|
},
|
||||||
{ key: "client", converter: webidl.converters.any },
|
{ key: "client", converter: webidl.converters.any },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -420,9 +466,10 @@
|
||||||
* @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
|
* @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
|
||||||
* @returns {Request}
|
* @returns {Request}
|
||||||
*/
|
*/
|
||||||
function fromInnerRequest(inner, guard) {
|
function fromInnerRequest(inner, signal, guard) {
|
||||||
const request = webidl.createBranded(Request);
|
const request = webidl.createBranded(Request);
|
||||||
request[_request] = inner;
|
request[_request] = inner;
|
||||||
|
request[_signal] = signal;
|
||||||
request[_headers] = headersFromHeaderList(inner.headerList, guard);
|
request[_headers] = headersFromHeaderList(inner.headerList, guard);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
* @property {string} statusMessage
|
* @property {string} statusMessage
|
||||||
* @property {[string, string][]} headerList
|
* @property {[string, string][]} headerList
|
||||||
* @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body
|
* @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body
|
||||||
|
* @property {boolean} aborted
|
||||||
* @property {string} [error]
|
* @property {string} [error]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -92,12 +93,14 @@
|
||||||
urlList,
|
urlList,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusMessage: response.statusMessage,
|
statusMessage: response.statusMessage,
|
||||||
|
aborted: response.aborted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultInnerResponse = {
|
const defaultInnerResponse = {
|
||||||
type: "default",
|
type: "default",
|
||||||
body: null,
|
body: null,
|
||||||
|
aborted: false,
|
||||||
url() {
|
url() {
|
||||||
if (this.urlList.length == 0) return null;
|
if (this.urlList.length == 0) return null;
|
||||||
return this.urlList[this.urlList.length - 1];
|
return this.urlList[this.urlList.length - 1];
|
||||||
|
@ -128,6 +131,15 @@
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {InnerResponse}
|
||||||
|
*/
|
||||||
|
function abortedNetworkError() {
|
||||||
|
const resp = networkError("aborted");
|
||||||
|
resp.aborted = true;
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
class Response {
|
class Response {
|
||||||
/** @type {InnerResponse} */
|
/** @type {InnerResponse} */
|
||||||
[_response];
|
[_response];
|
||||||
|
@ -446,4 +458,5 @@
|
||||||
window.__bootstrap.fetch.redirectStatus = redirectStatus;
|
window.__bootstrap.fetch.redirectStatus = redirectStatus;
|
||||||
window.__bootstrap.fetch.nullBodyStatus = nullBodyStatus;
|
window.__bootstrap.fetch.nullBodyStatus = nullBodyStatus;
|
||||||
window.__bootstrap.fetch.networkError = networkError;
|
window.__bootstrap.fetch.networkError = networkError;
|
||||||
|
window.__bootstrap.fetch.abortedNetworkError = abortedNetworkError;
|
||||||
})(globalThis);
|
})(globalThis);
|
||||||
|
|
|
@ -15,14 +15,18 @@
|
||||||
const core = window.Deno.core;
|
const core = window.Deno.core;
|
||||||
const webidl = window.__bootstrap.webidl;
|
const webidl = window.__bootstrap.webidl;
|
||||||
const { byteLowerCase } = window.__bootstrap.infra;
|
const { byteLowerCase } = window.__bootstrap.infra;
|
||||||
|
const { errorReadableStream } = window.__bootstrap.streams;
|
||||||
const { InnerBody, extractBody } = window.__bootstrap.fetchBody;
|
const { InnerBody, extractBody } = window.__bootstrap.fetchBody;
|
||||||
const {
|
const {
|
||||||
toInnerRequest,
|
toInnerRequest,
|
||||||
|
toInnerResponse,
|
||||||
fromInnerResponse,
|
fromInnerResponse,
|
||||||
redirectStatus,
|
redirectStatus,
|
||||||
nullBodyStatus,
|
nullBodyStatus,
|
||||||
networkError,
|
networkError,
|
||||||
|
abortedNetworkError,
|
||||||
} = window.__bootstrap.fetch;
|
} = window.__bootstrap.fetch;
|
||||||
|
const abortSignal = window.__bootstrap.abortSignal;
|
||||||
|
|
||||||
const REQUEST_BODY_HEADER_NAMES = [
|
const REQUEST_BODY_HEADER_NAMES = [
|
||||||
"content-encoding",
|
"content-encoding",
|
||||||
|
@ -68,10 +72,26 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} responseBodyRid
|
* @param {number} responseBodyRid
|
||||||
|
* @param {AbortSignal} [terminator]
|
||||||
* @returns {ReadableStream<Uint8Array>}
|
* @returns {ReadableStream<Uint8Array>}
|
||||||
*/
|
*/
|
||||||
function createResponseBodyStream(responseBodyRid) {
|
function createResponseBodyStream(responseBodyRid, terminator) {
|
||||||
return new ReadableStream({
|
function onAbort() {
|
||||||
|
if (readable) {
|
||||||
|
errorReadableStream(
|
||||||
|
readable,
|
||||||
|
new DOMException("Ongoing fetch was aborted.", "AbortError"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
core.close(responseBodyRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(lucacasonato): clean up registration
|
||||||
|
terminator[abortSignal.add](onAbort);
|
||||||
|
const readable = new ReadableStream({
|
||||||
type: "bytes",
|
type: "bytes",
|
||||||
async pull(controller) {
|
async pull(controller) {
|
||||||
try {
|
try {
|
||||||
|
@ -88,28 +108,45 @@
|
||||||
} else {
|
} else {
|
||||||
// We have reached the end of the body, so we close the stream.
|
// We have reached the end of the body, so we close the stream.
|
||||||
controller.close();
|
controller.close();
|
||||||
|
try {
|
||||||
core.close(responseBodyRid);
|
core.close(responseBodyRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (terminator.aborted) {
|
||||||
|
controller.error(
|
||||||
|
new DOMException("Ongoing fetch was aborted.", "AbortError"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
// There was an error while reading a chunk of the body, so we
|
// There was an error while reading a chunk of the body, so we
|
||||||
// error.
|
// error.
|
||||||
controller.error(err);
|
controller.error(err);
|
||||||
controller.close();
|
}
|
||||||
|
try {
|
||||||
core.close(responseBodyRid);
|
core.close(responseBodyRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
core.close(responseBodyRid);
|
if (!terminator.aborted) {
|
||||||
|
terminator[abortSignal.signalAbort]();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return readable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {InnerRequest} req
|
* @param {InnerRequest} req
|
||||||
* @param {boolean} recursive
|
* @param {boolean} recursive
|
||||||
|
* @param {AbortSignal} terminator
|
||||||
* @returns {Promise<InnerResponse>}
|
* @returns {Promise<InnerResponse>}
|
||||||
*/
|
*/
|
||||||
async function mainFetch(req, recursive) {
|
async function mainFetch(req, recursive, terminator) {
|
||||||
/** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
|
/** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
|
||||||
let reqBody = null;
|
let reqBody = null;
|
||||||
if (req.body !== null) {
|
if (req.body !== null) {
|
||||||
|
@ -130,7 +167,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { requestRid, requestBodyRid } = opFetch({
|
const { requestRid, requestBodyRid, cancelHandleRid } = opFetch({
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.currentUrl(),
|
url: req.currentUrl(),
|
||||||
headers: req.headerList,
|
headers: req.headerList,
|
||||||
|
@ -138,6 +175,20 @@
|
||||||
hasBody: reqBody !== null,
|
hasBody: reqBody !== null,
|
||||||
}, reqBody instanceof Uint8Array ? reqBody : null);
|
}, reqBody instanceof Uint8Array ? reqBody : null);
|
||||||
|
|
||||||
|
function onAbort() {
|
||||||
|
try {
|
||||||
|
core.close(cancelHandleRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
core.close(requestBodyRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terminator[abortSignal.add](onAbort);
|
||||||
|
|
||||||
if (requestBodyRid !== null) {
|
if (requestBodyRid !== null) {
|
||||||
if (reqBody === null || !(reqBody instanceof ReadableStream)) {
|
if (reqBody === null || !(reqBody instanceof ReadableStream)) {
|
||||||
throw new TypeError("Unreachable");
|
throw new TypeError("Unreachable");
|
||||||
|
@ -145,24 +196,49 @@
|
||||||
const reader = reqBody.getReader();
|
const reader = reqBody.getReader();
|
||||||
(async () => {
|
(async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read().catch((err) => {
|
||||||
|
if (terminator.aborted) return { done: true, value: undefined };
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
if (done) break;
|
if (done) break;
|
||||||
if (!(value instanceof Uint8Array)) {
|
if (!(value instanceof Uint8Array)) {
|
||||||
await reader.cancel("value not a Uint8Array");
|
await reader.cancel("value not a Uint8Array");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await opFetchRequestWrite(requestBodyRid, value);
|
await opFetchRequestWrite(requestBodyRid, value).catch((err) => {
|
||||||
|
if (terminator.aborted) return;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
if (terminator.aborted) break;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await reader.cancel(err);
|
await reader.cancel(err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
core.close(requestBodyRid);
|
core.close(requestBodyRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await opFetchSend(requestRid);
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await opFetchSend(requestRid).catch((err) => {
|
||||||
|
if (terminator.aborted) return;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
core.close(cancelHandleRid);
|
||||||
|
} catch (_) {
|
||||||
|
// might have already been closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (terminator.aborted) return abortedNetworkError();
|
||||||
|
|
||||||
/** @type {InnerResponse} */
|
/** @type {InnerResponse} */
|
||||||
const response = {
|
const response = {
|
||||||
headerList: resp.headers,
|
headerList: resp.headers,
|
||||||
|
@ -185,7 +261,7 @@
|
||||||
);
|
);
|
||||||
case "follow":
|
case "follow":
|
||||||
core.close(resp.responseRid);
|
core.close(resp.responseRid);
|
||||||
return httpRedirectFetch(req, response);
|
return httpRedirectFetch(req, response, terminator);
|
||||||
case "manual":
|
case "manual":
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -194,7 +270,9 @@
|
||||||
if (nullBodyStatus(response.status)) {
|
if (nullBodyStatus(response.status)) {
|
||||||
core.close(resp.responseRid);
|
core.close(resp.responseRid);
|
||||||
} else {
|
} else {
|
||||||
response.body = new InnerBody(createResponseBodyStream(resp.responseRid));
|
response.body = new InnerBody(
|
||||||
|
createResponseBodyStream(resp.responseRid, terminator),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recursive) return response;
|
if (recursive) return response;
|
||||||
|
@ -211,7 +289,7 @@
|
||||||
* @param {InnerResponse} response
|
* @param {InnerResponse} response
|
||||||
* @returns {Promise<InnerResponse>}
|
* @returns {Promise<InnerResponse>}
|
||||||
*/
|
*/
|
||||||
function httpRedirectFetch(request, response) {
|
function httpRedirectFetch(request, response, terminator) {
|
||||||
const locationHeaders = response.headerList.filter((entry) =>
|
const locationHeaders = response.headerList.filter((entry) =>
|
||||||
byteLowerCase(entry[0]) === "location"
|
byteLowerCase(entry[0]) === "location"
|
||||||
);
|
);
|
||||||
|
@ -264,14 +342,16 @@
|
||||||
request.body = res.body;
|
request.body = res.body;
|
||||||
}
|
}
|
||||||
request.urlList.push(locationURL.href);
|
request.urlList.push(locationURL.href);
|
||||||
return mainFetch(request, true);
|
return mainFetch(request, true, terminator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {RequestInfo} input
|
* @param {RequestInfo} input
|
||||||
* @param {RequestInit} init
|
* @param {RequestInit} init
|
||||||
*/
|
*/
|
||||||
async function fetch(input, init = {}) {
|
function fetch(input, init = {}) {
|
||||||
|
// 1.
|
||||||
|
const p = new Promise((resolve, reject) => {
|
||||||
const prefix = "Failed to call 'fetch'";
|
const prefix = "Failed to call 'fetch'";
|
||||||
webidl.requiredArguments(arguments.length, 1, { prefix });
|
webidl.requiredArguments(arguments.length, 1, { prefix });
|
||||||
input = webidl.converters["RequestInfo"](input, {
|
input = webidl.converters["RequestInfo"](input, {
|
||||||
|
@ -283,24 +363,69 @@
|
||||||
context: "Argument 2",
|
context: "Argument 2",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1.
|
|
||||||
const requestObject = new Request(input, init);
|
|
||||||
// 2.
|
// 2.
|
||||||
|
const requestObject = new Request(input, init);
|
||||||
|
// 3.
|
||||||
const request = toInnerRequest(requestObject);
|
const request = toInnerRequest(requestObject);
|
||||||
|
// 4.
|
||||||
|
if (requestObject.signal.aborted) {
|
||||||
|
reject(abortFetch(request, null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7.
|
||||||
|
let responseObject = null;
|
||||||
|
// 9.
|
||||||
|
let locallyAborted = false;
|
||||||
// 10.
|
// 10.
|
||||||
|
function onabort() {
|
||||||
|
locallyAborted = true;
|
||||||
|
reject(abortFetch(request, responseObject));
|
||||||
|
}
|
||||||
|
requestObject.signal[abortSignal.add](onabort);
|
||||||
|
|
||||||
if (!requestObject.headers.has("Accept")) {
|
if (!requestObject.headers.has("Accept")) {
|
||||||
request.headerList.push(["Accept", "*/*"]);
|
request.headerList.push(["Accept", "*/*"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12.
|
// 12.
|
||||||
const response = await mainFetch(request, false);
|
mainFetch(request, false, requestObject.signal).then((response) => {
|
||||||
|
// 12.1.
|
||||||
|
if (locallyAborted) return;
|
||||||
|
// 12.2.
|
||||||
|
if (response.aborted) {
|
||||||
|
reject(request, responseObject);
|
||||||
|
requestObject.signal[abortSignal.remove](onabort);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 12.3.
|
||||||
if (response.type === "error") {
|
if (response.type === "error") {
|
||||||
throw new TypeError(
|
const err = new TypeError(
|
||||||
"Fetch failed: " + (response.error ?? "unknown error"),
|
"Fetch failed: " + (response.error ?? "unknown error"),
|
||||||
);
|
);
|
||||||
|
reject(err);
|
||||||
|
requestObject.signal[abortSignal.remove](onabort);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responseObject = fromInnerResponse(response, "immutable");
|
||||||
|
resolve(responseObject);
|
||||||
|
requestObject.signal[abortSignal.remove](onabort);
|
||||||
|
}).catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
requestObject.signal[abortSignal.remove](onabort);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromInnerResponse(response, "immutable");
|
function abortFetch(request, responseObject) {
|
||||||
|
const error = new DOMException("Ongoing fetch was aborted.", "AbortError");
|
||||||
|
if (request.body !== null) request.body.cancel(error);
|
||||||
|
if (responseObject !== null) {
|
||||||
|
const response = toInnerResponse(responseObject);
|
||||||
|
if (response.body !== null) response.body.error(error);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.__bootstrap.fetch ??= {};
|
window.__bootstrap.fetch ??= {};
|
||||||
|
|
1
extensions/fetch/internal.d.ts
vendored
1
extensions/fetch/internal.d.ts
vendored
|
@ -82,6 +82,7 @@ declare namespace globalThis {
|
||||||
function toInnerRequest(request: Request): InnerRequest;
|
function toInnerRequest(request: Request): InnerRequest;
|
||||||
function fromInnerRequest(
|
function fromInnerRequest(
|
||||||
inner: InnerRequest,
|
inner: InnerRequest,
|
||||||
|
signal: AbortSignal | null,
|
||||||
guard:
|
guard:
|
||||||
| "request"
|
| "request"
|
||||||
| "immutable"
|
| "immutable"
|
||||||
|
|
|
@ -16,6 +16,7 @@ use deno_core::AsyncRefCell;
|
||||||
use deno_core::CancelFuture;
|
use deno_core::CancelFuture;
|
||||||
use deno_core::CancelHandle;
|
use deno_core::CancelHandle;
|
||||||
use deno_core::CancelTryFuture;
|
use deno_core::CancelTryFuture;
|
||||||
|
use deno_core::Canceled;
|
||||||
use deno_core::Extension;
|
use deno_core::Extension;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
use deno_core::RcRef;
|
use deno_core::RcRef;
|
||||||
|
@ -131,6 +132,7 @@ pub struct FetchArgs {
|
||||||
pub struct FetchReturn {
|
pub struct FetchReturn {
|
||||||
request_rid: ResourceId,
|
request_rid: ResourceId,
|
||||||
request_body_rid: Option<ResourceId>,
|
request_body_rid: Option<ResourceId>,
|
||||||
|
cancel_handle_rid: Option<ResourceId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn op_fetch<FP>(
|
pub fn op_fetch<FP>(
|
||||||
|
@ -157,7 +159,7 @@ where
|
||||||
|
|
||||||
// Check scheme before asking for net permission
|
// Check scheme before asking for net permission
|
||||||
let scheme = url.scheme();
|
let scheme = url.scheme();
|
||||||
let (request_rid, request_body_rid) = match scheme {
|
let (request_rid, request_body_rid, cancel_handle_rid) = match scheme {
|
||||||
"http" | "https" => {
|
"http" | "https" => {
|
||||||
let permissions = state.borrow_mut::<FP>();
|
let permissions = state.borrow_mut::<FP>();
|
||||||
permissions.check_net_url(&url)?;
|
permissions.check_net_url(&url)?;
|
||||||
|
@ -195,13 +197,19 @@ where
|
||||||
request = request.header(name, v);
|
request = request.header(name, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
let fut = request.send();
|
let cancel_handle = CancelHandle::new_rc();
|
||||||
|
let cancel_handle_ = cancel_handle.clone();
|
||||||
|
|
||||||
|
let fut = async move { request.send().or_cancel(cancel_handle_).await };
|
||||||
|
|
||||||
let request_rid = state
|
let request_rid = state
|
||||||
.resource_table
|
.resource_table
|
||||||
.add(FetchRequestResource(Box::pin(fut)));
|
.add(FetchRequestResource(Box::pin(fut)));
|
||||||
|
|
||||||
(request_rid, request_body_rid)
|
let cancel_handle_rid =
|
||||||
|
state.resource_table.add(FetchCancelHandle(cancel_handle));
|
||||||
|
|
||||||
|
(request_rid, request_body_rid, Some(cancel_handle_rid))
|
||||||
}
|
}
|
||||||
"data" => {
|
"data" => {
|
||||||
let data_url = DataUrl::process(url.as_str())
|
let data_url = DataUrl::process(url.as_str())
|
||||||
|
@ -216,13 +224,13 @@ where
|
||||||
.header(http::header::CONTENT_TYPE, data_url.mime_type().to_string())
|
.header(http::header::CONTENT_TYPE, data_url.mime_type().to_string())
|
||||||
.body(reqwest::Body::from(body))?;
|
.body(reqwest::Body::from(body))?;
|
||||||
|
|
||||||
let fut = async move { Ok(Response::from(response)) };
|
let fut = async move { Ok(Ok(Response::from(response))) };
|
||||||
|
|
||||||
let request_rid = state
|
let request_rid = state
|
||||||
.resource_table
|
.resource_table
|
||||||
.add(FetchRequestResource(Box::pin(fut)));
|
.add(FetchRequestResource(Box::pin(fut)));
|
||||||
|
|
||||||
(request_rid, None)
|
(request_rid, None, None)
|
||||||
}
|
}
|
||||||
"blob" => {
|
"blob" => {
|
||||||
let blob_url_storage =
|
let blob_url_storage =
|
||||||
|
@ -244,13 +252,13 @@ where
|
||||||
.header(http::header::CONTENT_TYPE, blob.media_type)
|
.header(http::header::CONTENT_TYPE, blob.media_type)
|
||||||
.body(reqwest::Body::from(blob.data))?;
|
.body(reqwest::Body::from(blob.data))?;
|
||||||
|
|
||||||
let fut = async move { Ok(Response::from(response)) };
|
let fut = async move { Ok(Ok(Response::from(response))) };
|
||||||
|
|
||||||
let request_rid = state
|
let request_rid = state
|
||||||
.resource_table
|
.resource_table
|
||||||
.add(FetchRequestResource(Box::pin(fut)));
|
.add(FetchRequestResource(Box::pin(fut)));
|
||||||
|
|
||||||
(request_rid, None)
|
(request_rid, None, None)
|
||||||
}
|
}
|
||||||
_ => return Err(type_error(format!("scheme '{}' not supported", scheme))),
|
_ => return Err(type_error(format!("scheme '{}' not supported", scheme))),
|
||||||
};
|
};
|
||||||
|
@ -258,6 +266,7 @@ where
|
||||||
Ok(FetchReturn {
|
Ok(FetchReturn {
|
||||||
request_rid,
|
request_rid,
|
||||||
request_body_rid,
|
request_body_rid,
|
||||||
|
cancel_handle_rid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,8 +296,9 @@ pub async fn op_fetch_send(
|
||||||
.expect("multiple op_fetch_send ongoing");
|
.expect("multiple op_fetch_send ongoing");
|
||||||
|
|
||||||
let res = match request.0.await {
|
let res = match request.0.await {
|
||||||
Ok(res) => res,
|
Ok(Ok(res)) => res,
|
||||||
Err(e) => return Err(type_error(e.to_string())),
|
Ok(Err(err)) => return Err(type_error(err.to_string())),
|
||||||
|
Err(_) => return Err(type_error("request was cancelled")),
|
||||||
};
|
};
|
||||||
|
|
||||||
//debug!("Fetch response {}", url);
|
//debug!("Fetch response {}", url);
|
||||||
|
@ -372,8 +382,11 @@ pub async fn op_fetch_response_read(
|
||||||
Ok(read)
|
Ok(read)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CancelableResponseResult =
|
||||||
|
Result<Result<Response, reqwest::Error>, Canceled>;
|
||||||
|
|
||||||
struct FetchRequestResource(
|
struct FetchRequestResource(
|
||||||
Pin<Box<dyn Future<Output = Result<Response, reqwest::Error>>>>,
|
Pin<Box<dyn Future<Output = CancelableResponseResult>>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Resource for FetchRequestResource {
|
impl Resource for FetchRequestResource {
|
||||||
|
@ -382,6 +395,18 @@ impl Resource for FetchRequestResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FetchCancelHandle(Rc<CancelHandle>);
|
||||||
|
|
||||||
|
impl Resource for FetchCancelHandle {
|
||||||
|
fn name(&self) -> Cow<str> {
|
||||||
|
"fetchCancelHandle".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(self: Rc<Self>) {
|
||||||
|
self.0.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct FetchRequestBodyResource {
|
struct FetchRequestBodyResource {
|
||||||
body: AsyncRefCell<mpsc::Sender<std::io::Result<Vec<u8>>>>,
|
body: AsyncRefCell<mpsc::Sender<std::io::Result<Vec<u8>>>>,
|
||||||
cancel: CancelHandle,
|
cancel: CancelHandle,
|
||||||
|
@ -391,6 +416,10 @@ impl Resource for FetchRequestBodyResource {
|
||||||
fn name(&self) -> Cow<str> {
|
fn name(&self) -> Cow<str> {
|
||||||
"fetchRequestBody".into()
|
"fetchRequestBody".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn close(self: Rc<Self>) {
|
||||||
|
self.cancel.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BytesStream =
|
type BytesStream =
|
||||||
|
@ -405,6 +434,10 @@ impl Resource for FetchResponseBodyResource {
|
||||||
fn name(&self) -> Cow<str> {
|
fn name(&self) -> Cow<str> {
|
||||||
"fetchResponseBody".into()
|
"fetchResponseBody".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn close(self: Rc<Self>) {
|
||||||
|
self.cancel.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HttpClientResource {
|
struct HttpClientResource {
|
||||||
|
|
|
@ -118,11 +118,25 @@
|
||||||
AbortSignal,
|
AbortSignal,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function newSignal() {
|
||||||
|
return new AbortSignal(illegalConstructorKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function follow(followingSignal, parentSignal) {
|
||||||
|
if (parentSignal.aborted) {
|
||||||
|
followingSignal[signalAbort]();
|
||||||
|
} else {
|
||||||
|
parentSignal[add](() => followingSignal[signalAbort]());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.AbortSignal = AbortSignal;
|
window.AbortSignal = AbortSignal;
|
||||||
window.AbortController = AbortController;
|
window.AbortController = AbortController;
|
||||||
window.__bootstrap.abortSignal = {
|
window.__bootstrap.abortSignal = {
|
||||||
add,
|
add,
|
||||||
signalAbort,
|
signalAbort,
|
||||||
remove,
|
remove,
|
||||||
|
follow,
|
||||||
|
newSignal,
|
||||||
};
|
};
|
||||||
})(this);
|
})(this);
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
headersList,
|
headersList,
|
||||||
body !== null ? new InnerBody(body) : null,
|
body !== null ? new InnerBody(body) : null,
|
||||||
);
|
);
|
||||||
const request = fromInnerRequest(innerRequest, "immutable");
|
const request = fromInnerRequest(innerRequest, null, "immutable");
|
||||||
|
|
||||||
const respondWith = createRespondWith(this, responseSenderRid);
|
const respondWith = createRespondWith(this, responseSenderRid);
|
||||||
|
|
||||||
|
|
|
@ -1052,7 +1052,6 @@
|
||||||
"Request interface: attribute keepalive",
|
"Request interface: attribute keepalive",
|
||||||
"Request interface: attribute isReloadNavigation",
|
"Request interface: attribute isReloadNavigation",
|
||||||
"Request interface: attribute isHistoryNavigation",
|
"Request interface: attribute isHistoryNavigation",
|
||||||
"Request interface: attribute signal",
|
|
||||||
"Request interface: attribute body",
|
"Request interface: attribute body",
|
||||||
"Request interface: attribute bodyUsed",
|
"Request interface: attribute bodyUsed",
|
||||||
"Request interface: new Request('about:blank') must inherit property \"destination\" with the proper type",
|
"Request interface: new Request('about:blank') must inherit property \"destination\" with the proper type",
|
||||||
|
@ -1065,14 +1064,22 @@
|
||||||
"Request interface: new Request('about:blank') must inherit property \"keepalive\" with the proper type",
|
"Request interface: new Request('about:blank') must inherit property \"keepalive\" with the proper type",
|
||||||
"Request interface: new Request('about:blank') must inherit property \"isReloadNavigation\" with the proper type",
|
"Request interface: new Request('about:blank') must inherit property \"isReloadNavigation\" with the proper type",
|
||||||
"Request interface: new Request('about:blank') must inherit property \"isHistoryNavigation\" with the proper type",
|
"Request interface: new Request('about:blank') must inherit property \"isHistoryNavigation\" with the proper type",
|
||||||
"Request interface: new Request('about:blank') must inherit property \"signal\" with the proper type",
|
|
||||||
"Response interface: operation error()",
|
"Response interface: operation error()",
|
||||||
"Response interface: operation redirect(USVString, optional unsigned short)",
|
"Response interface: operation redirect(USVString, optional unsigned short)",
|
||||||
"Response interface: attribute body",
|
"Response interface: attribute body",
|
||||||
"Response interface: attribute bodyUsed",
|
"Response interface: attribute bodyUsed",
|
||||||
"Response interface: calling redirect(USVString, optional unsigned short) on new Response() with too few arguments must throw TypeError",
|
"Response interface: calling redirect(USVString, optional unsigned short) on new Response() with too few arguments must throw TypeError",
|
||||||
"Window interface: operation fetch(RequestInfo, optional RequestInit)"
|
"Window interface: operation fetch(RequestInfo, optional RequestInit)"
|
||||||
|
],
|
||||||
|
"abort": {
|
||||||
|
"general.any.html": [
|
||||||
|
"response.arrayBuffer() rejects if already aborted",
|
||||||
|
"response.blob() rejects if already aborted",
|
||||||
|
"response.formData() rejects if already aborted",
|
||||||
|
"response.json() rejects if already aborted",
|
||||||
|
"response.text() rejects if already aborted"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"data-urls": {
|
"data-urls": {
|
||||||
"base64.any.html": true,
|
"base64.any.html": true,
|
||||||
|
|
Loading…
Reference in a new issue