fix(http): preserve response in error when stream is aborted after headers#10708
Conversation
…aders When a server sends response headers but aborts the stream before completing the body, axios now attaches the response object to the error. This allows consumers to access response metadata (status, headers) for debugging, retry logic, or user messaging. Fixes axios#6935
There was a problem hiding this comment.
1 issue found across 1 file
Confidence score: 4/5
- This PR is likely safe to merge, but there is a moderate consistency risk from how
AxiosErroris built inlib/adapters/http.js. - In
lib/adapters/http.js, attachingresponseafter constructingAxiosErrorskips constructor normalization ofstatus, which can produce inconsistent error objects and affect downstream error handling. - Pay close attention to
lib/adapters/http.js- ensureAxiosErroris initialized in a way that preserves normalizedstatusand consistent error shape.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/adapters/http.js">
<violation number="1" location="lib/adapters/http.js:826">
P2: Response is attached to AxiosError after construction, bypassing constructor normalization that sets `status`, causing inconsistent error objects.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
jasonsaayman
left a comment
There was a problem hiding this comment.
Thanks for picking this up. Please check the feedback:
1. err.status is left undefined (correctness bug)
AxiosError's constructor normalises status from response:
if (response) {
this.response = response;
this.status = response.status;
}
(lib/core/AxiosError.js:49-52)Both patched paths construct the error without response, then assign err.response = response afterwards. That bypasses the normalisation, so err.status stays undefined even though err.response.status is populated. AxiosError.toJSON() serialises the status directly, so logged/serialised errors will be inconsistent.
The convention already used elsewhere in this same file (see line 930) is to pass the response as the 5th argument:
responseStream.on('aborted', function handlerStreamAborted() {
if (rejected) return;
const err = new AxiosError(
'stream has been aborted',
AxiosError.ERR_BAD_RESPONSE,
config,
lastRequest,
response
);
responseStream.destroy(err);
reject(err);
});
responseStream.on('error', function handleStreamError(err) {
if (req.destroyed) return;
reject(AxiosError.from(err, null, config, lastRequest, response));
});This removes the post-hoc assignment, sets status correctly, and drops the now-unnecessary if (response) guard (which is unreachable anyway, response is const-declared at line 839 in the same closure, before the listeners are attached).
2. Please add a regression test
This closes a long-standing issue (#6935), and there are no tests. At a minimum, one test per path:
- aborted: server writes headers, then
res.destroy()mid-body → asserterr.response.status,err.response.headers, anderr.status. - error: same setup with an emitted stream error → same assertions.
Asserting err.status in particular would have caught issue #1.
3. Scope clarification (optional, for the PR description)
This fix only applies to buffered responses. When responseType: 'stream', the adapter takes the early branch at line 847, and these listeners are never attached. Worth calling out so users with streamed responses don't expect the same behaviour. Also worth noting that err.response.data will be undefined on the aborted path since data is only assigned in the 'end' handler; only metadata (status/headers) survives, which matches the stated intent.
Once 1 and 2 are addressed, I think this is good to go. Patch-level change, no breaking concerns.
Thanks, addressed in 47d5c57. Changes made:
Validated locally:
Scope note: this remains limited to buffered responses; |
What
When a server sends response headers but aborts the stream before completing the body, axios now attaches the
responseobject to the thrownAxiosError. This allows consumers to access response metadata (status,headers) for debugging, retry logic, or user messaging.How
Modified the
abortedanderrorevent handlers inlib/adapters/http.jsto attach theresponseobject to the error before rejecting.Testing
The fix can be tested with the reproduction case from the issue:
res.destroy()err.response— now populatedCloses #6935
Summary by cubic
Preserves the Node
httpadapter’s response on stream aborts and decode errors after headers are sent so callers can read status and headers.error.statusnow mirrors the response status.Description
responsetoAxiosErroronabortedand streamerrorevents in the Nodehttpadapter.error.responseis set anderror.status === response.status.v1.x; no behavior changes beyond error shape.Docs
/docs/for Node error handling: when a response stream is aborted or decoding fails after headers,axiosincludeserror.response(status, headers) and setserror.statusto the response status.Testing
error.response.status === 206,x-stream-error: yes, anderror.status === 206.error.response.status === 200,x-stream-aborted: yes, anderror.status === 200.Semantic version impact
Written for commit 1dfadfa. Summary will update on new commits. Review in cubic