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.
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 againsthttp://127.0.0.1/path(default port 80) instead of the actual ephemeral port, producingECONNREFUSED. 7.3.0 works.Environment
Root cause
After #907 (v7.3.1),
serverAddress()returns a portless placeholder URL whenapp.address()isnull(the loopback bind is resolved asynchronously) and defers the real URL toend()/dispatch():However, superagent's streaming API builds the request object eagerly, before
end()runs:superagent/lib/node/index.js:406—write()callsthis.request()superagent/lib/node/index.js:656—request()returns the cachedthis.reqif already createdSo the request connects to port 80 immediately; the later
this.urlcorrection indispatch()has no effect on the already-created request.Reproduction
Result:
err: ECONNREFUSED: Connection refused status: undefinederr: null status: 200Non-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 forlistening(asend()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.