2023-04-22 13:48:21 -04:00
|
|
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
2023-09-07 09:09:16 -04:00
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
const core = globalThis.Deno.core;
|
|
|
|
const primordials = globalThis.__bootstrap.primordials;
|
2023-04-26 18:58:18 -04:00
|
|
|
const internals = globalThis.__bootstrap.internals;
|
2023-04-22 13:48:21 -04:00
|
|
|
|
2023-09-11 20:06:38 -04:00
|
|
|
const { BadResourcePrototype, InterruptedPrototype } = core;
|
2023-04-22 13:48:21 -04:00
|
|
|
import { InnerBody } from "ext:deno_fetch/22_body.js";
|
|
|
|
import { Event } from "ext:deno_web/02_event.js";
|
|
|
|
import {
|
|
|
|
fromInnerResponse,
|
|
|
|
newInnerResponse,
|
|
|
|
toInnerResponse,
|
|
|
|
} from "ext:deno_fetch/23_response.js";
|
2023-04-26 18:58:18 -04:00
|
|
|
import { fromInnerRequest, toInnerRequest } from "ext:deno_fetch/23_request.js";
|
2023-04-22 13:48:21 -04:00
|
|
|
import { AbortController } from "ext:deno_web/03_abort_signal.js";
|
|
|
|
import {
|
|
|
|
_eventLoop,
|
|
|
|
_idleTimeoutDuration,
|
|
|
|
_idleTimeoutTimeout,
|
|
|
|
_protocol,
|
|
|
|
_readyState,
|
|
|
|
_rid,
|
|
|
|
_role,
|
|
|
|
_server,
|
|
|
|
_serverHandleIdleTimeout,
|
|
|
|
SERVER,
|
|
|
|
WebSocket,
|
|
|
|
} from "ext:deno_websocket/01_websocket.js";
|
|
|
|
import {
|
|
|
|
Deferred,
|
|
|
|
getReadableStreamResourceBacking,
|
|
|
|
readableStreamForRid,
|
|
|
|
ReadableStreamPrototype,
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
resourceForReadableStream,
|
2023-04-22 13:48:21 -04:00
|
|
|
} from "ext:deno_web/06_streams.js";
|
2023-05-31 19:20:39 -04:00
|
|
|
import { listen, TcpConn } from "ext:deno_net/01_net.js";
|
|
|
|
import { listenTls } from "ext:deno_net/02_tls.js";
|
2023-04-22 13:48:21 -04:00
|
|
|
const {
|
2023-06-05 15:57:01 -04:00
|
|
|
ArrayPrototypePush,
|
2023-06-13 14:05:23 -04:00
|
|
|
Error,
|
2023-04-22 13:48:21 -04:00
|
|
|
ObjectPrototypeIsPrototypeOf,
|
2023-05-01 09:30:02 -04:00
|
|
|
PromisePrototypeCatch,
|
2023-04-22 13:48:21 -04:00
|
|
|
Symbol,
|
2023-05-19 17:14:40 -04:00
|
|
|
SymbolFor,
|
2023-04-22 13:48:21 -04:00
|
|
|
TypeError,
|
|
|
|
Uint8Array,
|
2023-05-01 09:30:02 -04:00
|
|
|
Uint8ArrayPrototype,
|
2023-04-22 13:48:21 -04:00
|
|
|
} = primordials;
|
|
|
|
|
2023-04-30 04:50:24 -04:00
|
|
|
const {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_get_request_headers,
|
|
|
|
op_http_get_request_method_and_url,
|
|
|
|
op_http_read_request_body,
|
|
|
|
op_http_serve,
|
2023-05-31 19:20:39 -04:00
|
|
|
op_http_serve_on,
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_promise_complete,
|
|
|
|
op_http_set_response_body_bytes,
|
|
|
|
op_http_set_response_body_resource,
|
|
|
|
op_http_set_response_body_text,
|
|
|
|
op_http_set_response_header,
|
|
|
|
op_http_set_response_headers,
|
2023-05-18 22:10:25 -04:00
|
|
|
op_http_set_response_trailers,
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_upgrade_raw,
|
2023-05-15 19:24:41 -04:00
|
|
|
op_http_upgrade_websocket_next,
|
2023-05-30 20:02:52 -04:00
|
|
|
op_http_try_wait,
|
2023-05-15 19:24:41 -04:00
|
|
|
op_http_wait,
|
2023-09-11 20:06:38 -04:00
|
|
|
op_http_cancel,
|
|
|
|
op_http_close,
|
2023-06-06 05:01:28 -04:00
|
|
|
} = core.ensureFastOps();
|
2023-04-22 13:48:21 -04:00
|
|
|
const _upgraded = Symbol("_upgraded");
|
|
|
|
|
|
|
|
function internalServerError() {
|
|
|
|
// "Internal Server Error"
|
|
|
|
return new Response(
|
|
|
|
new Uint8Array([
|
|
|
|
73,
|
|
|
|
110,
|
|
|
|
116,
|
|
|
|
101,
|
|
|
|
114,
|
|
|
|
110,
|
|
|
|
97,
|
|
|
|
108,
|
|
|
|
32,
|
|
|
|
83,
|
|
|
|
101,
|
|
|
|
114,
|
|
|
|
118,
|
|
|
|
101,
|
|
|
|
114,
|
|
|
|
32,
|
|
|
|
69,
|
|
|
|
114,
|
|
|
|
114,
|
|
|
|
111,
|
|
|
|
114,
|
|
|
|
]),
|
|
|
|
{ status: 500 },
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Used to ensure that user returns a valid response (but not a different response) from handlers that are upgraded.
|
|
|
|
const UPGRADE_RESPONSE_SENTINEL = fromInnerResponse(
|
|
|
|
newInnerResponse(101),
|
|
|
|
"immutable",
|
|
|
|
);
|
|
|
|
|
2023-04-26 18:58:18 -04:00
|
|
|
function upgradeHttpRaw(req, conn) {
|
|
|
|
const inner = toInnerRequest(req);
|
|
|
|
if (inner._wantsUpgrade) {
|
|
|
|
return inner._wantsUpgrade("upgradeHttpRaw", conn);
|
|
|
|
}
|
|
|
|
throw new TypeError("upgradeHttpRaw may only be used with Deno.serve");
|
|
|
|
}
|
|
|
|
|
2023-05-18 22:10:25 -04:00
|
|
|
function addTrailers(resp, headerList) {
|
|
|
|
const inner = toInnerResponse(resp);
|
|
|
|
op_http_set_response_trailers(inner.slabId, headerList);
|
|
|
|
}
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
class InnerRequest {
|
|
|
|
#slabId;
|
|
|
|
#context;
|
|
|
|
#methodAndUri;
|
|
|
|
#streamRid;
|
|
|
|
#body;
|
|
|
|
#upgraded;
|
2023-07-30 09:13:28 -04:00
|
|
|
#urlValue;
|
2023-04-22 13:48:21 -04:00
|
|
|
|
|
|
|
constructor(slabId, context) {
|
|
|
|
this.#slabId = slabId;
|
|
|
|
this.#context = context;
|
|
|
|
this.#upgraded = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
this.#slabId = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
get [_upgraded]() {
|
|
|
|
return this.#upgraded;
|
|
|
|
}
|
|
|
|
|
|
|
|
_wantsUpgrade(upgradeType, ...originalArgs) {
|
2023-04-23 11:59:46 -04:00
|
|
|
if (this.#upgraded) {
|
|
|
|
throw new Deno.errors.Http("already upgraded");
|
|
|
|
}
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new Deno.errors.Http("already closed");
|
|
|
|
}
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
// upgradeHttp is async
|
|
|
|
// TODO(mmastrac)
|
|
|
|
if (upgradeType == "upgradeHttp") {
|
|
|
|
throw "upgradeHttp is unavailable in Deno.serve at this time";
|
|
|
|
}
|
|
|
|
|
2023-04-26 18:58:18 -04:00
|
|
|
// upgradeHttpRaw is sync
|
2023-04-22 13:48:21 -04:00
|
|
|
if (upgradeType == "upgradeHttpRaw") {
|
2023-04-26 18:58:18 -04:00
|
|
|
const slabId = this.#slabId;
|
|
|
|
const underlyingConn = originalArgs[0];
|
|
|
|
|
|
|
|
this.url();
|
|
|
|
this.headerList;
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
this.#upgraded = () => {};
|
|
|
|
|
2023-05-08 17:07:45 -04:00
|
|
|
const upgradeRid = op_http_upgrade_raw(slabId);
|
2023-04-26 18:58:18 -04:00
|
|
|
|
|
|
|
const conn = new TcpConn(
|
|
|
|
upgradeRid,
|
|
|
|
underlyingConn?.remoteAddr,
|
|
|
|
underlyingConn?.localAddr,
|
|
|
|
);
|
|
|
|
|
|
|
|
return { response: UPGRADE_RESPONSE_SENTINEL, conn };
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// upgradeWebSocket is sync
|
|
|
|
if (upgradeType == "upgradeWebSocket") {
|
|
|
|
const response = originalArgs[0];
|
|
|
|
const ws = originalArgs[1];
|
|
|
|
|
2023-04-23 11:59:46 -04:00
|
|
|
const slabId = this.#slabId;
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
this.url();
|
|
|
|
this.headerList;
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
const goAhead = new Deferred();
|
|
|
|
this.#upgraded = () => {
|
|
|
|
goAhead.resolve();
|
|
|
|
};
|
|
|
|
|
|
|
|
// Start the upgrade in the background.
|
|
|
|
(async () => {
|
|
|
|
try {
|
2023-05-15 19:24:41 -04:00
|
|
|
// Returns the upgraded websocket connection
|
|
|
|
const wsRid = await op_http_upgrade_websocket_next(
|
2023-04-23 11:59:46 -04:00
|
|
|
slabId,
|
2023-04-22 13:48:21 -04:00
|
|
|
response.headerList,
|
|
|
|
);
|
|
|
|
|
|
|
|
// We have to wait for the go-ahead signal
|
|
|
|
await goAhead;
|
|
|
|
|
|
|
|
ws[_rid] = wsRid;
|
|
|
|
ws[_readyState] = WebSocket.OPEN;
|
|
|
|
ws[_role] = SERVER;
|
|
|
|
const event = new Event("open");
|
|
|
|
ws.dispatchEvent(event);
|
|
|
|
|
|
|
|
ws[_eventLoop]();
|
|
|
|
if (ws[_idleTimeoutDuration]) {
|
|
|
|
ws.addEventListener(
|
|
|
|
"close",
|
|
|
|
() => clearTimeout(ws[_idleTimeoutTimeout]),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
ws[_serverHandleIdleTimeout]();
|
|
|
|
} catch (error) {
|
|
|
|
const event = new ErrorEvent("error", { error });
|
|
|
|
ws.dispatchEvent(event);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
return { response: UPGRADE_RESPONSE_SENTINEL, socket: ws };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
url() {
|
2023-07-30 09:13:28 -04:00
|
|
|
if (this.#urlValue !== undefined) {
|
|
|
|
return this.#urlValue;
|
|
|
|
}
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
if (this.#methodAndUri === undefined) {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
|
|
|
// TODO(mmastrac): This is quite slow as we're serializing a large number of values. We may want to consider
|
|
|
|
// splitting this up into multiple ops.
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const path = this.#methodAndUri[2];
|
|
|
|
|
|
|
|
// * is valid for OPTIONS
|
|
|
|
if (path === "*") {
|
2023-07-30 09:13:28 -04:00
|
|
|
return this.#urlValue = "*";
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the path is empty, return the authority (valid for CONNECT)
|
|
|
|
if (path == "") {
|
2023-07-30 09:13:28 -04:00
|
|
|
return this.#urlValue = this.#methodAndUri[1];
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// CONNECT requires an authority
|
|
|
|
if (this.#methodAndUri[0] == "CONNECT") {
|
2023-07-30 09:13:28 -04:00
|
|
|
return this.#urlValue = this.#methodAndUri[1];
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const hostname = this.#methodAndUri[1];
|
|
|
|
if (hostname) {
|
|
|
|
// Construct a URL from the scheme, the hostname, and the path
|
2023-07-30 09:13:28 -04:00
|
|
|
return this.#urlValue = this.#context.scheme + hostname + path;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Construct a URL from the scheme, the fallback hostname, and the path
|
2023-07-30 09:13:28 -04:00
|
|
|
return this.#urlValue = this.#context.scheme + this.#context.fallbackHost +
|
|
|
|
path;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get remoteAddr() {
|
|
|
|
if (this.#methodAndUri === undefined) {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
transport: "tcp",
|
|
|
|
hostname: this.#methodAndUri[3],
|
|
|
|
port: this.#methodAndUri[4],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
get method() {
|
|
|
|
if (this.#methodAndUri === undefined) {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
return this.#methodAndUri[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
get body() {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
|
|
|
if (this.#body !== undefined) {
|
|
|
|
return this.#body;
|
|
|
|
}
|
|
|
|
// If the method is GET or HEAD, we do not want to include a body here, even if the Rust
|
|
|
|
// side of the code is willing to provide it to us.
|
|
|
|
if (this.method == "GET" || this.method == "HEAD") {
|
|
|
|
this.#body = null;
|
|
|
|
return null;
|
|
|
|
}
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#streamRid = op_http_read_request_body(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
this.#body = new InnerBody(readableStreamForRid(this.#streamRid, false));
|
|
|
|
return this.#body;
|
|
|
|
}
|
|
|
|
|
|
|
|
get headerList() {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
2023-06-02 11:59:16 -04:00
|
|
|
const headers = [];
|
|
|
|
const reqHeaders = op_http_get_request_headers(this.#slabId);
|
|
|
|
for (let i = 0; i < reqHeaders.length; i += 2) {
|
2023-06-05 15:57:01 -04:00
|
|
|
ArrayPrototypePush(headers, [reqHeaders[i], reqHeaders[i + 1]]);
|
2023-06-02 11:59:16 -04:00
|
|
|
}
|
|
|
|
return headers;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get slabId() {
|
|
|
|
return this.#slabId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class CallbackContext {
|
2023-05-31 19:20:39 -04:00
|
|
|
abortController;
|
2023-04-22 13:48:21 -04:00
|
|
|
scheme;
|
|
|
|
fallbackHost;
|
|
|
|
serverRid;
|
|
|
|
closed;
|
2023-09-11 20:06:38 -04:00
|
|
|
closing;
|
2023-04-22 13:48:21 -04:00
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
constructor(signal, args) {
|
2023-09-11 20:06:38 -04:00
|
|
|
// The abort signal triggers a non-graceful shutdown
|
2023-05-31 19:20:39 -04:00
|
|
|
signal?.addEventListener(
|
|
|
|
"abort",
|
2023-09-11 20:06:38 -04:00
|
|
|
() => {
|
|
|
|
op_http_cancel(this.serverRid, false);
|
|
|
|
},
|
2023-05-31 19:20:39 -04:00
|
|
|
{ once: true },
|
|
|
|
);
|
|
|
|
this.abortController = new AbortController();
|
2023-04-22 13:48:21 -04:00
|
|
|
this.serverRid = args[0];
|
|
|
|
this.scheme = args[1];
|
|
|
|
this.fallbackHost = args[2];
|
|
|
|
this.closed = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
try {
|
|
|
|
this.closed = true;
|
|
|
|
core.tryClose(this.serverRid);
|
|
|
|
} catch {
|
|
|
|
// Pass
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-10 13:45:55 -04:00
|
|
|
class ServeHandlerInfo {
|
|
|
|
#inner = null;
|
|
|
|
constructor(inner) {
|
|
|
|
this.#inner = inner;
|
|
|
|
}
|
|
|
|
get remoteAddr() {
|
|
|
|
return this.#inner.remoteAddr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
function fastSyncResponseOrStream(req, respBody, status) {
|
2023-04-22 13:48:21 -04:00
|
|
|
if (respBody === null || respBody === undefined) {
|
|
|
|
// Don't set the body
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
op_http_set_promise_complete(req, status);
|
|
|
|
return;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const stream = respBody.streamOrStatic;
|
|
|
|
const body = stream.body;
|
|
|
|
|
|
|
|
if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, body)) {
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
op_http_set_response_body_bytes(req, body, status);
|
|
|
|
return;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof body === "string") {
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
op_http_set_response_body_text(req, body, status);
|
|
|
|
return;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// At this point in the response it needs to be a stream
|
|
|
|
if (!ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, stream)) {
|
|
|
|
throw TypeError("invalid response");
|
|
|
|
}
|
|
|
|
const resourceBacking = getReadableStreamResourceBacking(stream);
|
|
|
|
if (resourceBacking) {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_body_resource(
|
2023-04-22 13:48:21 -04:00
|
|
|
req,
|
|
|
|
resourceBacking.rid,
|
|
|
|
resourceBacking.autoClose,
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
status,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const rid = resourceForReadableStream(stream);
|
|
|
|
op_http_set_response_body_resource(
|
|
|
|
req,
|
|
|
|
rid,
|
|
|
|
true,
|
|
|
|
status,
|
2023-04-22 13:48:21 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maps the incoming request slab ID to a fully-fledged Request object, passes it to the user-provided
|
|
|
|
* callback, then extracts the response that was returned from that callback. The response is then pulled
|
|
|
|
* apart and handled on the Rust side.
|
|
|
|
*
|
|
|
|
* This function returns a promise that will only reject in the case of abnormal exit.
|
|
|
|
*/
|
2023-05-31 19:20:39 -04:00
|
|
|
function mapToCallback(context, callback, onError) {
|
|
|
|
const signal = context.abortController.signal;
|
2023-06-09 17:21:26 -04:00
|
|
|
const hasCallback = callback.length > 0;
|
|
|
|
const hasOneCallback = callback.length === 1;
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
return async function (req) {
|
|
|
|
// Get the response from the user-provided callback. If that fails, use onError. If that fails, return a fallback
|
|
|
|
// 500 error.
|
2023-04-26 12:41:54 -04:00
|
|
|
let innerRequest;
|
2023-04-22 13:48:21 -04:00
|
|
|
let response;
|
|
|
|
try {
|
2023-06-09 17:21:26 -04:00
|
|
|
if (hasCallback) {
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest = new InnerRequest(req, context);
|
|
|
|
const request = fromInnerRequest(innerRequest, signal, "immutable");
|
2023-06-09 17:21:26 -04:00
|
|
|
if (hasOneCallback) {
|
2023-04-26 12:41:54 -04:00
|
|
|
response = await callback(request);
|
|
|
|
} else {
|
2023-08-10 13:45:55 -04:00
|
|
|
response = await callback(
|
|
|
|
request,
|
|
|
|
new ServeHandlerInfo(innerRequest),
|
|
|
|
);
|
2023-04-26 12:41:54 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
response = await callback();
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
} catch (error) {
|
|
|
|
try {
|
|
|
|
response = await onError(error);
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Exception in onError while handling exception", error);
|
|
|
|
response = internalServerError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const inner = toInnerResponse(response);
|
2023-04-26 12:41:54 -04:00
|
|
|
if (innerRequest?.[_upgraded]) {
|
2023-04-22 13:48:21 -04:00
|
|
|
// We're done here as the connection has been upgraded during the callback and no longer requires servicing.
|
|
|
|
if (response !== UPGRADE_RESPONSE_SENTINEL) {
|
|
|
|
console.error("Upgrade response was not returned from callback");
|
|
|
|
context.close();
|
|
|
|
}
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest?.[_upgraded]();
|
2023-04-22 13:48:21 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did everything shut down while we were waiting?
|
|
|
|
if (context.closed) {
|
2023-05-16 19:00:59 -04:00
|
|
|
// We're shutting down, so this status shouldn't make it back to the client but "Service Unavailable" seems appropriate
|
|
|
|
op_http_set_promise_complete(req, 503);
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest?.close();
|
2023-04-22 13:48:21 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const status = inner.status;
|
|
|
|
const headers = inner.headerList;
|
|
|
|
if (headers && headers.length > 0) {
|
|
|
|
if (headers.length == 1) {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_header(req, headers[0][0], headers[0][1]);
|
2023-04-22 13:48:21 -04:00
|
|
|
} else {
|
2023-06-06 10:55:37 -04:00
|
|
|
op_http_set_response_headers(req, headers);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
feat(ext/web): resourceForReadableStream (#20180)
Extracted from fast streams work.
This is a resource wrapper for `ReadableStream`, allowing us to treat
all `ReadableStream` instances as resources, and remove special paths in
both `fetch` and `serve`.
Performance with a ReadableStream response yields ~18% improvement:
```
return new Response(new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]));
controller.close();
}
})
```
This patch:
```
12:36 $ third_party/prebuilt/mac/wrk http://localhost:8080
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 99.96us 100.03us 6.65ms 98.84%
Req/Sec 47.73k 2.43k 51.02k 89.11%
959308 requests in 10.10s, 117.10MB read
Requests/sec: 94978.71
Transfer/sec: 11.59MB
```
main:
```
Running 10s test @ http://localhost:8080
2 threads and 10 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 163.03us 685.51us 19.73ms 99.27%
Req/Sec 39.50k 3.98k 66.11k 95.52%
789582 requests in 10.10s, 82.83MB read
Requests/sec: 78182.65
Transfer/sec: 8.20MB
```
2023-08-17 09:52:37 -04:00
|
|
|
fastSyncResponseOrStream(req, inner.body, status);
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest?.close();
|
2023-04-22 13:48:21 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-05-18 20:59:23 -04:00
|
|
|
function serve(arg1, arg2) {
|
2023-04-22 13:48:21 -04:00
|
|
|
let options = undefined;
|
|
|
|
let handler = undefined;
|
|
|
|
if (typeof arg1 === "function") {
|
|
|
|
handler = arg1;
|
|
|
|
} else if (typeof arg2 === "function") {
|
|
|
|
handler = arg2;
|
|
|
|
options = arg1;
|
|
|
|
} else {
|
|
|
|
options = arg1;
|
|
|
|
}
|
|
|
|
if (handler === undefined) {
|
|
|
|
if (options === undefined) {
|
|
|
|
throw new TypeError(
|
|
|
|
"No handler was provided, so an options bag is mandatory.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
handler = options.handler;
|
|
|
|
}
|
|
|
|
if (typeof handler !== "function") {
|
|
|
|
throw new TypeError("A handler function must be provided.");
|
|
|
|
}
|
|
|
|
if (options === undefined) {
|
|
|
|
options = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const wantsHttps = options.cert || options.key;
|
|
|
|
const signal = options.signal;
|
|
|
|
const onError = options.onError ?? function (error) {
|
|
|
|
console.error(error);
|
|
|
|
return internalServerError();
|
|
|
|
};
|
|
|
|
const listenOpts = {
|
|
|
|
hostname: options.hostname ?? "0.0.0.0",
|
2023-07-03 19:46:32 -04:00
|
|
|
port: options.port ?? 8000,
|
2023-04-22 13:48:21 -04:00
|
|
|
reusePort: options.reusePort ?? false,
|
|
|
|
};
|
|
|
|
|
2023-07-19 14:43:49 -04:00
|
|
|
if (options.certFile || options.keyFile) {
|
|
|
|
throw new TypeError(
|
|
|
|
"Unsupported 'certFile' / 'keyFile' options provided: use 'cert' / 'key' instead.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (options.alpnProtocols) {
|
|
|
|
throw new TypeError(
|
|
|
|
"Unsupported 'alpnProtocols' option provided. 'h2' and 'http/1.1' are automatically supported.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
let listener;
|
2023-04-22 13:48:21 -04:00
|
|
|
if (wantsHttps) {
|
|
|
|
if (!options.cert || !options.key) {
|
|
|
|
throw new TypeError(
|
|
|
|
"Both cert and key must be provided to enable HTTPS.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
listenOpts.cert = options.cert;
|
|
|
|
listenOpts.key = options.key;
|
|
|
|
listenOpts.alpnProtocols = ["h2", "http/1.1"];
|
2023-05-31 19:20:39 -04:00
|
|
|
listener = listenTls(listenOpts);
|
2023-04-22 13:48:21 -04:00
|
|
|
listenOpts.port = listener.addr.port;
|
|
|
|
} else {
|
2023-05-31 19:20:39 -04:00
|
|
|
listener = listen(listenOpts);
|
2023-04-22 13:48:21 -04:00
|
|
|
listenOpts.port = listener.addr.port;
|
|
|
|
}
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
const onListen = (scheme) => {
|
2023-06-14 08:58:41 -04:00
|
|
|
// If the hostname is "0.0.0.0", we display "localhost" in console
|
|
|
|
// because browsers in Windows don't resolve "0.0.0.0".
|
|
|
|
// See the discussion in https://github.com/denoland/deno_std/issues/1165
|
|
|
|
const hostname = listenOpts.hostname == "0.0.0.0"
|
|
|
|
? "localhost"
|
|
|
|
: listenOpts.hostname;
|
2023-05-31 19:20:39 -04:00
|
|
|
const port = listenOpts.port;
|
2023-06-14 08:58:41 -04:00
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
if (options.onListen) {
|
2023-06-14 08:58:41 -04:00
|
|
|
options.onListen({ hostname, port });
|
2023-05-31 19:20:39 -04:00
|
|
|
} else {
|
|
|
|
console.log(`Listening on ${scheme}${hostname}:${port}/`);
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
};
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
return serveHttpOnListener(listener, signal, handler, onError, onListen);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary listener.
|
|
|
|
*/
|
|
|
|
function serveHttpOnListener(listener, signal, handler, onError, onListen) {
|
|
|
|
const context = new CallbackContext(signal, op_http_serve(listener.rid));
|
|
|
|
const callback = mapToCallback(context, handler, onError);
|
|
|
|
|
|
|
|
onListen(context.scheme);
|
|
|
|
|
|
|
|
return serveHttpOn(context, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary connection.
|
|
|
|
*/
|
|
|
|
function serveHttpOnConnection(connection, signal, handler, onError, onListen) {
|
|
|
|
const context = new CallbackContext(signal, op_http_serve_on(connection.rid));
|
|
|
|
const callback = mapToCallback(context, handler, onError);
|
|
|
|
|
|
|
|
onListen(context.scheme);
|
|
|
|
|
|
|
|
return serveHttpOn(context, callback);
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
function serveHttpOn(context, callback) {
|
2023-05-19 17:14:40 -04:00
|
|
|
let ref = true;
|
|
|
|
let currentPromise = null;
|
|
|
|
const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId");
|
|
|
|
|
2023-06-10 06:17:56 -04:00
|
|
|
const promiseErrorHandler = (error) => {
|
|
|
|
// Abnormal exit
|
|
|
|
console.error(
|
|
|
|
"Terminating Deno.serve loop due to unexpected error",
|
|
|
|
error,
|
|
|
|
);
|
|
|
|
context.close();
|
|
|
|
};
|
|
|
|
|
2023-05-18 20:59:23 -04:00
|
|
|
// Run the server
|
|
|
|
const finished = (async () => {
|
2023-06-09 18:45:56 -04:00
|
|
|
const rid = context.serverRid;
|
2023-05-18 20:59:23 -04:00
|
|
|
while (true) {
|
|
|
|
let req;
|
|
|
|
try {
|
2023-05-30 20:02:52 -04:00
|
|
|
// Attempt to pull as many requests out of the queue as possible before awaiting. This API is
|
|
|
|
// a synchronous, non-blocking API that returns u32::MAX if anything goes wrong.
|
2023-08-03 16:36:32 -04:00
|
|
|
while ((req = op_http_try_wait(rid)) !== -1) {
|
2023-06-10 06:17:56 -04:00
|
|
|
PromisePrototypeCatch(callback(req), promiseErrorHandler);
|
2023-05-30 20:02:52 -04:00
|
|
|
}
|
2023-05-19 17:14:40 -04:00
|
|
|
currentPromise = op_http_wait(rid);
|
|
|
|
if (!ref) {
|
|
|
|
core.unrefOp(currentPromise[promiseIdSymbol]);
|
|
|
|
}
|
|
|
|
req = await currentPromise;
|
|
|
|
currentPromise = null;
|
2023-05-18 20:59:23 -04:00
|
|
|
} catch (error) {
|
|
|
|
if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) {
|
|
|
|
break;
|
|
|
|
}
|
2023-09-11 20:06:38 -04:00
|
|
|
if (ObjectPrototypeIsPrototypeOf(InterruptedPrototype, error)) {
|
|
|
|
break;
|
|
|
|
}
|
2023-05-18 20:59:23 -04:00
|
|
|
throw new Deno.errors.Http(error);
|
|
|
|
}
|
2023-08-03 16:36:32 -04:00
|
|
|
if (req === -1) {
|
2023-04-22 13:48:21 -04:00
|
|
|
break;
|
|
|
|
}
|
2023-06-10 06:17:56 -04:00
|
|
|
PromisePrototypeCatch(callback(req), promiseErrorHandler);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
2023-09-11 20:06:38 -04:00
|
|
|
|
|
|
|
if (!context.closed && !context.closing) {
|
|
|
|
context.closed = true;
|
|
|
|
await op_http_close(rid, false);
|
|
|
|
context.close();
|
|
|
|
}
|
2023-05-18 20:59:23 -04:00
|
|
|
})();
|
2023-04-22 13:48:21 -04:00
|
|
|
|
2023-05-19 17:14:40 -04:00
|
|
|
return {
|
|
|
|
finished,
|
2023-09-11 20:06:38 -04:00
|
|
|
async shutdown() {
|
|
|
|
if (!context.closed && !context.closing) {
|
|
|
|
// Shut this HTTP server down gracefully
|
|
|
|
context.closing = true;
|
|
|
|
await op_http_close(context.serverRid, true);
|
|
|
|
context.closed = true;
|
|
|
|
}
|
|
|
|
},
|
2023-06-13 14:05:23 -04:00
|
|
|
then() {
|
|
|
|
throw new Error(
|
|
|
|
"Deno.serve no longer returns a promise. await server.finished instead of server.",
|
|
|
|
);
|
|
|
|
},
|
2023-05-19 17:14:40 -04:00
|
|
|
ref() {
|
|
|
|
ref = true;
|
|
|
|
if (currentPromise) {
|
|
|
|
core.refOp(currentPromise[promiseIdSymbol]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
unref() {
|
|
|
|
ref = false;
|
|
|
|
if (currentPromise) {
|
|
|
|
core.unrefOp(currentPromise[promiseIdSymbol]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
2023-05-18 22:10:25 -04:00
|
|
|
internals.addTrailers = addTrailers;
|
2023-04-26 18:58:18 -04:00
|
|
|
internals.upgradeHttpRaw = upgradeHttpRaw;
|
2023-05-31 19:20:39 -04:00
|
|
|
internals.serveHttpOnListener = serveHttpOnListener;
|
|
|
|
internals.serveHttpOnConnection = serveHttpOnConnection;
|
2023-04-26 18:58:18 -04:00
|
|
|
|
2023-06-06 06:29:55 -04:00
|
|
|
export {
|
|
|
|
addTrailers,
|
|
|
|
serve,
|
|
|
|
serveHttpOnConnection,
|
|
|
|
serveHttpOnListener,
|
|
|
|
upgradeHttpRaw,
|
|
|
|
};
|