Skip to content

7.3.1: streaming requests (.write) against an app factory hit port 80 (ECONNREFUSED) #911

Description

@doberkofler

Description

supertest 7.3.1 regresses streaming requests created with .write() when built from an app factory (request(app)), where supertest starts its own ephemeral server. The underlying request is created against http://127.0.0.1/path (default port 80) instead of the actual ephemeral port, producing ECONNREFUSED. 7.3.0 works.

Environment

  • supertest: 7.3.1 (7.3.0 works)
  • superagent: 10.3.0
  • Node: 26.10.0 (macOS/darwin)

Root cause

After #907 (v7.3.1), serverAddress() returns a portless placeholder URL when app.address() is null (the loopback bind is resolved asynchronously) and defers the real URL to end() / dispatch():

// lib/test.js (7.3.1)
if (!addr) {
  this._serverPath = path;
  const protocol = app instanceof Server ? 'https' : 'http';
  return protocol + '://127.0.0.1' + path;   // no port yet
}
// ...
dispatch = () => {
  removeStartListeners();
  try {
    if (this._serverPath !== undefined) {
      this.url = this.formatServerUrl(server, server.address(), this._serverPath);
      this._serverPath = undefined;
    }
    super.end(finish);
  } ...

However, superagent's streaming API builds the request object eagerly, before end() runs:

  • superagent/lib/node/index.js:406 — write() calls this.request()
  • superagent/lib/node/index.js:656 — request() returns the cached this.req if already created

So the request connects to port 80 immediately; the later this.url correction in dispatch() has no effect on the already-created request.

Reproduction

const express = require('express');
const request = require('supertest');

const app = express();
app.post('/f', (_req, res) => res.send('ok'));

const test = request(app).post('/f');
test.set('Content-Type', 'multipart/form-data; boundary=foo');
test.write('--foo\r\n');
test.write('Content-Disposition: form-data; name="user_name"\r\n\r\nTobi\r\n--foo--');

test.end((err, res) => {
  console.log('err:', err && err.message, 'status:', res && res.status);
});

Result:

  • 7.3.1: err: ECONNREFUSED: Connection refused status: undefined
  • 7.3.0: err: null status: 200

Non-streaming styles are unaffected: .send(Buffer), .field()/.attach() and plain .get()/.post() work.

Suggested fix

Make the real URL available before a caller can create the request, or make write()/pipe() wait for listening (as end() already does), so the deferred-URL mechanism is not observable to superagent's eager request creation.

Impact

Blocks upgrading to 7.3.1 for anyone using the raw streaming API (.write()/.pipe()); only a test rewrite to .send(Buffer) or .field()/.attach() is available as a workaround.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions