1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 08:09:08 -05:00

feat(ext/http): Add addr to HttpServer (#23442)

Adds an `addr` field to `HttpServer` to simplify the pattern
`Deno.serve({ onListen({ port } => listenPort = port })`. This becomes:
`const server = Deno.serve({}); port = server.addr.port`.

Changes:
- Refactors `serve` overloads to split TLS out (in preparation for
landing a place for the TLS SNI information)
- Adds an `addr` field to `HttpServer` that matches the `addr` field of
the corresponding `Deno.Listener`s.
This commit is contained in:
Matt Mastracci 2024-04-19 18:09:50 -06:00 committed by GitHub
parent 93a40c1c64
commit 365e1f48f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 137 additions and 32 deletions

View file

@ -6217,7 +6217,7 @@ declare namespace Deno {
*/ */
export function gid(): number | null; export function gid(): number | null;
/** Information for a HTTP request. /** Additional information for an HTTP request and its connection.
* *
* @category HTTP Server * @category HTTP Server
*/ */
@ -6269,7 +6269,7 @@ declare namespace Deno {
onError?: (error: unknown) => Response | Promise<Response>; onError?: (error: unknown) => Response | Promise<Response>;
/** The callback which is called when the server starts listening. */ /** The callback which is called when the server starts listening. */
onListen?: (params: { hostname: string; port: number }) => void; onListen?: (localAddr: Deno.NetAddr) => void;
} }
/** Additional options which are used when opening a TLS (HTTPS) server. /** Additional options which are used when opening a TLS (HTTPS) server.
@ -6304,6 +6304,14 @@ declare namespace Deno {
handler: ServeHandler; handler: ServeHandler;
} }
/**
* @category HTTP Server
*/
export interface ServeTlsInit {
/** The handler to invoke to process each incoming request. */
handler: ServeHandler;
}
/** @category HTTP Server */ /** @category HTTP Server */
export interface ServeUnixOptions { export interface ServeUnixOptions {
/** The unix domain socket path to listen on. */ /** The unix domain socket path to listen on. */
@ -6316,7 +6324,7 @@ declare namespace Deno {
onError?: (error: unknown) => Response | Promise<Response>; onError?: (error: unknown) => Response | Promise<Response>;
/** The callback which is called when the server starts listening. */ /** The callback which is called when the server starts listening. */
onListen?: (params: { path: string }) => void; onListen?: (localAddr: Deno.UnixAddr) => void;
} }
/** Information for a unix domain socket HTTP request. /** Information for a unix domain socket HTTP request.
@ -6353,12 +6361,16 @@ declare namespace Deno {
* *
* @category HTTP Server * @category HTTP Server
*/ */
export interface HttpServer extends AsyncDisposable { export interface HttpServer<A extends Deno.Addr = Deno.Addr>
extends AsyncDisposable {
/** A promise that resolves once server finishes - eg. when aborted using /** A promise that resolves once server finishes - eg. when aborted using
* the signal passed to {@linkcode ServeOptions.signal}. * the signal passed to {@linkcode ServeOptions.signal}.
*/ */
finished: Promise<void>; finished: Promise<void>;
/** The local address this server is listening on. */
addr: A;
/** /**
* Make the server block the event loop from finishing. * Make the server block the event loop from finishing.
* *
@ -6395,7 +6407,7 @@ declare namespace Deno {
* *
* @category HTTP Server * @category HTTP Server
*/ */
export function serve(handler: ServeHandler): HttpServer; export function serve(handler: ServeHandler): HttpServer<Deno.NetAddr>;
/** Serves HTTP requests with the given option bag and handler. /** Serves HTTP requests with the given option bag and handler.
* *
* You can specify the socket path with `path` option. * You can specify the socket path with `path` option.
@ -6444,7 +6456,67 @@ declare namespace Deno {
export function serve( export function serve(
options: ServeUnixOptions, options: ServeUnixOptions,
handler: ServeUnixHandler, handler: ServeUnixHandler,
): HttpServer; ): HttpServer<Deno.UnixAddr>;
/** Serves HTTP requests with the given option bag and handler.
*
* You can specify an object with a port and hostname option, which is the
* address to listen on. The default is port `8000` on hostname `"127.0.0.1"`.
*
* You can change the address to listen on using the `hostname` and `port`
* options. The below example serves on port `3000` and hostname `"0.0.0.0"`.
*
* ```ts
* Deno.serve(
* { port: 3000, hostname: "0.0.0.0" },
* (_req) => new Response("Hello, world")
* );
* ```
*
* You can stop the server with an {@linkcode AbortSignal}. The abort signal
* needs to be passed as the `signal` option in the options bag. The server
* aborts when the abort signal is aborted. To wait for the server to close,
* await the promise returned from the `Deno.serve` API.
*
* ```ts
* const ac = new AbortController();
*
* const server = Deno.serve(
* { signal: ac.signal },
* (_req) => new Response("Hello, world")
* );
* server.finished.then(() => console.log("Server closed"));
*
* console.log("Closing server...");
* ac.abort();
* ```
*
* By default `Deno.serve` prints the message
* `Listening on http://<hostname>:<port>/` on listening. If you like to
* change this behavior, you can specify a custom `onListen` callback.
*
* ```ts
* Deno.serve({
* onListen({ port, hostname }) {
* console.log(`Server started at http://${hostname}:${port}`);
* // ... more info specific to your server ..
* },
* }, (_req) => new Response("Hello, world"));
* ```
*
* To enable TLS you must specify the `key` and `cert` options.
*
* ```ts
* const cert = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n";
* const key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n";
* Deno.serve({ cert, key }, (_req) => new Response("Hello, world"));
* ```
*
* @category HTTP Server
*/
export function serve(
options: ServeOptions,
handler: ServeHandler,
): HttpServer<Deno.NetAddr>;
/** Serves HTTP requests with the given option bag and handler. /** Serves HTTP requests with the given option bag and handler.
* *
* You can specify an object with a port and hostname option, which is the * You can specify an object with a port and hostname option, which is the
@ -6503,11 +6575,10 @@ declare namespace Deno {
*/ */
export function serve( export function serve(
options: options:
| ServeOptions
| ServeTlsOptions | ServeTlsOptions
| (ServeTlsOptions & TlsCertifiedKeyOptions), | (ServeTlsOptions & TlsCertifiedKeyOptions),
handler: ServeHandler, handler: ServeHandler,
): HttpServer; ): HttpServer<Deno.NetAddr>;
/** Serves HTTP requests with the given option bag. /** Serves HTTP requests with the given option bag.
* *
* You can specify an object with the path option, which is the * You can specify an object with the path option, which is the
@ -6534,7 +6605,7 @@ declare namespace Deno {
*/ */
export function serve( export function serve(
options: ServeUnixInit & ServeUnixOptions, options: ServeUnixInit & ServeUnixOptions,
): HttpServer; ): HttpServer<Deno.UnixAddr>;
/** Serves HTTP requests with the given option bag. /** Serves HTTP requests with the given option bag.
* *
* You can specify an object with a port and hostname option, which is the * You can specify an object with a port and hostname option, which is the
@ -6563,10 +6634,39 @@ declare namespace Deno {
export function serve( export function serve(
options: options:
& ServeInit & ServeInit
& ServeOptions,
): HttpServer<Deno.NetAddr>;
/** Serves HTTP requests with the given option bag.
*
* You can specify an object with a port and hostname option, which is the
* address to listen on. The default is port `8000` on hostname `"127.0.0.1"`.
*
* ```ts
* const ac = new AbortController();
*
* const server = Deno.serve({
* port: 3000,
* hostname: "0.0.0.0",
* handler: (_req) => new Response("Hello, world"),
* signal: ac.signal,
* onListen({ port, hostname }) {
* console.log(`Server started at http://${hostname}:${port}`);
* },
* });
* server.finished.then(() => console.log("Server closed"));
*
* console.log("Closing server...");
* ac.abort();
* ```
*
* @category HTTP Server
*/
export function serve(
options:
& ServeTlsInit
& ( & (
| ServeOptions
| ServeTlsOptions | ServeTlsOptions
| (ServeTlsOptions & TlsCertifiedKeyOptions) | (ServeTlsOptions & TlsCertifiedKeyOptions)
), ),
): HttpServer; ): HttpServer<Deno.NetAddr>;
} }

View file

@ -552,7 +552,7 @@ function serve(arg1, arg2) {
const path = listener.addr.path; const path = listener.addr.path;
return serveHttpOnListener(listener, signal, handler, onError, () => { return serveHttpOnListener(listener, signal, handler, onError, () => {
if (options.onListen) { if (options.onListen) {
options.onListen({ path }); options.onListen(listener.addr);
} else { } else {
console.log(`Listening on ${path}`); console.log(`Listening on ${path}`);
} }
@ -593,19 +593,18 @@ function serve(arg1, arg2) {
listenOpts.port = listener.addr.port; listenOpts.port = listener.addr.port;
} }
const onListen = (scheme) => { const addr = listener.addr;
// If the hostname is "0.0.0.0", we display "localhost" in console // If the hostname is "0.0.0.0", we display "localhost" in console
// because browsers in Windows don't resolve "0.0.0.0". // because browsers in Windows don't resolve "0.0.0.0".
// See the discussion in https://github.com/denoland/deno_std/issues/1165 // See the discussion in https://github.com/denoland/deno_std/issues/1165
const hostname = listenOpts.hostname == "0.0.0.0" const hostname = addr.hostname == "0.0.0.0" ? "localhost" : addr.hostname;
? "localhost" addr.hostname = hostname;
: listenOpts.hostname;
const port = listenOpts.port;
const onListen = (scheme) => {
if (options.onListen) { if (options.onListen) {
options.onListen({ hostname, port }); options.onListen(addr);
} else { } else {
console.log(`Listening on ${scheme}${hostname}:${port}/`); console.log(`Listening on ${scheme}${addr.hostname}:${addr.port}/`);
} }
}; };
@ -625,7 +624,7 @@ function serveHttpOnListener(listener, signal, handler, onError, onListen) {
onListen(context.scheme); onListen(context.scheme);
return serveHttpOn(context, callback); return serveHttpOn(context, listener.addr, callback);
} }
/** /**
@ -641,10 +640,10 @@ function serveHttpOnConnection(connection, signal, handler, onError, onListen) {
onListen(context.scheme); onListen(context.scheme);
return serveHttpOn(context, callback); return serveHttpOn(context, connection.localAddr, callback);
} }
function serveHttpOn(context, callback) { function serveHttpOn(context, addr, callback) {
let ref = true; let ref = true;
let currentPromise = null; let currentPromise = null;
@ -700,6 +699,7 @@ function serveHttpOn(context, callback) {
})(); })();
return { return {
addr,
finished, finished,
async shutdown() { async shutdown() {
if (!context.closing && !context.closed) { if (!context.closing && !context.closed) {

View file

@ -43,7 +43,10 @@ function onListen(
} }
async function makeServer( async function makeServer(
handler: (req: Request) => Response | Promise<Response>, handler: (
req: Request,
info: Deno.ServeHandlerInfo,
) => Response | Promise<Response>,
): Promise< ): Promise<
{ {
finished: Promise<void>; finished: Promise<void>;
@ -432,7 +435,7 @@ Deno.test(async function httpServerRejectsOnAddrInUse() {
Deno.test({ permissions: { net: true } }, async function httpServerBasic() { Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
const ac = new AbortController(); const ac = new AbortController();
const deferred = Promise.withResolvers<void>(); const deferred = Promise.withResolvers<void>();
const listeningDeferred = Promise.withResolvers<void>(); const listeningDeferred = Promise.withResolvers<Deno.NetAddr>();
const server = Deno.serve({ const server = Deno.serve({
handler: async (request, { remoteAddr }) => { handler: async (request, { remoteAddr }) => {
@ -447,11 +450,13 @@ Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
}, },
port: servePort, port: servePort,
signal: ac.signal, signal: ac.signal,
onListen: onListen(listeningDeferred.resolve), onListen: (addr) => listeningDeferred.resolve(addr),
onError: createOnErrorCb(ac), onError: createOnErrorCb(ac),
}); });
await listeningDeferred.promise; const addr = await listeningDeferred.promise;
assertEquals(addr.hostname, server.addr.hostname);
assertEquals(addr.port, server.addr.port);
const resp = await fetch(`http://127.0.0.1:${servePort}/`, { const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: { "connection": "close" }, headers: { "connection": "close" },
}); });
@ -472,7 +477,7 @@ Deno.test(
async function httpServerOnListener() { async function httpServerOnListener() {
const ac = new AbortController(); const ac = new AbortController();
const deferred = Promise.withResolvers<void>(); const deferred = Promise.withResolvers<void>();
const listeningDeferred = Promise.withResolvers<void>(); const listeningDeferred = Promise.withResolvers();
const listener = Deno.listen({ port: servePort }); const listener = Deno.listen({ port: servePort });
const server = serveHttpOnListener( const server = serveHttpOnListener(
listener, listener,
@ -3812,7 +3817,7 @@ Deno.test(
permissions: { run: true, read: true, write: true }, permissions: { run: true, read: true, write: true },
}, },
async function httpServerUnixDomainSocket() { async function httpServerUnixDomainSocket() {
const { promise, resolve } = Promise.withResolvers<{ path: string }>(); const { promise, resolve } = Promise.withResolvers<Deno.UnixAddr>();
const ac = new AbortController(); const ac = new AbortController();
const filePath = tmpUnixSocketPath(); const filePath = tmpUnixSocketPath();
const server = Deno.serve( const server = Deno.serve(
@ -3830,7 +3835,7 @@ Deno.test(
}, },
); );
assertEquals(await promise, { path: filePath }); assertEquals((await promise).path, filePath);
assertEquals( assertEquals(
"hello world!", "hello world!",
await curlRequest(["--unix-socket", filePath, "http://localhost"]), await curlRequest(["--unix-socket", filePath, "http://localhost"]),