server - uhop/tape-six GitHub Wiki

tape-six/server

HTTP server harness for tests: spin up a real node:http server on an ephemeral port, run your client code against it, and tear it down reliably — plus a request recorder for asserting what your code sent over the wire. Works on Node, Bun, and Deno.

Not to be confused with tape6-server, the CLI web server that hosts browser-based test runs. This page is about the tape-six/server.js module you import inside test files to create HTTP fixtures.

import {withServer, setupServer, startServer, record} from 'tape-six/server.js';

Quick start

import test from 'tape-six';
import {withServer, record} from 'tape-six/server.js';

test('client sends the right requests', async t => {
  const rec = record(); // records every request, answers 204
  await withServer(rec, async base => {
    await fetch(base + '/upload', {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify({a: 1})
    });
    t.equal(rec.requests.length, 1, 'one call made');
    t.match(rec.requests[0], {method: 'POST', url: '/upload', body: '{"a":1}'});
  });
});

port: 0 (the default) asks the OS for a free port, so parallel test files get collision-free servers by construction.

API

withServer(serverHandler, clientHandler, opts?)

Scoped resource for a single test: creates an http.Server with serverHandler, starts it, runs clientHandler(base, lifecycle), and closes the server in finally — cleanup runs whether clientHandler resolves, rejects, or throws. Returns the clientHandler result.

  • serverHandler — a standard node:http request listener (req, res). Node calls it once per incoming request.
  • clientHandler(base, lifecycle) — the test body; base is the bound URL, e.g. "http://127.0.0.1:54321".
  • opts{host = '127.0.0.1', port = 0}.

setupServer(serverHandler, opts?)

Suite-shared variant: registers beforeAll to start the server and afterAll to close it, and returns a frozen context with live getters — server, base, port, host. Don't destructure it at module load: the properties read the running server at access time, and destructuring captures stale undefined values.

Per-test state reset stays on your side (compose your own beforeEach); setupServer owns the lifecycle, the caller owns state.

startServer(server, opts?)

The procedural primitive under both helpers: takes an existing http.Server, starts listening, and resolves to a lifecycle handle {server, base, port, host, close}. It races 'listening' against 'error', so a busy port or EACCES rejects instead of hanging. close() is idempotent and calls server.closeAllConnections() (when available) so keep-alive sockets don't delay teardown.

record(handler?)

Recording wrapper: returns a request listener that captures every request it serves onto its own requests array, then answers 204 — or delegates to handler when one is given.

Each entry is a plain object:

interface RecordedRequest {
  method: string;
  url: string;
  headers: Record<string, string | string[] | undefined>; // lower-cased names
  body: string; // eagerly buffered UTF-8 text; '' when empty
}
  • Eager by design: the body is fully read before anything responds, so entries are always complete plain objects — assert with t.match and t.any wildcards.
  • Delegates read entry.body: a handler(req, res, entry) receives the captured entry as its third argument; the req stream is already drained — don't try to read it again.
  • Reset between tests with rec.requests.length = 0 (e.g. in a beforeEach when paired with setupServer).
const rec = record((req, res, entry) => {
  res.setHeader('content-type', 'text/plain');
  res.end('len:' + entry.body.length);
});

Cross-runtime notes

  • The harness is built on node:http, which Node, Bun, and Deno all provide — one implementation, three CLI runtimes. It is not for browsers: keep tests that import it under a CLI-only pattern (the cli test set in your tape6 config) so browser runs don't pick them up. See Set up tests.
  • The default host is an explicit '127.0.0.1' rather than 'localhost', avoiding dual-stack surprises on macOS where localhost may resolve to ::1.

Troubleshooting

  • A test hangs waiting for the server — it won't: startServer rejects on 'error' (port busy, permissions). If you pass a fixed port, prefer 0 unless the test genuinely needs a known port.
  • setupServer values are undefined — the context was destructured at module load, before beforeAll ran. Keep the object and read ctx.base inside tests.
  • A record() delegate sees an empty request stream — expected: the body was buffered eagerly; read entry.body instead.
  • Teardown is slow with keep-alive clientsclose() already force-closes connections on runtimes that support closeAllConnections(); on others, make sure clients don't hold sockets open past the test.

See Also

⚠️ **GitHub.com Fallback** ⚠️