Skip to content

feat: support concurrent requests against a shared server (fixes #726) - #895

Open
viniciusamc wants to merge 1 commit into
forwardemail:masterfrom
viniciusamc:feat/concurrent-requests
Open

feat: support concurrent requests against a shared server (fixes #726)#895
viniciusamc wants to merge 1 commit into
forwardemail:masterfrom
viniciusamc:feat/concurrent-requests

Conversation

@viniciusamc

Copy link
Copy Markdown
Contributor

Problem

Fixes #726.

When multiple requests share a server that supertest started (request(server) with a non-listening server, or request.agent(app)), the first request to finish closes the server while the others are still in flight — they die with ECONNRESET/ECONNREFUSED/EHOSTUNREACH. This is the flake reported in #726 (and the "2-3 flakes per run in a ~2000-test suite" comment there).

The root cause is in lib/test.js: only the Test that called listen(0) holds this._server, and its end() unconditionally closes the server as soon as its own response arrives.

Fix

Replace "first request owns and closes the server" with in-flight counting:

  • serverAddress(): when supertest starts the server it marks it with an in-flight counter; every Test sharing that server increments it and keeps the reference.
  • end(): decrements exactly once per test (guarded, since end() can run twice via .expect(status, fn) + await) and closes the server only when the counter reaches zero.

Servers the user started are never counted and never closed, as before. Single-request behavior is unchanged: the counter goes 1→0 in that request's own end(), closing the server before assertions exactly as today.

New API: .concurrently(n, build)

With the lifecycle fixed, concurrent testing (races, idempotency) becomes a first-class use case, so this also adds an ergonomic entry point:

const [res1, res2] = await request(app)
  .concurrently(2, (r, i) => r.post('/payments').send({ amount: 10 }));

It runs n requests against one shared server (a function app is wrapped in a single server, instead of one per request) and resolves with the responses in build order. Plain Promise.all over hand-built requests works too — the method just removes the boilerplate.

Tests

  • Regression test for Concurrent requests result in ECONNREFUSED #726: two requests racing on one shared server, the slower one still in flight when the faster finishes. Fails with read ECONNRESET without the fix.
  • Genuine-concurrency test: the handler only responds once all requests have arrived, so it deadlocks unless requests truly run in parallel.
  • User-started servers are left running; argument validation; per-request assertions.

Known trade-off: a Test that is built but never dispatched now holds its slot, keeping the shared server open (previously this leak existed only when the abandoned test was the server-starting one).

Checklist

  • I have ensured my pull request is not behind the main or master branch of the original repository.
  • I have rebased all commits where necessary so that reviewing this pull request can be done without having to merge it first.
  • I have written a commit message that passes commitlint linting.
  • I have ensured that my code changes pass linting tests.
  • I have ensured that my code changes pass unit tests.
  • I have described my pull request and the reasons for code changes along with context if necessary.

@viniciusamc
viniciusamc force-pushed the feat/concurrent-requests branch from c1e2120 to fff316b Compare August 4, 2026 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent requests result in ECONNREFUSED

1 participant