denoland/std

Provide a way to tell when the server is ready

sholladay opened this issue · 5 comments

Is your feature request related to a problem? Please describe.

When making a web server with std/http and listening with the server.listenAndServe() function of the Server class, It is difficult to write code for logs and tests (etc) that rely on the server being ready, because readiness is never signaled and the promise never resolves while the server is running.

import { Server } from 'https://deno.land/std@0.132.0/http/server.ts';

const server = new Server({
    handler() {
        return new Response('Hello, World!');
    },
    port : 3000
});

await server.listenAndServe();
await fetch('http://localhost:3000');  // <-- This line is never reached
import { Server } from 'https://deno.land/std@0.132.0/http/server.ts';

const server = new Server({
    handler() {
        return new Response('Hello, World!');
    },
    port : 3000
});

server.listenAndServe();               // <-- Doesn't wait for server to be ready
await fetch('http://localhost:3000');  // <-- Prone to race conditions

Describe the solution you'd like

I would like the promise returned by Server#listenAndServe() (and functions that use it) to resolve when the server is listening (i.e. when it is ready for requests) as opposed to never resolving, which isn't useful.

Describe alternatives you've considered

Having a callback separate from the promise resolution, as proposed in #2047 is an alternative approach, but IMO that is less desirable.

The recommended way i think is to make the handler function outside of the server, then for testing you just use new Request as the argument for it. Here's how dotland for does it https://github.com/denoland/dotland/blob/3ec18e8a1e1de84432213ca6befdc15a287896ce/worker/handler_test.ts

I also use that technique for testing. Pogo has server.inject() to do requests without an actual fetch() call. It's great in most cases. However, for end-to-end tests and other scenarios, we're going to need to use the network. It's important that we be able to tell when the server is ready for requests. For reference, Node.js has had events and listen callbacks for this even since the early days.

@sholladay what about this code? I think it could be a workaround for what you need:

import type { ConnInfo } from "./vendor/deno.land/std@0.136.0/http/server.ts";
import { serveListener } from "./vendor/deno.land/std@0.136.0/http/server.ts";

const handler = (_request: Request, _connInfo: ConnInfo) => {
  return new Response("Hello world!");
};

const listener = Deno.listen({ port: 8000 });
serveListener(listener, handler);

// The listener and the server are ready, do your fetch/requests here ...

Oak does the same, after using Deno.listen and initializing the Application. A event listen is fired:

https://github.com/oakserver/oak/blob/main/application.ts#L546
https://github.com/oakserver/oak/blob/c4fe8b35c0362b9b153e1cf8af4081c7a3905834/http_server_native.ts#L91

Hope it helps 😄

@sant123 serveListener(), like the other functions I mentioned, also returns a promise, which your code is ignoring.

As I understand it ...

serveListener(...)
fetch(...)

... is prone to race conditions. But I could be mistaken. Either way, it's unusual to treat an async function as having a synchronous result.

Yeah that makes sense, however don't forget that Deno.listen does not return a promise and from there is already listening for new connections. The serveListener just adds the handler for http requests so when doing a fetch request it will work and race conditions will not be present. Could you try my code above and let me know if it works?