mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
refactor: use resourceForReadableStream for fetch (#20217)
Switch `ext/fetch` over to `resourceForReadableStream` to simplify and unify implementation with `ext/serve`. This allows us to work in Rust with resources only. Two additional changes made to `resourceForReadableStream` were required: - Add an optional length to `resourceForReadableStream` which translates to `size_hint` - Fix a bug where writing to a closed stream that was full would panic
This commit is contained in:
parent
687ae870d1
commit
e6e708e46c
12 changed files with 311 additions and 358 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1187,6 +1187,7 @@ dependencies = [
|
||||||
"deno_tls",
|
"deno_tls",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"http",
|
"http",
|
||||||
|
"pin-project",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
assert,
|
assert,
|
||||||
assertEquals,
|
assertEquals,
|
||||||
assertRejects,
|
assertRejects,
|
||||||
|
assertThrows,
|
||||||
delay,
|
delay,
|
||||||
fail,
|
fail,
|
||||||
unimplemented,
|
unimplemented,
|
||||||
|
@ -523,7 +524,7 @@ Deno.test(
|
||||||
);
|
);
|
||||||
|
|
||||||
Deno.test({ permissions: { net: true } }, async function fetchInitBlobBody() {
|
Deno.test({ permissions: { net: true } }, async function fetchInitBlobBody() {
|
||||||
const data = "const a = 1";
|
const data = "const a = 1 🦕";
|
||||||
const blob = new Blob([data], {
|
const blob = new Blob([data], {
|
||||||
type: "text/javascript",
|
type: "text/javascript",
|
||||||
});
|
});
|
||||||
|
@ -555,7 +556,11 @@ Deno.test(
|
||||||
async function fetchInitFormDataBlobFilenameBody() {
|
async function fetchInitFormDataBlobFilenameBody() {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("field", "value");
|
form.append("field", "value");
|
||||||
form.append("file", new Blob([new TextEncoder().encode("deno")]));
|
form.append(
|
||||||
|
"file",
|
||||||
|
new Blob([new TextEncoder().encode("deno")]),
|
||||||
|
"file name",
|
||||||
|
);
|
||||||
const response = await fetch("http://localhost:4545/echo_server", {
|
const response = await fetch("http://localhost:4545/echo_server", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: form,
|
body: form,
|
||||||
|
@ -564,7 +569,28 @@ Deno.test(
|
||||||
assertEquals(form.get("field"), resultForm.get("field"));
|
assertEquals(form.get("field"), resultForm.get("field"));
|
||||||
const file = resultForm.get("file");
|
const file = resultForm.get("file");
|
||||||
assert(file instanceof File);
|
assert(file instanceof File);
|
||||||
assertEquals(file.name, "blob");
|
assertEquals(file.name, "file name");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { net: true } },
|
||||||
|
async function fetchInitFormDataFileFilenameBody() {
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("field", "value");
|
||||||
|
form.append(
|
||||||
|
"file",
|
||||||
|
new File([new Blob([new TextEncoder().encode("deno")])], "file name"),
|
||||||
|
);
|
||||||
|
const response = await fetch("http://localhost:4545/echo_server", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
const resultForm = await response.formData();
|
||||||
|
assertEquals(form.get("field"), resultForm.get("field"));
|
||||||
|
const file = resultForm.get("file");
|
||||||
|
assert(file instanceof File);
|
||||||
|
assertEquals(file.name, "file name");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1193,10 +1219,8 @@ Deno.test(
|
||||||
"accept-encoding: gzip, br\r\n",
|
"accept-encoding: gzip, br\r\n",
|
||||||
`host: ${addr}\r\n`,
|
`host: ${addr}\r\n`,
|
||||||
`transfer-encoding: chunked\r\n\r\n`,
|
`transfer-encoding: chunked\r\n\r\n`,
|
||||||
"6\r\n",
|
"B\r\n",
|
||||||
"hello \r\n",
|
"hello world\r\n",
|
||||||
"5\r\n",
|
|
||||||
"world\r\n",
|
|
||||||
"0\r\n\r\n",
|
"0\r\n\r\n",
|
||||||
].join("");
|
].join("");
|
||||||
assertEquals(actual, expected);
|
assertEquals(actual, expected);
|
||||||
|
@ -1259,13 +1283,19 @@ Deno.test(
|
||||||
Deno.test(
|
Deno.test(
|
||||||
{ permissions: { net: true } },
|
{ permissions: { net: true } },
|
||||||
async function fetchNoServerReadableStreamBody() {
|
async function fetchNoServerReadableStreamBody() {
|
||||||
const { promise, resolve } = Promise.withResolvers<void>();
|
const completed = Promise.withResolvers<void>();
|
||||||
|
const failed = Promise.withResolvers<void>();
|
||||||
const body = new ReadableStream({
|
const body = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
controller.enqueue(new Uint8Array([1]));
|
controller.enqueue(new Uint8Array([1]));
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
controller.enqueue(new Uint8Array([2]));
|
// This is technically a race. If the fetch has failed by this point, the enqueue will
|
||||||
resolve();
|
// throw. If not, it will succeed. Windows appears to take a while to time out the fetch,
|
||||||
|
// so we will just wait for that here before we attempt to enqueue so it's consistent
|
||||||
|
// across platforms.
|
||||||
|
await failed.promise;
|
||||||
|
assertThrows(() => controller.enqueue(new Uint8Array([2])));
|
||||||
|
completed.resolve();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1273,7 +1303,8 @@ Deno.test(
|
||||||
await assertRejects(async () => {
|
await assertRejects(async () => {
|
||||||
await fetch(nonExistentHostname, { body, method: "POST" });
|
await fetch(nonExistentHostname, { body, method: "POST" });
|
||||||
}, TypeError);
|
}, TypeError);
|
||||||
await promise;
|
failed.resolve();
|
||||||
|
await completed.promise;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1853,8 +1884,9 @@ Deno.test(
|
||||||
async function fetchBlobUrl(): Promise<void> {
|
async function fetchBlobUrl(): Promise<void> {
|
||||||
const blob = new Blob(["ok"], { type: "text/plain" });
|
const blob = new Blob(["ok"], { type: "text/plain" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
assert(url.startsWith("blob:"), `URL was ${url}`);
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
assert(res.url.startsWith("blob:http://js-unit-tests/"));
|
assertEquals(res.url, url);
|
||||||
assertEquals(res.status, 200);
|
assertEquals(res.status, 200);
|
||||||
assertEquals(res.headers.get("content-length"), "2");
|
assertEquals(res.headers.get("content-length"), "2");
|
||||||
assertEquals(res.headers.get("content-type"), "text/plain");
|
assertEquals(res.headers.get("content-type"), "text/plain");
|
||||||
|
@ -1941,9 +1973,12 @@ Deno.test(
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(err instanceof TypeError);
|
assert(err instanceof TypeError, `err was not a TypeError ${err}`);
|
||||||
assert(err.cause);
|
assert(err.cause, `err.cause was null ${err}`);
|
||||||
assert(err.cause instanceof Error);
|
assert(
|
||||||
|
err.cause instanceof Error,
|
||||||
|
`err.cause was not an Error ${err.cause}`,
|
||||||
|
);
|
||||||
assertEquals(err.cause.message, "foo");
|
assertEquals(err.cause.message, "foo");
|
||||||
|
|
||||||
await server;
|
await server;
|
||||||
|
@ -1968,7 +2003,12 @@ Deno.test(
|
||||||
method: "POST",
|
method: "POST",
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
fail("abort should not throw");
|
||||||
|
}
|
||||||
await promise;
|
await promise;
|
||||||
},
|
},
|
||||||
DOMException,
|
DOMException,
|
||||||
|
|
|
@ -190,44 +190,46 @@ Deno.test(async function readableStream() {
|
||||||
|
|
||||||
// Close the stream after reading everything
|
// Close the stream after reading everything
|
||||||
Deno.test(async function readableStreamClose() {
|
Deno.test(async function readableStreamClose() {
|
||||||
const { promise: cancelPromise, resolve: cancelResolve } = Promise
|
const cancel = Promise.withResolvers();
|
||||||
.withResolvers();
|
const rid = resourceForReadableStream(
|
||||||
const rid = resourceForReadableStream(helloWorldStream(false, cancelResolve));
|
helloWorldStream(false, cancel.resolve),
|
||||||
|
);
|
||||||
const buffer = new Uint8Array(1024);
|
const buffer = new Uint8Array(1024);
|
||||||
const nread = await core.ops.op_read(rid, buffer);
|
const nread = await core.ops.op_read(rid, buffer);
|
||||||
assertEquals(nread, 12);
|
assertEquals(nread, 12);
|
||||||
core.ops.op_close(rid);
|
core.ops.op_close(rid);
|
||||||
assertEquals(await cancelPromise, "resource closed");
|
assertEquals(await cancel.promise, "resource closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close the stream without reading everything
|
// Close the stream without reading everything
|
||||||
Deno.test(async function readableStreamClosePartialRead() {
|
Deno.test(async function readableStreamClosePartialRead() {
|
||||||
const { promise: cancelPromise, resolve: cancelResolve } = Promise
|
const cancel = Promise.withResolvers();
|
||||||
.withResolvers();
|
const rid = resourceForReadableStream(
|
||||||
const rid = resourceForReadableStream(helloWorldStream(false, cancelResolve));
|
helloWorldStream(false, cancel.resolve),
|
||||||
|
);
|
||||||
const buffer = new Uint8Array(5);
|
const buffer = new Uint8Array(5);
|
||||||
const nread = await core.ops.op_read(rid, buffer);
|
const nread = await core.ops.op_read(rid, buffer);
|
||||||
assertEquals(nread, 5);
|
assertEquals(nread, 5);
|
||||||
core.ops.op_close(rid);
|
core.ops.op_close(rid);
|
||||||
assertEquals(await cancelPromise, "resource closed");
|
assertEquals(await cancel.promise, "resource closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close the stream without reading anything
|
// Close the stream without reading anything
|
||||||
Deno.test(async function readableStreamCloseWithoutRead() {
|
Deno.test(async function readableStreamCloseWithoutRead() {
|
||||||
const { promise: cancelPromise, resolve: cancelResolve } = Promise
|
const cancel = Promise.withResolvers();
|
||||||
.withResolvers();
|
const rid = resourceForReadableStream(
|
||||||
const rid = resourceForReadableStream(helloWorldStream(false, cancelResolve));
|
helloWorldStream(false, cancel.resolve),
|
||||||
|
);
|
||||||
core.ops.op_close(rid);
|
core.ops.op_close(rid);
|
||||||
assertEquals(await cancelPromise, "resource closed");
|
assertEquals(await cancel.promise, "resource closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close the stream without reading anything
|
// Close the stream without reading anything
|
||||||
Deno.test(async function readableStreamCloseWithoutRead2() {
|
Deno.test(async function readableStreamCloseWithoutRead2() {
|
||||||
const { promise: cancelPromise, resolve: cancelResolve } = Promise
|
const cancel = Promise.withResolvers();
|
||||||
.withResolvers();
|
const rid = resourceForReadableStream(longAsyncStream(cancel.resolve));
|
||||||
const rid = resourceForReadableStream(longAsyncStream(cancelResolve));
|
|
||||||
core.ops.op_close(rid);
|
core.ops.op_close(rid);
|
||||||
assertEquals(await cancelPromise, "resource closed");
|
assertEquals(await cancel.promise, "resource closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test(async function readableStreamPartial() {
|
Deno.test(async function readableStreamPartial() {
|
||||||
|
@ -439,16 +441,19 @@ function createStreamTest(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test(async function readableStreamWithAggressiveResourceClose() {
|
// 1024 is the size of the internal packet buffer -- we want to make sure we fill the internal pipe fully.
|
||||||
|
for (const packetCount of [1, 1024]) {
|
||||||
|
Deno.test(`readableStreamWithAggressiveResourceClose_${packetCount}`, async function () {
|
||||||
let first = true;
|
let first = true;
|
||||||
const { promise: reasonPromise, resolve: reasonResolve } = Promise
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
.withResolvers();
|
|
||||||
const rid = resourceForReadableStream(
|
const rid = resourceForReadableStream(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
pull(controller) {
|
pull(controller) {
|
||||||
if (first) {
|
if (first) {
|
||||||
// We queue this up and then immediately close the resource (not the reader)
|
// We queue this up and then immediately close the resource (not the reader)
|
||||||
|
for (let i = 0; i < packetCount; i++) {
|
||||||
controller.enqueue(new Uint8Array(1));
|
controller.enqueue(new Uint8Array(1));
|
||||||
|
}
|
||||||
core.close(rid);
|
core.close(rid);
|
||||||
// This doesn't throw, even though the resource is closed
|
// This doesn't throw, even though the resource is closed
|
||||||
controller.enqueue(new Uint8Array(1));
|
controller.enqueue(new Uint8Array(1));
|
||||||
|
@ -456,15 +461,18 @@ Deno.test(async function readableStreamWithAggressiveResourceClose() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel(reason) {
|
cancel(reason) {
|
||||||
reasonResolve(reason);
|
resolve(reason);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
for (let i = 0; i < packetCount; i++) {
|
||||||
await core.ops.op_read(rid, new Uint8Array(1));
|
await core.ops.op_read(rid, new Uint8Array(1));
|
||||||
|
}
|
||||||
fail();
|
fail();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
assertEquals(e.message, "operation canceled");
|
assertEquals(e.message, "operation canceled");
|
||||||
}
|
}
|
||||||
assertEquals(await reasonPromise, "resource closed");
|
assertEquals(await promise, "resource closed");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -14,11 +14,12 @@ const core = globalThis.Deno.core;
|
||||||
const ops = core.ops;
|
const ops = core.ops;
|
||||||
import * as webidl from "ext:deno_webidl/00_webidl.js";
|
import * as webidl from "ext:deno_webidl/00_webidl.js";
|
||||||
import { byteLowerCase } from "ext:deno_web/00_infra.js";
|
import { byteLowerCase } from "ext:deno_web/00_infra.js";
|
||||||
import { BlobPrototype } from "ext:deno_web/09_file.js";
|
|
||||||
import {
|
import {
|
||||||
errorReadableStream,
|
errorReadableStream,
|
||||||
|
getReadableStreamResourceBacking,
|
||||||
readableStreamForRid,
|
readableStreamForRid,
|
||||||
ReadableStreamPrototype,
|
ReadableStreamPrototype,
|
||||||
|
resourceForReadableStream,
|
||||||
} from "ext:deno_web/06_streams.js";
|
} from "ext:deno_web/06_streams.js";
|
||||||
import { extractBody, InnerBody } from "ext:deno_fetch/22_body.js";
|
import { extractBody, InnerBody } from "ext:deno_fetch/22_body.js";
|
||||||
import { processUrlList, toInnerRequest } from "ext:deno_fetch/23_request.js";
|
import { processUrlList, toInnerRequest } from "ext:deno_fetch/23_request.js";
|
||||||
|
@ -37,22 +38,17 @@ const {
|
||||||
ArrayPrototypeSplice,
|
ArrayPrototypeSplice,
|
||||||
ArrayPrototypeFilter,
|
ArrayPrototypeFilter,
|
||||||
ArrayPrototypeIncludes,
|
ArrayPrototypeIncludes,
|
||||||
|
Error,
|
||||||
ObjectPrototypeIsPrototypeOf,
|
ObjectPrototypeIsPrototypeOf,
|
||||||
Promise,
|
Promise,
|
||||||
PromisePrototypeThen,
|
PromisePrototypeThen,
|
||||||
PromisePrototypeCatch,
|
PromisePrototypeCatch,
|
||||||
SafeArrayIterator,
|
SafeArrayIterator,
|
||||||
SafeWeakMap,
|
|
||||||
String,
|
String,
|
||||||
StringPrototypeStartsWith,
|
StringPrototypeStartsWith,
|
||||||
StringPrototypeToLowerCase,
|
StringPrototypeToLowerCase,
|
||||||
TypeError,
|
TypeError,
|
||||||
Uint8Array,
|
|
||||||
Uint8ArrayPrototype,
|
Uint8ArrayPrototype,
|
||||||
WeakMapPrototypeDelete,
|
|
||||||
WeakMapPrototypeGet,
|
|
||||||
WeakMapPrototypeHas,
|
|
||||||
WeakMapPrototypeSet,
|
|
||||||
} = primordials;
|
} = primordials;
|
||||||
|
|
||||||
const REQUEST_BODY_HEADER_NAMES = [
|
const REQUEST_BODY_HEADER_NAMES = [
|
||||||
|
@ -62,28 +58,9 @@ const REQUEST_BODY_HEADER_NAMES = [
|
||||||
"content-type",
|
"content-type",
|
||||||
];
|
];
|
||||||
|
|
||||||
const requestBodyReaders = new SafeWeakMap();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {{ method: string, url: string, headers: [string, string][], clientRid: number | null, hasBody: boolean }} args
|
|
||||||
* @param {Uint8Array | null} body
|
|
||||||
* @returns {{ requestRid: number, requestBodyRid: number | null, cancelHandleRid: number | null }}
|
|
||||||
*/
|
|
||||||
function opFetch(method, url, headers, clientRid, hasBody, bodyLength, body) {
|
|
||||||
return ops.op_fetch(
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
headers,
|
|
||||||
clientRid,
|
|
||||||
hasBody,
|
|
||||||
bodyLength,
|
|
||||||
body,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} rid
|
* @param {number} rid
|
||||||
* @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number }>}
|
* @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number, error: string? }>}
|
||||||
*/
|
*/
|
||||||
function opFetchSend(rid) {
|
function opFetchSend(rid) {
|
||||||
return core.opAsync("op_fetch_send", rid);
|
return core.opAsync("op_fetch_send", rid);
|
||||||
|
@ -145,154 +122,59 @@ async function mainFetch(req, recursive, terminator) {
|
||||||
|
|
||||||
/** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
|
/** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
|
||||||
let reqBody = null;
|
let reqBody = null;
|
||||||
|
let reqRid = null;
|
||||||
|
|
||||||
if (req.body !== null) {
|
if (req.body) {
|
||||||
if (
|
const stream = req.body.streamOrStatic;
|
||||||
ObjectPrototypeIsPrototypeOf(
|
const body = stream.body;
|
||||||
ReadableStreamPrototype,
|
|
||||||
req.body.streamOrStatic,
|
if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, body)) {
|
||||||
)
|
reqBody = body;
|
||||||
) {
|
} else if (typeof body === "string") {
|
||||||
if (
|
reqBody = core.encode(body);
|
||||||
req.body.length === null ||
|
} else if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, stream)) {
|
||||||
ObjectPrototypeIsPrototypeOf(BlobPrototype, req.body.source)
|
const resourceBacking = getReadableStreamResourceBacking(stream);
|
||||||
) {
|
if (resourceBacking) {
|
||||||
reqBody = req.body.stream;
|
reqRid = resourceBacking.rid;
|
||||||
} else {
|
} else {
|
||||||
const reader = req.body.stream.getReader();
|
reqRid = resourceForReadableStream(stream, req.body.length);
|
||||||
WeakMapPrototypeSet(requestBodyReaders, req, reader);
|
|
||||||
const r1 = await reader.read();
|
|
||||||
if (r1.done) {
|
|
||||||
reqBody = new Uint8Array(0);
|
|
||||||
} else {
|
|
||||||
reqBody = r1.value;
|
|
||||||
const r2 = await reader.read();
|
|
||||||
if (!r2.done) throw new TypeError("Unreachable");
|
|
||||||
}
|
|
||||||
WeakMapPrototypeDelete(requestBodyReaders, req);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
req.body.streamOrStatic.consumed = true;
|
throw TypeError("invalid body");
|
||||||
reqBody = req.body.streamOrStatic.body;
|
|
||||||
// TODO(@AaronO): plumb support for StringOrBuffer all the way
|
|
||||||
reqBody = typeof reqBody === "string" ? core.encode(reqBody) : reqBody;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { requestRid, requestBodyRid, cancelHandleRid } = opFetch(
|
const { requestRid, cancelHandleRid } = ops.op_fetch(
|
||||||
req.method,
|
req.method,
|
||||||
req.currentUrl(),
|
req.currentUrl(),
|
||||||
req.headerList,
|
req.headerList,
|
||||||
req.clientRid,
|
req.clientRid,
|
||||||
reqBody !== null,
|
reqBody !== null || reqRid !== null,
|
||||||
req.body?.length,
|
reqBody,
|
||||||
ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, reqBody) ? reqBody : null,
|
reqRid,
|
||||||
);
|
);
|
||||||
|
|
||||||
function onAbort() {
|
function onAbort() {
|
||||||
if (cancelHandleRid !== null) {
|
if (cancelHandleRid !== null) {
|
||||||
core.tryClose(cancelHandleRid);
|
core.tryClose(cancelHandleRid);
|
||||||
}
|
}
|
||||||
if (requestBodyRid !== null) {
|
|
||||||
core.tryClose(requestBodyRid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
terminator[abortSignal.add](onAbort);
|
terminator[abortSignal.add](onAbort);
|
||||||
|
|
||||||
let requestSendError;
|
|
||||||
let requestSendErrorSet = false;
|
|
||||||
|
|
||||||
async function propagateError(err, message) {
|
|
||||||
// TODO(lucacasonato): propagate error into response body stream
|
|
||||||
try {
|
|
||||||
await core.writeTypeError(requestBodyRid, message);
|
|
||||||
} catch (err) {
|
|
||||||
if (!requestSendErrorSet) {
|
|
||||||
requestSendErrorSet = true;
|
|
||||||
requestSendError = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!requestSendErrorSet) {
|
|
||||||
requestSendErrorSet = true;
|
|
||||||
requestSendError = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestBodyRid !== null) {
|
|
||||||
if (
|
|
||||||
reqBody === null ||
|
|
||||||
!ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, reqBody)
|
|
||||||
) {
|
|
||||||
throw new TypeError("Unreachable");
|
|
||||||
}
|
|
||||||
const reader = reqBody.getReader();
|
|
||||||
WeakMapPrototypeSet(requestBodyReaders, req, reader);
|
|
||||||
(async () => {
|
|
||||||
let done = false;
|
|
||||||
while (!done) {
|
|
||||||
let val;
|
|
||||||
try {
|
|
||||||
const res = await reader.read();
|
|
||||||
done = res.done;
|
|
||||||
val = res.value;
|
|
||||||
} catch (err) {
|
|
||||||
if (terminator.aborted) break;
|
|
||||||
await propagateError(err, "failed to read");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (done) break;
|
|
||||||
if (!ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, val)) {
|
|
||||||
const error = new TypeError(
|
|
||||||
"Item in request body ReadableStream is not a Uint8Array",
|
|
||||||
);
|
|
||||||
await reader.cancel(error);
|
|
||||||
await propagateError(error, error.message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await core.writeAll(requestBodyRid, val);
|
|
||||||
} catch (err) {
|
|
||||||
if (terminator.aborted) break;
|
|
||||||
await reader.cancel(err);
|
|
||||||
await propagateError(err, "failed to write");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (done && !terminator.aborted) {
|
|
||||||
try {
|
|
||||||
await core.shutdown(requestBodyRid);
|
|
||||||
} catch (err) {
|
|
||||||
if (!terminator.aborted) {
|
|
||||||
await propagateError(err, "failed to flush");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WeakMapPrototypeDelete(requestBodyReaders, req);
|
|
||||||
reader.releaseLock();
|
|
||||||
core.tryClose(requestBodyRid);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
let resp;
|
let resp;
|
||||||
try {
|
try {
|
||||||
resp = await opFetchSend(requestRid);
|
resp = await opFetchSend(requestRid);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (terminator.aborted) return;
|
if (terminator.aborted) return;
|
||||||
if (requestSendErrorSet) {
|
|
||||||
// if the request body stream errored, we want to propagate that error
|
|
||||||
// instead of the original error from opFetchSend
|
|
||||||
throw new TypeError("Failed to fetch: request body stream errored", {
|
|
||||||
cause: requestSendError,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (requestBodyRid !== null) {
|
|
||||||
core.tryClose(requestBodyRid);
|
|
||||||
}
|
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
if (cancelHandleRid !== null) {
|
if (cancelHandleRid !== null) {
|
||||||
core.tryClose(cancelHandleRid);
|
core.tryClose(cancelHandleRid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Re-throw any body errors
|
||||||
|
if (resp.error) {
|
||||||
|
throw new TypeError("body failed", { cause: new Error(resp.error) });
|
||||||
|
}
|
||||||
if (terminator.aborted) return abortedNetworkError();
|
if (terminator.aborted) return abortedNetworkError();
|
||||||
|
|
||||||
processUrlList(req.urlList, req.urlListProcessed);
|
processUrlList(req.urlList, req.urlListProcessed);
|
||||||
|
@ -510,9 +392,8 @@ function fetch(input, init = {}) {
|
||||||
|
|
||||||
function abortFetch(request, responseObject, error) {
|
function abortFetch(request, responseObject, error) {
|
||||||
if (request.body !== null) {
|
if (request.body !== null) {
|
||||||
if (WeakMapPrototypeHas(requestBodyReaders, request)) {
|
// Cancel the body if we haven't taken it as a resource yet
|
||||||
WeakMapPrototypeGet(requestBodyReaders, request).cancel(error);
|
if (!request.body.streamOrStatic.locked) {
|
||||||
} else {
|
|
||||||
request.body.cancel(error);
|
request.body.cancel(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ deno_core.workspace = true
|
||||||
deno_tls.workspace = true
|
deno_tls.workspace = true
|
||||||
dyn-clone = "1"
|
dyn-clone = "1"
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
pin-project.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
212
ext/fetch/lib.rs
212
ext/fetch/lib.rs
|
@ -11,6 +11,8 @@ use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::task::Context;
|
||||||
|
use std::task::Poll;
|
||||||
|
|
||||||
use deno_core::anyhow::Error;
|
use deno_core::anyhow::Error;
|
||||||
use deno_core::error::type_error;
|
use deno_core::error::type_error;
|
||||||
|
@ -21,13 +23,11 @@ use deno_core::futures::FutureExt;
|
||||||
use deno_core::futures::Stream;
|
use deno_core::futures::Stream;
|
||||||
use deno_core::futures::StreamExt;
|
use deno_core::futures::StreamExt;
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use deno_core::BufView;
|
|
||||||
use deno_core::WriteOutcome;
|
|
||||||
|
|
||||||
use deno_core::unsync::spawn;
|
use deno_core::unsync::spawn;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
use deno_core::AsyncRefCell;
|
use deno_core::AsyncRefCell;
|
||||||
use deno_core::AsyncResult;
|
use deno_core::AsyncResult;
|
||||||
|
use deno_core::BufView;
|
||||||
use deno_core::ByteString;
|
use deno_core::ByteString;
|
||||||
use deno_core::CancelFuture;
|
use deno_core::CancelFuture;
|
||||||
use deno_core::CancelHandle;
|
use deno_core::CancelHandle;
|
||||||
|
@ -62,7 +62,6 @@ use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::sync::mpsc;
|
|
||||||
|
|
||||||
// Re-export reqwest and data_url
|
// Re-export reqwest and data_url
|
||||||
pub use data_url;
|
pub use data_url;
|
||||||
|
@ -184,7 +183,6 @@ pub fn get_declaration() -> PathBuf {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FetchReturn {
|
pub struct FetchReturn {
|
||||||
pub request_rid: ResourceId,
|
pub request_rid: ResourceId,
|
||||||
pub request_body_rid: Option<ResourceId>,
|
|
||||||
pub cancel_handle_rid: Option<ResourceId>,
|
pub cancel_handle_rid: Option<ResourceId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,6 +214,59 @@ pub fn get_or_create_client_from_state(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub struct ResourceToBodyAdapter(
|
||||||
|
Rc<dyn Resource>,
|
||||||
|
Option<Pin<Box<dyn Future<Output = Result<BufView, Error>>>>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl ResourceToBodyAdapter {
|
||||||
|
pub fn new(resource: Rc<dyn Resource>) -> Self {
|
||||||
|
let future = resource.clone().read(64 * 1024);
|
||||||
|
Self(resource, Some(future))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: we only use this on a single-threaded executor
|
||||||
|
unsafe impl Send for ResourceToBodyAdapter {}
|
||||||
|
// SAFETY: we only use this on a single-threaded executor
|
||||||
|
unsafe impl Sync for ResourceToBodyAdapter {}
|
||||||
|
|
||||||
|
impl Stream for ResourceToBodyAdapter {
|
||||||
|
type Item = Result<BufView, Error>;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<Self::Item>> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
if let Some(mut fut) = this.1.take() {
|
||||||
|
match fut.poll_unpin(cx) {
|
||||||
|
Poll::Pending => {
|
||||||
|
this.1 = Some(fut);
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
Poll::Ready(res) => match res {
|
||||||
|
Ok(buf) if buf.is_empty() => Poll::Ready(None),
|
||||||
|
Ok(_) => {
|
||||||
|
this.1 = Some(this.0.clone().read(64 * 1024));
|
||||||
|
Poll::Ready(Some(res))
|
||||||
|
}
|
||||||
|
_ => Poll::Ready(Some(res)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Poll::Ready(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ResourceToBodyAdapter {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.clone().close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
#[serde]
|
#[serde]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -226,8 +277,8 @@ pub fn op_fetch<FP>(
|
||||||
#[serde] headers: Vec<(ByteString, ByteString)>,
|
#[serde] headers: Vec<(ByteString, ByteString)>,
|
||||||
#[smi] client_rid: Option<u32>,
|
#[smi] client_rid: Option<u32>,
|
||||||
has_body: bool,
|
has_body: bool,
|
||||||
#[number] body_length: Option<u64>,
|
|
||||||
#[buffer] data: Option<JsBuffer>,
|
#[buffer] data: Option<JsBuffer>,
|
||||||
|
#[smi] resource: Option<ResourceId>,
|
||||||
) -> Result<FetchReturn, AnyError>
|
) -> Result<FetchReturn, AnyError>
|
||||||
where
|
where
|
||||||
FP: FetchPermissions + 'static,
|
FP: FetchPermissions + 'static,
|
||||||
|
@ -244,7 +295,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, cancel_handle_rid) = match scheme {
|
let (request_rid, cancel_handle_rid) = match scheme {
|
||||||
"file" => {
|
"file" => {
|
||||||
let path = url.to_file_path().map_err(|_| {
|
let path = url.to_file_path().map_err(|_| {
|
||||||
type_error("NetworkError when attempting to fetch resource.")
|
type_error("NetworkError when attempting to fetch resource.")
|
||||||
|
@ -268,7 +319,7 @@ where
|
||||||
let maybe_cancel_handle_rid = maybe_cancel_handle
|
let maybe_cancel_handle_rid = maybe_cancel_handle
|
||||||
.map(|ch| state.resource_table.add(FetchCancelHandle(ch)));
|
.map(|ch| state.resource_table.add(FetchCancelHandle(ch)));
|
||||||
|
|
||||||
(request_rid, None, maybe_cancel_handle_rid)
|
(request_rid, maybe_cancel_handle_rid)
|
||||||
}
|
}
|
||||||
"http" | "https" => {
|
"http" | "https" => {
|
||||||
let permissions = state.borrow_mut::<FP>();
|
let permissions = state.borrow_mut::<FP>();
|
||||||
|
@ -282,34 +333,25 @@ where
|
||||||
|
|
||||||
let mut request = client.request(method.clone(), url);
|
let mut request = client.request(method.clone(), url);
|
||||||
|
|
||||||
let request_body_rid = if has_body {
|
if has_body {
|
||||||
match data {
|
match (data, resource) {
|
||||||
None => {
|
(Some(data), _) => {
|
||||||
// If no body is passed, we return a writer for streaming the body.
|
|
||||||
let (tx, stream) = tokio::sync::mpsc::channel(1);
|
|
||||||
|
|
||||||
// If the size of the body is known, we include a content-length
|
|
||||||
// header explicitly.
|
|
||||||
if let Some(body_size) = body_length {
|
|
||||||
request =
|
|
||||||
request.header(CONTENT_LENGTH, HeaderValue::from(body_size))
|
|
||||||
}
|
|
||||||
|
|
||||||
request = request.body(Body::wrap_stream(FetchBodyStream(stream)));
|
|
||||||
|
|
||||||
let request_body_rid =
|
|
||||||
state.resource_table.add(FetchRequestBodyResource {
|
|
||||||
body: AsyncRefCell::new(Some(tx)),
|
|
||||||
cancel: CancelHandle::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
Some(request_body_rid)
|
|
||||||
}
|
|
||||||
Some(data) => {
|
|
||||||
// If a body is passed, we use it, and don't return a body for streaming.
|
// If a body is passed, we use it, and don't return a body for streaming.
|
||||||
request = request.body(data.to_vec());
|
request = request.body(data.to_vec());
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
(_, Some(resource)) => {
|
||||||
|
let resource = state.resource_table.take_any(resource)?;
|
||||||
|
match resource.size_hint() {
|
||||||
|
(body_size, Some(n)) if body_size == n && body_size > 0 => {
|
||||||
|
request =
|
||||||
|
request.header(CONTENT_LENGTH, HeaderValue::from(body_size));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
request = request
|
||||||
|
.body(Body::wrap_stream(ResourceToBodyAdapter::new(resource)))
|
||||||
|
}
|
||||||
|
(None, None) => unreachable!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// POST and PUT requests should always have a 0 length content-length,
|
// POST and PUT requests should always have a 0 length content-length,
|
||||||
|
@ -317,7 +359,6 @@ where
|
||||||
if matches!(method, Method::POST | Method::PUT) {
|
if matches!(method, Method::POST | Method::PUT) {
|
||||||
request = request.header(CONTENT_LENGTH, HeaderValue::from(0));
|
request = request.header(CONTENT_LENGTH, HeaderValue::from(0));
|
||||||
}
|
}
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut header_map = HeaderMap::new();
|
let mut header_map = HeaderMap::new();
|
||||||
|
@ -354,7 +395,7 @@ where
|
||||||
.send()
|
.send()
|
||||||
.or_cancel(cancel_handle_)
|
.or_cancel(cancel_handle_)
|
||||||
.await
|
.await
|
||||||
.map(|res| res.map_err(|err| type_error(err.to_string())))
|
.map(|res| res.map_err(|err| err.into()))
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_rid = state
|
let request_rid = state
|
||||||
|
@ -364,7 +405,7 @@ where
|
||||||
let cancel_handle_rid =
|
let cancel_handle_rid =
|
||||||
state.resource_table.add(FetchCancelHandle(cancel_handle));
|
state.resource_table.add(FetchCancelHandle(cancel_handle));
|
||||||
|
|
||||||
(request_rid, request_body_rid, Some(cancel_handle_rid))
|
(request_rid, Some(cancel_handle_rid))
|
||||||
}
|
}
|
||||||
"data" => {
|
"data" => {
|
||||||
let data_url = DataUrl::process(url.as_str())
|
let data_url = DataUrl::process(url.as_str())
|
||||||
|
@ -385,7 +426,7 @@ where
|
||||||
.resource_table
|
.resource_table
|
||||||
.add(FetchRequestResource(Box::pin(fut)));
|
.add(FetchRequestResource(Box::pin(fut)));
|
||||||
|
|
||||||
(request_rid, None, None)
|
(request_rid, None)
|
||||||
}
|
}
|
||||||
"blob" => {
|
"blob" => {
|
||||||
// Blob URL resolution happens in the JS side of fetch. If we got here is
|
// Blob URL resolution happens in the JS side of fetch. If we got here is
|
||||||
|
@ -397,12 +438,11 @@ where
|
||||||
|
|
||||||
Ok(FetchReturn {
|
Ok(FetchReturn {
|
||||||
request_rid,
|
request_rid,
|
||||||
request_body_rid,
|
|
||||||
cancel_handle_rid,
|
cancel_handle_rid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FetchResponse {
|
pub struct FetchResponse {
|
||||||
pub status: u16,
|
pub status: u16,
|
||||||
|
@ -413,6 +453,7 @@ pub struct FetchResponse {
|
||||||
pub content_length: Option<u64>,
|
pub content_length: Option<u64>,
|
||||||
pub remote_addr_ip: Option<String>,
|
pub remote_addr_ip: Option<String>,
|
||||||
pub remote_addr_port: Option<u16>,
|
pub remote_addr_port: Option<u16>,
|
||||||
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2(async)]
|
#[op2(async)]
|
||||||
|
@ -432,7 +473,29 @@ pub async fn op_fetch_send(
|
||||||
|
|
||||||
let res = match request.0.await {
|
let res = match request.0.await {
|
||||||
Ok(Ok(res)) => res,
|
Ok(Ok(res)) => res,
|
||||||
Ok(Err(err)) => return Err(type_error(err.to_string())),
|
Ok(Err(err)) => {
|
||||||
|
// We're going to try and rescue the error cause from a stream and return it from this fetch.
|
||||||
|
// If any error in the chain is a reqwest body error, return that as a special result we can use to
|
||||||
|
// reconstruct an error chain (eg: `new TypeError(..., { cause: new Error(...) })`).
|
||||||
|
// TODO(mmastrac): it would be a lot easier if we just passed a v8::Global through here instead
|
||||||
|
let mut err_ref: &dyn std::error::Error = err.as_ref();
|
||||||
|
while let Some(err) = std::error::Error::source(err_ref) {
|
||||||
|
if let Some(err) = err.downcast_ref::<reqwest::Error>() {
|
||||||
|
if err.is_body() {
|
||||||
|
// Extracts the next error cause and uses that for the message
|
||||||
|
if let Some(err) = std::error::Error::source(err) {
|
||||||
|
return Ok(FetchResponse {
|
||||||
|
error: Some(err.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err_ref = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(type_error(err.to_string()));
|
||||||
|
}
|
||||||
Err(_) => return Err(type_error("request was cancelled")),
|
Err(_) => return Err(type_error("request was cancelled")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -465,6 +528,7 @@ pub async fn op_fetch_send(
|
||||||
content_length,
|
content_length,
|
||||||
remote_addr_ip,
|
remote_addr_ip,
|
||||||
remote_addr_port,
|
remote_addr_port,
|
||||||
|
error: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -599,74 +663,6 @@ impl Resource for FetchCancelHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps a [`mpsc::Receiver`] in a [`Stream`] that can be used as a Hyper [`Body`].
|
|
||||||
pub struct FetchBodyStream(pub mpsc::Receiver<Result<bytes::Bytes, Error>>);
|
|
||||||
|
|
||||||
impl Stream for FetchBodyStream {
|
|
||||||
type Item = Result<bytes::Bytes, Error>;
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut std::task::Context<'_>,
|
|
||||||
) -> std::task::Poll<Option<Self::Item>> {
|
|
||||||
self.0.poll_recv(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FetchRequestBodyResource {
|
|
||||||
pub body: AsyncRefCell<Option<mpsc::Sender<Result<bytes::Bytes, Error>>>>,
|
|
||||||
pub cancel: CancelHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Resource for FetchRequestBodyResource {
|
|
||||||
fn name(&self) -> Cow<str> {
|
|
||||||
"fetchRequestBody".into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(self: Rc<Self>, buf: BufView) -> AsyncResult<WriteOutcome> {
|
|
||||||
Box::pin(async move {
|
|
||||||
let bytes: bytes::Bytes = buf.into();
|
|
||||||
let nwritten = bytes.len();
|
|
||||||
let body = RcRef::map(&self, |r| &r.body).borrow_mut().await;
|
|
||||||
let body = (*body).as_ref();
|
|
||||||
let cancel = RcRef::map(self, |r| &r.cancel);
|
|
||||||
let body = body.ok_or(type_error(
|
|
||||||
"request body receiver not connected (request closed)",
|
|
||||||
))?;
|
|
||||||
body.send(Ok(bytes)).or_cancel(cancel).await?.map_err(|_| {
|
|
||||||
type_error("request body receiver not connected (request closed)")
|
|
||||||
})?;
|
|
||||||
Ok(WriteOutcome::Full { nwritten })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_error(self: Rc<Self>, error: Error) -> AsyncResult<()> {
|
|
||||||
async move {
|
|
||||||
let body = RcRef::map(&self, |r| &r.body).borrow_mut().await;
|
|
||||||
let body = (*body).as_ref();
|
|
||||||
let cancel = RcRef::map(self, |r| &r.cancel);
|
|
||||||
let body = body.ok_or(type_error(
|
|
||||||
"request body receiver not connected (request closed)",
|
|
||||||
))?;
|
|
||||||
body.send(Err(error)).or_cancel(cancel).await??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
.boxed_local()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(self: Rc<Self>) -> AsyncResult<()> {
|
|
||||||
async move {
|
|
||||||
let mut body = RcRef::map(&self, |r| &r.body).borrow_mut().await;
|
|
||||||
body.take();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
.boxed_local()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close(self: Rc<Self>) {
|
|
||||||
self.cancel.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BytesStream =
|
type BytesStream =
|
||||||
Pin<Box<dyn Stream<Item = Result<bytes::Bytes, std::io::Error>> + Unpin>>;
|
Pin<Box<dyn Stream<Item = Result<bytes::Bytes, std::io::Error>> + Unpin>>;
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,17 @@ use deno_core::error::type_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
use deno_core::AsyncRefCell;
|
|
||||||
use deno_core::ByteString;
|
use deno_core::ByteString;
|
||||||
use deno_core::CancelFuture;
|
use deno_core::CancelFuture;
|
||||||
use deno_core::CancelHandle;
|
use deno_core::CancelHandle;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
|
use deno_core::ResourceId;
|
||||||
use deno_fetch::get_or_create_client_from_state;
|
use deno_fetch::get_or_create_client_from_state;
|
||||||
use deno_fetch::FetchBodyStream;
|
|
||||||
use deno_fetch::FetchCancelHandle;
|
use deno_fetch::FetchCancelHandle;
|
||||||
use deno_fetch::FetchRequestBodyResource;
|
|
||||||
use deno_fetch::FetchRequestResource;
|
use deno_fetch::FetchRequestResource;
|
||||||
use deno_fetch::FetchReturn;
|
use deno_fetch::FetchReturn;
|
||||||
use deno_fetch::HttpClientResource;
|
use deno_fetch::HttpClientResource;
|
||||||
|
use deno_fetch::ResourceToBodyAdapter;
|
||||||
use reqwest::header::HeaderMap;
|
use reqwest::header::HeaderMap;
|
||||||
use reqwest::header::HeaderName;
|
use reqwest::header::HeaderName;
|
||||||
use reqwest::header::HeaderValue;
|
use reqwest::header::HeaderValue;
|
||||||
|
@ -31,7 +30,7 @@ pub fn op_node_http_request<P>(
|
||||||
#[string] url: String,
|
#[string] url: String,
|
||||||
#[serde] headers: Vec<(ByteString, ByteString)>,
|
#[serde] headers: Vec<(ByteString, ByteString)>,
|
||||||
#[smi] client_rid: Option<u32>,
|
#[smi] client_rid: Option<u32>,
|
||||||
has_body: bool,
|
#[smi] body: Option<ResourceId>,
|
||||||
) -> Result<FetchReturn, AnyError>
|
) -> Result<FetchReturn, AnyError>
|
||||||
where
|
where
|
||||||
P: crate::NodePermissions + 'static,
|
P: crate::NodePermissions + 'static,
|
||||||
|
@ -63,25 +62,16 @@ where
|
||||||
|
|
||||||
let mut request = client.request(method.clone(), url).headers(header_map);
|
let mut request = client.request(method.clone(), url).headers(header_map);
|
||||||
|
|
||||||
let request_body_rid = if has_body {
|
if let Some(body) = body {
|
||||||
// If no body is passed, we return a writer for streaming the body.
|
request = request.body(Body::wrap_stream(ResourceToBodyAdapter::new(
|
||||||
let (tx, stream) = tokio::sync::mpsc::channel(1);
|
state.resource_table.take_any(body)?,
|
||||||
|
)));
|
||||||
request = request.body(Body::wrap_stream(FetchBodyStream(stream)));
|
|
||||||
|
|
||||||
let request_body_rid = state.resource_table.add(FetchRequestBodyResource {
|
|
||||||
body: AsyncRefCell::new(Some(tx)),
|
|
||||||
cancel: CancelHandle::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
Some(request_body_rid)
|
|
||||||
} else {
|
} else {
|
||||||
// POST and PUT requests should always have a 0 length content-length,
|
// POST and PUT requests should always have a 0 length content-length,
|
||||||
// if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
|
// if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
|
||||||
if matches!(method, Method::POST | Method::PUT) {
|
if matches!(method, Method::POST | Method::PUT) {
|
||||||
request = request.header(CONTENT_LENGTH, HeaderValue::from(0));
|
request = request.header(CONTENT_LENGTH, HeaderValue::from(0));
|
||||||
}
|
}
|
||||||
None
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let cancel_handle = CancelHandle::new_rc();
|
let cancel_handle = CancelHandle::new_rc();
|
||||||
|
@ -104,7 +94,6 @@ where
|
||||||
|
|
||||||
Ok(FetchReturn {
|
Ok(FetchReturn {
|
||||||
request_rid,
|
request_rid,
|
||||||
request_body_rid,
|
|
||||||
cancel_handle_rid: Some(cancel_handle_rid),
|
cancel_handle_rid: Some(cancel_handle_rid),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
// TODO(petamoriken): enable prefer-primordials for node polyfills
|
||||||
// deno-lint-ignore-file prefer-primordials
|
// deno-lint-ignore-file prefer-primordials
|
||||||
|
|
||||||
const core = globalThis.__bootstrap.core;
|
|
||||||
import { getDefaultHighWaterMark } from "ext:deno_node/internal/streams/state.mjs";
|
import { getDefaultHighWaterMark } from "ext:deno_node/internal/streams/state.mjs";
|
||||||
import assert from "ext:deno_node/internal/assert.mjs";
|
import assert from "ext:deno_node/internal/assert.mjs";
|
||||||
import EE from "node:events";
|
import EE from "node:events";
|
||||||
|
@ -544,7 +543,7 @@ export class OutgoingMessage extends Stream {
|
||||||
data = new Uint8Array(data.buffer);
|
data = new Uint8Array(data.buffer);
|
||||||
}
|
}
|
||||||
if (data.buffer.byteLength > 0) {
|
if (data.buffer.byteLength > 0) {
|
||||||
core.writeAll(this._bodyWriteRid, data).then(() => {
|
this._bodyWriter.write(data).then(() => {
|
||||||
callback?.();
|
callback?.();
|
||||||
this.emit("drain");
|
this.emit("drain");
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
|
|
@ -58,6 +58,7 @@ import { createHttpClient } from "ext:deno_fetch/22_http_client.js";
|
||||||
import { headersEntries } from "ext:deno_fetch/20_headers.js";
|
import { headersEntries } from "ext:deno_fetch/20_headers.js";
|
||||||
import { timerId } from "ext:deno_web/03_abort_signal.js";
|
import { timerId } from "ext:deno_web/03_abort_signal.js";
|
||||||
import { clearTimeout as webClearTimeout } from "ext:deno_web/02_timers.js";
|
import { clearTimeout as webClearTimeout } from "ext:deno_web/02_timers.js";
|
||||||
|
import { resourceForReadableStream } from "ext:deno_web/06_streams.js";
|
||||||
import { TcpConn } from "ext:deno_net/01_net.js";
|
import { TcpConn } from "ext:deno_net/01_net.js";
|
||||||
|
|
||||||
enum STATUS_CODES {
|
enum STATUS_CODES {
|
||||||
|
@ -586,15 +587,28 @@ class ClientRequest extends OutgoingMessage {
|
||||||
const client = this._getClient() ?? createHttpClient({ http2: false });
|
const client = this._getClient() ?? createHttpClient({ http2: false });
|
||||||
this._client = client;
|
this._client = client;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.method === "POST" || this.method === "PATCH" || this.method === "PUT"
|
||||||
|
) {
|
||||||
|
const { readable, writable } = new TransformStream({
|
||||||
|
cancel: (e) => {
|
||||||
|
this._requestSendError = e;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._bodyWritable = writable;
|
||||||
|
this._bodyWriter = writable.getWriter();
|
||||||
|
|
||||||
|
this._bodyWriteRid = resourceForReadableStream(readable);
|
||||||
|
}
|
||||||
|
|
||||||
this._req = core.ops.op_node_http_request(
|
this._req = core.ops.op_node_http_request(
|
||||||
this.method,
|
this.method,
|
||||||
url,
|
url,
|
||||||
headers,
|
headers,
|
||||||
client.rid,
|
client.rid,
|
||||||
(this.method === "POST" || this.method === "PATCH" ||
|
this._bodyWriteRid,
|
||||||
this.method === "PUT") && this._contentLength !== 0,
|
|
||||||
);
|
);
|
||||||
this._bodyWriteRid = this._req.requestBodyRid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_implicitHeader() {
|
_implicitHeader() {
|
||||||
|
@ -638,23 +652,11 @@ class ClientRequest extends OutgoingMessage {
|
||||||
this._implicitHeader();
|
this._implicitHeader();
|
||||||
this._send("", "latin1");
|
this._send("", "latin1");
|
||||||
}
|
}
|
||||||
|
this._bodyWriter?.close();
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const [res, _] = await Promise.all([
|
const res = await core.opAsync("op_fetch_send", this._req.requestRid);
|
||||||
core.opAsync("op_fetch_send", this._req.requestRid),
|
|
||||||
(async () => {
|
|
||||||
if (this._bodyWriteRid) {
|
|
||||||
try {
|
|
||||||
await core.shutdown(this._bodyWriteRid);
|
|
||||||
} catch (err) {
|
|
||||||
this._requestSendError = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
core.tryClose(this._bodyWriteRid);
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
]);
|
|
||||||
try {
|
try {
|
||||||
cb?.();
|
cb?.();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
|
@ -12,6 +12,7 @@ const {
|
||||||
op_arraybuffer_was_detached,
|
op_arraybuffer_was_detached,
|
||||||
op_transfer_arraybuffer,
|
op_transfer_arraybuffer,
|
||||||
op_readable_stream_resource_allocate,
|
op_readable_stream_resource_allocate,
|
||||||
|
op_readable_stream_resource_allocate_sized,
|
||||||
op_readable_stream_resource_get_sink,
|
op_readable_stream_resource_get_sink,
|
||||||
op_readable_stream_resource_write_error,
|
op_readable_stream_resource_write_error,
|
||||||
op_readable_stream_resource_write_buf,
|
op_readable_stream_resource_write_buf,
|
||||||
|
@ -863,13 +864,16 @@ function readableStreamReadFn(reader, sink) {
|
||||||
* read operations, and those read operations will be fed by the output of the
|
* read operations, and those read operations will be fed by the output of the
|
||||||
* ReadableStream source.
|
* ReadableStream source.
|
||||||
* @param {ReadableStream<Uint8Array>} stream
|
* @param {ReadableStream<Uint8Array>} stream
|
||||||
|
* @param {number | undefined} length
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
function resourceForReadableStream(stream) {
|
function resourceForReadableStream(stream, length) {
|
||||||
const reader = acquireReadableStreamDefaultReader(stream);
|
const reader = acquireReadableStreamDefaultReader(stream);
|
||||||
|
|
||||||
// Allocate the resource
|
// Allocate the resource
|
||||||
const rid = op_readable_stream_resource_allocate();
|
const rid = typeof length == "number"
|
||||||
|
? op_readable_stream_resource_allocate_sized(length)
|
||||||
|
: op_readable_stream_resource_allocate();
|
||||||
|
|
||||||
// Close the Reader we get from the ReadableStream when the resource is closed, ignoring any errors
|
// Close the Reader we get from the ReadableStream when the resource is closed, ignoring any errors
|
||||||
PromisePrototypeCatch(
|
PromisePrototypeCatch(
|
||||||
|
|
|
@ -91,6 +91,7 @@ deno_core::extension!(deno_web,
|
||||||
op_sleep,
|
op_sleep,
|
||||||
op_transfer_arraybuffer,
|
op_transfer_arraybuffer,
|
||||||
stream_resource::op_readable_stream_resource_allocate,
|
stream_resource::op_readable_stream_resource_allocate,
|
||||||
|
stream_resource::op_readable_stream_resource_allocate_sized,
|
||||||
stream_resource::op_readable_stream_resource_get_sink,
|
stream_resource::op_readable_stream_resource_get_sink,
|
||||||
stream_resource::op_readable_stream_resource_write_error,
|
stream_resource::op_readable_stream_resource_write_error,
|
||||||
stream_resource::op_readable_stream_resource_write_buf,
|
stream_resource::op_readable_stream_resource_write_buf,
|
||||||
|
|
|
@ -197,7 +197,14 @@ impl BoundedBufferChannelInner {
|
||||||
pub fn write(&mut self, buffer: V8Slice<u8>) -> Result<(), V8Slice<u8>> {
|
pub fn write(&mut self, buffer: V8Slice<u8>) -> Result<(), V8Slice<u8>> {
|
||||||
let next_producer_index = (self.ring_producer + 1) % BUFFER_CHANNEL_SIZE;
|
let next_producer_index = (self.ring_producer + 1) % BUFFER_CHANNEL_SIZE;
|
||||||
if next_producer_index == self.ring_consumer {
|
if next_producer_index == self.ring_consumer {
|
||||||
return Err(buffer);
|
// Note that we may have been allowed to write because of a close/error condition, but the
|
||||||
|
// underlying channel is actually closed. If this is the case, we return `Ok(())`` and just
|
||||||
|
// drop the bytes on the floor.
|
||||||
|
return if self.closed || self.error.is_some() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(buffer)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_size += buffer.len();
|
self.current_size += buffer.len();
|
||||||
|
@ -336,6 +343,7 @@ struct ReadableStreamResource {
|
||||||
channel: BoundedBufferChannel,
|
channel: BoundedBufferChannel,
|
||||||
cancel_handle: CancelHandle,
|
cancel_handle: CancelHandle,
|
||||||
data: ReadableStreamResourceData,
|
data: ReadableStreamResourceData,
|
||||||
|
size_hint: (u64, Option<u64>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadableStreamResource {
|
impl ReadableStreamResource {
|
||||||
|
@ -378,6 +386,10 @@ impl Resource for ReadableStreamResource {
|
||||||
fn close(self: Rc<Self>) {
|
fn close(self: Rc<Self>) {
|
||||||
self.close_channel();
|
self.close_channel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (u64, Option<u64>) {
|
||||||
|
self.size_hint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for ReadableStreamResource {
|
impl Drop for ReadableStreamResource {
|
||||||
|
@ -438,6 +450,25 @@ pub fn op_readable_stream_resource_allocate(state: &mut OpState) -> ResourceId {
|
||||||
cancel_handle: Default::default(),
|
cancel_handle: Default::default(),
|
||||||
channel: BoundedBufferChannel::default(),
|
channel: BoundedBufferChannel::default(),
|
||||||
data: ReadableStreamResourceData { completion },
|
data: ReadableStreamResourceData { completion },
|
||||||
|
size_hint: (0, None),
|
||||||
|
};
|
||||||
|
state.resource_table.add(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocate a resource that wraps a ReadableStream, with a size hint.
|
||||||
|
#[op2(fast)]
|
||||||
|
#[smi]
|
||||||
|
pub fn op_readable_stream_resource_allocate_sized(
|
||||||
|
state: &mut OpState,
|
||||||
|
#[number] length: u64,
|
||||||
|
) -> ResourceId {
|
||||||
|
let completion = CompletionHandle::default();
|
||||||
|
let resource = ReadableStreamResource {
|
||||||
|
read_queue: Default::default(),
|
||||||
|
cancel_handle: Default::default(),
|
||||||
|
channel: BoundedBufferChannel::default(),
|
||||||
|
data: ReadableStreamResourceData { completion },
|
||||||
|
size_hint: (length, Some(length)),
|
||||||
};
|
};
|
||||||
state.resource_table.add(resource)
|
state.resource_table.add(resource)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue