mirror of
https://github.com/denoland/deno.git
synced 2024-11-29 16:30:56 -05:00
1a1283fa4d
This PR improves performance of `Deno.Serve` when providing `info` argument by creating `ServeHandlerInfo` class instead of creating an object literal with a getter on every request. ```js Deno.serve((_req, info) => new Response(info.remoteAddr.transport) }); ``` ### Benchmarks ``` wrk -d 10s --latency http://127.0.0.1:4500 Running 10s test @ http://127.0.0.1:4500 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 42.34us 16.30us 1.66ms 95.88% Req/Sec 118.17k 2.95k 127.38k 76.73% Latency Distribution 50% 38.00us 75% 41.00us 90% 56.00us 99% 83.00us 2375298 requests in 10.10s, 319.40MB read Requests/sec: 235177.04 Transfer/sec: 31.62MB ``` **main** ``` wrk -d 10s --latency http://127.0.0.1:4500 Running 10s test @ http://127.0.0.1:4500 2 threads and 10 connections Thread Stats Avg Stdev Max +/- Stdev Latency 78.86us 211.06us 3.58ms 96.52% Req/Sec 105.90k 4.35k 117.41k 78.22% Latency Distribution 50% 41.00us 75% 53.00us 90% 62.00us 99% 1.18ms 2127534 requests in 10.10s, 286.09MB read Requests/sec: 210647.49 Transfer/sec: 28.33MB ``` ``` cpu: 13th Gen Intel(R) Core(TM) i9-13900H runtime: deno 1.36.0 (x86_64-unknown-linux-gnu) benchmark time (avg) iter/s (min … max) p75 p99 p995 -------------------------------------------------------------------------- ----------------------------- new ServeHandlerInfo 3.43 ns/iter 291,508,889.3 (3.07 ns … 12.21 ns) 3.42 ns 3.84 ns 3.87 ns {} with getter 133.84 ns/iter 7,471,528.9 (92.9 ns … 458.95 ns) 132.45 ns 364.96 ns 429.43 ns ``` ---- ### Drawbacks: `.remoteAddr` is now not enumerable ``` ServeHandlerInfo {} ``` vs ``` { remoteAddr: [Getter] } ``` It'll break any code trying to iterate through `info` keys (Doubt there's anyone doing it though) ```js Deno.serve((req, info) => { console.log(Object.keys(info).length === 0) // true; return new Response("yes"); });
797 lines
21 KiB
JavaScript
797 lines
21 KiB
JavaScript
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
// deno-lint-ignore-file camelcase
|
|
const core = globalThis.Deno.core;
|
|
const primordials = globalThis.__bootstrap.primordials;
|
|
const internals = globalThis.__bootstrap.internals;
|
|
|
|
const { BadResourcePrototype } = core;
|
|
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";
|
|
import { fromInnerRequest, toInnerRequest } from "ext:deno_fetch/23_request.js";
|
|
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,
|
|
readableStreamClose,
|
|
readableStreamForRid,
|
|
ReadableStreamPrototype,
|
|
} from "ext:deno_web/06_streams.js";
|
|
import { listen, TcpConn } from "ext:deno_net/01_net.js";
|
|
import { listenTls } from "ext:deno_net/02_tls.js";
|
|
const {
|
|
ArrayPrototypePush,
|
|
Error,
|
|
ObjectPrototypeIsPrototypeOf,
|
|
PromisePrototypeCatch,
|
|
SafeSet,
|
|
SafeSetIterator,
|
|
SetPrototypeAdd,
|
|
SetPrototypeDelete,
|
|
Symbol,
|
|
SymbolFor,
|
|
TypeError,
|
|
Uint8Array,
|
|
Uint8ArrayPrototype,
|
|
} = primordials;
|
|
|
|
const {
|
|
op_http_get_request_headers,
|
|
op_http_get_request_method_and_url,
|
|
op_http_read_request_body,
|
|
op_http_serve,
|
|
op_http_serve_on,
|
|
op_http_set_promise_complete,
|
|
op_http_set_response_body_bytes,
|
|
op_http_set_response_body_resource,
|
|
op_http_set_response_body_stream,
|
|
op_http_set_response_body_text,
|
|
op_http_set_response_header,
|
|
op_http_set_response_headers,
|
|
op_http_set_response_trailers,
|
|
op_http_upgrade_raw,
|
|
op_http_upgrade_websocket_next,
|
|
op_http_try_wait,
|
|
op_http_wait,
|
|
} = core.ensureFastOps();
|
|
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",
|
|
);
|
|
|
|
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");
|
|
}
|
|
|
|
function addTrailers(resp, headerList) {
|
|
const inner = toInnerResponse(resp);
|
|
op_http_set_response_trailers(inner.slabId, headerList);
|
|
}
|
|
|
|
class InnerRequest {
|
|
#slabId;
|
|
#context;
|
|
#methodAndUri;
|
|
#streamRid;
|
|
#body;
|
|
#upgraded;
|
|
#urlValue;
|
|
|
|
constructor(slabId, context) {
|
|
this.#slabId = slabId;
|
|
this.#context = context;
|
|
this.#upgraded = false;
|
|
}
|
|
|
|
close() {
|
|
if (this.#streamRid !== undefined) {
|
|
core.close(this.#streamRid);
|
|
this.#streamRid = undefined;
|
|
}
|
|
this.#slabId = undefined;
|
|
}
|
|
|
|
get [_upgraded]() {
|
|
return this.#upgraded;
|
|
}
|
|
|
|
_wantsUpgrade(upgradeType, ...originalArgs) {
|
|
if (this.#upgraded) {
|
|
throw new Deno.errors.Http("already upgraded");
|
|
}
|
|
if (this.#slabId === undefined) {
|
|
throw new Deno.errors.Http("already closed");
|
|
}
|
|
|
|
// upgradeHttp is async
|
|
// TODO(mmastrac)
|
|
if (upgradeType == "upgradeHttp") {
|
|
throw "upgradeHttp is unavailable in Deno.serve at this time";
|
|
}
|
|
|
|
// upgradeHttpRaw is sync
|
|
if (upgradeType == "upgradeHttpRaw") {
|
|
const slabId = this.#slabId;
|
|
const underlyingConn = originalArgs[0];
|
|
|
|
this.url();
|
|
this.headerList;
|
|
this.close();
|
|
|
|
this.#upgraded = () => {};
|
|
|
|
const upgradeRid = op_http_upgrade_raw(slabId);
|
|
|
|
const conn = new TcpConn(
|
|
upgradeRid,
|
|
underlyingConn?.remoteAddr,
|
|
underlyingConn?.localAddr,
|
|
);
|
|
|
|
return { response: UPGRADE_RESPONSE_SENTINEL, conn };
|
|
}
|
|
|
|
// upgradeWebSocket is sync
|
|
if (upgradeType == "upgradeWebSocket") {
|
|
const response = originalArgs[0];
|
|
const ws = originalArgs[1];
|
|
|
|
const slabId = this.#slabId;
|
|
|
|
this.url();
|
|
this.headerList;
|
|
this.close();
|
|
|
|
const goAhead = new Deferred();
|
|
this.#upgraded = () => {
|
|
goAhead.resolve();
|
|
};
|
|
|
|
// Start the upgrade in the background.
|
|
(async () => {
|
|
try {
|
|
// Returns the upgraded websocket connection
|
|
const wsRid = await op_http_upgrade_websocket_next(
|
|
slabId,
|
|
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() {
|
|
if (this.#urlValue !== undefined) {
|
|
return this.#urlValue;
|
|
}
|
|
|
|
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.
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
|
}
|
|
|
|
const path = this.#methodAndUri[2];
|
|
|
|
// * is valid for OPTIONS
|
|
if (path === "*") {
|
|
return this.#urlValue = "*";
|
|
}
|
|
|
|
// If the path is empty, return the authority (valid for CONNECT)
|
|
if (path == "") {
|
|
return this.#urlValue = this.#methodAndUri[1];
|
|
}
|
|
|
|
// CONNECT requires an authority
|
|
if (this.#methodAndUri[0] == "CONNECT") {
|
|
return this.#urlValue = this.#methodAndUri[1];
|
|
}
|
|
|
|
const hostname = this.#methodAndUri[1];
|
|
if (hostname) {
|
|
// Construct a URL from the scheme, the hostname, and the path
|
|
return this.#urlValue = this.#context.scheme + hostname + path;
|
|
}
|
|
|
|
// Construct a URL from the scheme, the fallback hostname, and the path
|
|
return this.#urlValue = this.#context.scheme + this.#context.fallbackHost +
|
|
path;
|
|
}
|
|
|
|
get remoteAddr() {
|
|
if (this.#methodAndUri === undefined) {
|
|
if (this.#slabId === undefined) {
|
|
throw new TypeError("request closed");
|
|
}
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
|
}
|
|
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");
|
|
}
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
|
}
|
|
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;
|
|
}
|
|
this.#streamRid = op_http_read_request_body(this.#slabId);
|
|
this.#body = new InnerBody(readableStreamForRid(this.#streamRid, false));
|
|
return this.#body;
|
|
}
|
|
|
|
get headerList() {
|
|
if (this.#slabId === undefined) {
|
|
throw new TypeError("request closed");
|
|
}
|
|
const headers = [];
|
|
const reqHeaders = op_http_get_request_headers(this.#slabId);
|
|
for (let i = 0; i < reqHeaders.length; i += 2) {
|
|
ArrayPrototypePush(headers, [reqHeaders[i], reqHeaders[i + 1]]);
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
get slabId() {
|
|
return this.#slabId;
|
|
}
|
|
}
|
|
|
|
class CallbackContext {
|
|
abortController;
|
|
responseBodies;
|
|
scheme;
|
|
fallbackHost;
|
|
serverRid;
|
|
closed;
|
|
|
|
constructor(signal, args) {
|
|
signal?.addEventListener(
|
|
"abort",
|
|
() => this.close(),
|
|
{ once: true },
|
|
);
|
|
this.abortController = new AbortController();
|
|
this.responseBodies = new SafeSet();
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
class ServeHandlerInfo {
|
|
#inner = null;
|
|
constructor(inner) {
|
|
this.#inner = inner;
|
|
}
|
|
get remoteAddr() {
|
|
return this.#inner.remoteAddr;
|
|
}
|
|
}
|
|
|
|
function fastSyncResponseOrStream(req, respBody) {
|
|
if (respBody === null || respBody === undefined) {
|
|
// Don't set the body
|
|
return null;
|
|
}
|
|
|
|
const stream = respBody.streamOrStatic;
|
|
const body = stream.body;
|
|
|
|
if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, body)) {
|
|
op_http_set_response_body_bytes(req, body);
|
|
return null;
|
|
}
|
|
|
|
if (typeof body === "string") {
|
|
op_http_set_response_body_text(req, body);
|
|
return null;
|
|
}
|
|
|
|
// 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) {
|
|
op_http_set_response_body_resource(
|
|
req,
|
|
resourceBacking.rid,
|
|
resourceBacking.autoClose,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
async function asyncResponse(responseBodies, req, status, stream) {
|
|
const reader = stream.getReader();
|
|
let responseRid;
|
|
let closed = false;
|
|
let timeout;
|
|
|
|
try {
|
|
// IMPORTANT: We get a performance boost from this optimization, but V8 is very
|
|
// sensitive to the order and structure. Benchmark any changes to this code.
|
|
|
|
// Optimize for streams that are done in zero or one packets. We will not
|
|
// have to allocate a resource in this case.
|
|
const { value: value1, done: done1 } = await reader.read();
|
|
if (done1) {
|
|
closed = true;
|
|
// Exit 1: no response body at all, extreme fast path
|
|
// Reader will be closed by finally block
|
|
return;
|
|
}
|
|
|
|
// The second value cannot block indefinitely, as someone may be waiting on a response
|
|
// of the first packet that may influence this packet. We set this timeout arbitrarily to 250ms
|
|
// and we race it.
|
|
let timeoutPromise;
|
|
timeout = setTimeout(() => {
|
|
responseRid = op_http_set_response_body_stream(req);
|
|
SetPrototypeAdd(responseBodies, responseRid);
|
|
op_http_set_promise_complete(req, status);
|
|
// TODO(mmastrac): if this promise fails before we get to the await below, it crashes
|
|
// the process with an error:
|
|
//
|
|
// 'Uncaught (in promise) BadResource: failed to write'.
|
|
//
|
|
// To avoid this, we're going to swallow errors here and allow the code later in the
|
|
// file to re-throw them in a way that doesn't appear to be an uncaught promise rejection.
|
|
timeoutPromise = PromisePrototypeCatch(
|
|
core.writeAll(responseRid, value1),
|
|
() => null,
|
|
);
|
|
}, 250);
|
|
const { value: value2, done: done2 } = await reader.read();
|
|
|
|
if (timeoutPromise) {
|
|
await timeoutPromise;
|
|
if (done2) {
|
|
closed = true;
|
|
// Exit 2(a): read 2 is EOS, and timeout resolved.
|
|
// Reader will be closed by finally block
|
|
// Response stream will be closed by finally block.
|
|
return;
|
|
}
|
|
|
|
// Timeout resolved, value1 written but read2 is not EOS. Carry value2 forward.
|
|
} else {
|
|
clearTimeout(timeout);
|
|
timeout = undefined;
|
|
|
|
if (done2) {
|
|
// Exit 2(b): read 2 is EOS, and timeout did not resolve as we read fast enough.
|
|
// Reader will be closed by finally block
|
|
// No response stream
|
|
closed = true;
|
|
op_http_set_response_body_bytes(req, value1);
|
|
return;
|
|
}
|
|
|
|
responseRid = op_http_set_response_body_stream(req);
|
|
SetPrototypeAdd(responseBodies, responseRid);
|
|
op_http_set_promise_complete(req, status);
|
|
// Write our first packet
|
|
await core.writeAll(responseRid, value1);
|
|
}
|
|
|
|
await core.writeAll(responseRid, value2);
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) {
|
|
closed = true;
|
|
break;
|
|
}
|
|
await core.writeAll(responseRid, value);
|
|
}
|
|
} catch (error) {
|
|
closed = true;
|
|
try {
|
|
await reader.cancel(error);
|
|
} catch {
|
|
// Pass
|
|
}
|
|
} finally {
|
|
if (!closed) {
|
|
readableStreamClose(reader);
|
|
}
|
|
if (timeout !== undefined) {
|
|
clearTimeout(timeout);
|
|
}
|
|
if (responseRid) {
|
|
core.tryClose(responseRid);
|
|
SetPrototypeDelete(responseBodies, responseRid);
|
|
} else {
|
|
op_http_set_promise_complete(req, status);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
function mapToCallback(context, callback, onError) {
|
|
const responseBodies = context.responseBodies;
|
|
const signal = context.abortController.signal;
|
|
const hasCallback = callback.length > 0;
|
|
const hasOneCallback = callback.length === 1;
|
|
|
|
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.
|
|
let innerRequest;
|
|
let response;
|
|
try {
|
|
if (hasCallback) {
|
|
innerRequest = new InnerRequest(req, context);
|
|
const request = fromInnerRequest(innerRequest, signal, "immutable");
|
|
if (hasOneCallback) {
|
|
response = await callback(request);
|
|
} else {
|
|
response = await callback(
|
|
request,
|
|
new ServeHandlerInfo(innerRequest),
|
|
);
|
|
}
|
|
} else {
|
|
response = await callback();
|
|
}
|
|
} catch (error) {
|
|
try {
|
|
response = await onError(error);
|
|
} catch (error) {
|
|
console.error("Exception in onError while handling exception", error);
|
|
response = internalServerError();
|
|
}
|
|
}
|
|
|
|
const inner = toInnerResponse(response);
|
|
if (innerRequest?.[_upgraded]) {
|
|
// 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();
|
|
}
|
|
innerRequest?.[_upgraded]();
|
|
return;
|
|
}
|
|
|
|
// Did everything shut down while we were waiting?
|
|
if (context.closed) {
|
|
// 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);
|
|
innerRequest?.close();
|
|
return;
|
|
}
|
|
|
|
const status = inner.status;
|
|
const headers = inner.headerList;
|
|
if (headers && headers.length > 0) {
|
|
if (headers.length == 1) {
|
|
op_http_set_response_header(req, headers[0][0], headers[0][1]);
|
|
} else {
|
|
op_http_set_response_headers(req, headers);
|
|
}
|
|
}
|
|
|
|
// Attempt to respond quickly to this request, otherwise extract the stream
|
|
const stream = fastSyncResponseOrStream(req, inner.body);
|
|
if (stream !== null) {
|
|
// Handle the stream asynchronously
|
|
await asyncResponse(responseBodies, req, status, stream);
|
|
} else {
|
|
op_http_set_promise_complete(req, status);
|
|
}
|
|
|
|
innerRequest?.close();
|
|
};
|
|
}
|
|
|
|
function serve(arg1, arg2) {
|
|
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",
|
|
port: options.port ?? 8000,
|
|
reusePort: options.reusePort ?? false,
|
|
};
|
|
|
|
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.",
|
|
);
|
|
}
|
|
|
|
let listener;
|
|
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"];
|
|
listener = listenTls(listenOpts);
|
|
listenOpts.port = listener.addr.port;
|
|
} else {
|
|
listener = listen(listenOpts);
|
|
listenOpts.port = listener.addr.port;
|
|
}
|
|
|
|
const onListen = (scheme) => {
|
|
// 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;
|
|
const port = listenOpts.port;
|
|
|
|
if (options.onListen) {
|
|
options.onListen({ hostname, port });
|
|
} else {
|
|
console.log(`Listening on ${scheme}${hostname}:${port}/`);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
function serveHttpOn(context, callback) {
|
|
let ref = true;
|
|
let currentPromise = null;
|
|
const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId");
|
|
|
|
const promiseErrorHandler = (error) => {
|
|
// Abnormal exit
|
|
console.error(
|
|
"Terminating Deno.serve loop due to unexpected error",
|
|
error,
|
|
);
|
|
context.close();
|
|
};
|
|
|
|
// Run the server
|
|
const finished = (async () => {
|
|
const rid = context.serverRid;
|
|
while (true) {
|
|
let req;
|
|
try {
|
|
// 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.
|
|
while ((req = op_http_try_wait(rid)) !== -1) {
|
|
PromisePrototypeCatch(callback(req), promiseErrorHandler);
|
|
}
|
|
currentPromise = op_http_wait(rid);
|
|
if (!ref) {
|
|
core.unrefOp(currentPromise[promiseIdSymbol]);
|
|
}
|
|
req = await currentPromise;
|
|
currentPromise = null;
|
|
} catch (error) {
|
|
if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) {
|
|
break;
|
|
}
|
|
throw new Deno.errors.Http(error);
|
|
}
|
|
if (req === -1) {
|
|
break;
|
|
}
|
|
PromisePrototypeCatch(callback(req), promiseErrorHandler);
|
|
}
|
|
|
|
for (const streamRid of new SafeSetIterator(context.responseBodies)) {
|
|
core.tryClose(streamRid);
|
|
}
|
|
})();
|
|
|
|
return {
|
|
finished,
|
|
then() {
|
|
throw new Error(
|
|
"Deno.serve no longer returns a promise. await server.finished instead of server.",
|
|
);
|
|
},
|
|
ref() {
|
|
ref = true;
|
|
if (currentPromise) {
|
|
core.refOp(currentPromise[promiseIdSymbol]);
|
|
}
|
|
},
|
|
unref() {
|
|
ref = false;
|
|
if (currentPromise) {
|
|
core.unrefOp(currentPromise[promiseIdSymbol]);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
internals.addTrailers = addTrailers;
|
|
internals.upgradeHttpRaw = upgradeHttpRaw;
|
|
internals.serveHttpOnListener = serveHttpOnListener;
|
|
internals.serveHttpOnConnection = serveHttpOnConnection;
|
|
|
|
export {
|
|
addTrailers,
|
|
serve,
|
|
serveHttpOnConnection,
|
|
serveHttpOnListener,
|
|
upgradeHttpRaw,
|
|
};
|