summaryrefslogtreecommitdiff
path: root/ext/http/01_http.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/http/01_http.js')
-rw-r--r--ext/http/01_http.js236
1 files changed, 233 insertions, 3 deletions
diff --git a/ext/http/01_http.js b/ext/http/01_http.js
index 7224df3c5..ab0d6626a 100644
--- a/ext/http/01_http.js
+++ b/ext/http/01_http.js
@@ -31,8 +31,8 @@ import {
_serverHandleIdleTimeout,
WebSocket,
} from "ext:deno_websocket/01_websocket.js";
-import { TcpConn, UnixConn } from "ext:deno_net/01_net.js";
-import { TlsConn } from "ext:deno_net/02_tls.js";
+import { listen, TcpConn, UnixConn } from "ext:deno_net/01_net.js";
+import { listenTls, TlsConn } from "ext:deno_net/02_tls.js";
import {
Deferred,
getReadableStreamResourceBacking,
@@ -50,10 +50,13 @@ const {
Set,
SetPrototypeAdd,
SetPrototypeDelete,
+ SetPrototypeClear,
StringPrototypeCharCodeAt,
StringPrototypeIncludes,
StringPrototypeToLowerCase,
StringPrototypeSplit,
+ SafeSet,
+ PromisePrototypeCatch,
Symbol,
SymbolAsyncIterator,
TypeError,
@@ -554,4 +557,231 @@ function buildCaseInsensitiveCommaValueFinder(checkText) {
internals.buildCaseInsensitiveCommaValueFinder =
buildCaseInsensitiveCommaValueFinder;
-export { _ws, HttpConn, upgradeHttp, upgradeHttpRaw, upgradeWebSocket };
+function hostnameForDisplay(hostname) {
+ // 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
+ return hostname === "0.0.0.0" ? "localhost" : hostname;
+}
+
+async function respond(handler, requestEvent, connInfo, onError) {
+ let response;
+
+ try {
+ response = await handler(requestEvent.request, connInfo);
+
+ if (response.bodyUsed && response.body !== null) {
+ throw new TypeError("Response body already consumed.");
+ }
+ } catch (e) {
+ // Invoke `onError` handler if the request handler throws.
+ response = await onError(e);
+ }
+
+ try {
+ // Send the response.
+ await requestEvent.respondWith(response);
+ } catch {
+ // `respondWith()` can throw for various reasons, including downstream and
+ // upstream connection errors, as well as errors thrown during streaming
+ // of the response content. In order to avoid false negatives, we ignore
+ // the error here and let `serveHttp` close the connection on the
+ // following iteration if it is in fact a downstream connection error.
+ }
+}
+
+async function serveConnection(
+ server,
+ activeHttpConnections,
+ handler,
+ httpConn,
+ connInfo,
+ onError,
+) {
+ while (!server.closed) {
+ let requestEvent = null;
+
+ try {
+ // Yield the new HTTP request on the connection.
+ requestEvent = await httpConn.nextRequest();
+ } catch {
+ // Connection has been closed.
+ break;
+ }
+
+ if (requestEvent === null) {
+ break;
+ }
+
+ respond(handler, requestEvent, connInfo, onError);
+ }
+
+ SetPrototypeDelete(activeHttpConnections, httpConn);
+ try {
+ httpConn.close();
+ } catch {
+ // Connection has already been closed.
+ }
+}
+
+async function serve(arg1, arg2) {
+ let options = undefined;
+ let handler = undefined;
+ if (typeof arg1 === "function") {
+ handler = arg1;
+ options = arg2;
+ } 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 signal = options.signal;
+ const onError = options.onError ?? function (error) {
+ console.error(error);
+ return new Response("Internal Server Error", { status: 500 });
+ };
+ const onListen = options.onListen ?? function ({ port }) {
+ console.log(
+ `Listening on http://${hostnameForDisplay(listenOpts.hostname)}:${port}/`,
+ );
+ };
+ const listenOpts = {
+ hostname: options.hostname ?? "127.0.0.1",
+ port: options.port ?? 9000,
+ reuseport: options.reusePort ?? false,
+ };
+
+ if (options.cert || options.key) {
+ 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;
+ }
+
+ let listener;
+ if (listenOpts.cert && listenOpts.key) {
+ listener = listenTls({
+ hostname: listenOpts.hostname,
+ port: listenOpts.port,
+ cert: listenOpts.cert,
+ key: listenOpts.key,
+ });
+ } else {
+ listener = listen({
+ hostname: listenOpts.hostname,
+ port: listenOpts.port,
+ });
+ }
+
+ const serverDeferred = new Deferred();
+ const activeHttpConnections = new SafeSet();
+
+ const server = {
+ transport: listenOpts.cert && listenOpts.key ? "https" : "http",
+ hostname: listenOpts.hostname,
+ port: listenOpts.port,
+ closed: false,
+
+ close() {
+ if (server.closed) {
+ return;
+ }
+ server.closed = true;
+ try {
+ listener.close();
+ } catch {
+ // Might have been already closed.
+ }
+
+ for (const httpConn of new SafeSetIterator(activeHttpConnections)) {
+ try {
+ httpConn.close();
+ } catch {
+ // Might have been already closed.
+ }
+ }
+
+ SetPrototypeClear(activeHttpConnections);
+ serverDeferred.resolve();
+ },
+
+ async serve() {
+ while (!server.closed) {
+ let conn;
+
+ try {
+ conn = await listener.accept();
+ } catch {
+ // Listener has been closed.
+ if (!server.closed) {
+ console.log("Listener has closed unexpectedly");
+ }
+ break;
+ }
+
+ let httpConn;
+ try {
+ const rid = ops.op_http_start(conn.rid);
+ httpConn = new HttpConn(rid, conn.remoteAddr, conn.localAddr);
+ } catch {
+ // Connection has been closed;
+ continue;
+ }
+
+ SetPrototypeAdd(activeHttpConnections, httpConn);
+
+ const connInfo = {
+ localAddr: conn.localAddr,
+ remoteAddr: conn.remoteAddr,
+ };
+ // Serve the HTTP connection
+ serveConnection(
+ server,
+ activeHttpConnections,
+ handler,
+ httpConn,
+ connInfo,
+ onError,
+ );
+ }
+ await serverDeferred.promise;
+ },
+ };
+
+ signal?.addEventListener(
+ "abort",
+ () => {
+ try {
+ server.close();
+ } catch {
+ // Pass
+ }
+ },
+ { once: true },
+ );
+
+ onListen(listener.addr);
+
+ await PromisePrototypeCatch(server.serve(), console.error);
+}
+
+export { _ws, HttpConn, serve, upgradeHttp, upgradeHttpRaw, upgradeWebSocket };