Skip to content

fix(http): handle socket errors to prevent EPIPE process crash#10662

Closed
vitingr wants to merge 3 commits intoaxios:v1.xfrom
vitingr:fix/issue-10558-failed-requestes-crashing-node-process-with-epipe
Closed

fix(http): handle socket errors to prevent EPIPE process crash#10662
vitingr wants to merge 3 commits intoaxios:v1.xfrom
vitingr:fix/issue-10558-failed-requestes-crashing-node-process-with-epipe

Conversation

@vitingr
Copy link
Copy Markdown

@vitingr vitingr commented Apr 6, 2026

Problem

When uploading a large body, the server can close the connection before
the client finishes writing. Node.js emits an error event on the socket
with code EPIPE. Since Axios had no error listener on the socket, this
became an uncaughtException and crashed the process on Linux/macOS.

Fixes #10558

Root cause

In lib/adapters/http.js, the handleRequestSocket callback set up
keep-alive but never attached a socket-level error handler.

Solution

Attach a single error listener per socket using a Symbol guard to avoid
leaking listeners on keep-alive connections that are reused across requests.

The error is forwarded via req.destroy(err), which triggers the existing
handleRequestError handler, so the promise rejects cleanly instead of
crashing the process.

Testing

  • Verified crash on Linux (Node.js v24, via Docker)
  • Verified clean rejection after fix on Linux
  • Verified listener presence on Windows via manual EPIPE emission

Notes

  • The fetch adapter is not affected (handled internally by the Fetch API)

  • Windows does not exhibit the crash due to socketErrorListener in
    Node's _http_client, but the fix is still necessary for Linux/macOS
    production environments

    Before

image

After

image

I've used this .js script to test the issue in windows environment:

const http = require("node:http");
const axios = require("axios");

const server = http.createServer((req, res) => {
  req.on("error", () => {});
  res.on("error", () => {});
  setTimeout(() => {
    res.end();
    res.socket.destroy();
  }, 50);
});

async function main() {
  await new Promise((resolve) => server.listen(resolve));
  const endpoint = "http://127.0.0.1:" + server.address().port;
  console.log("Server listening:", endpoint);

  const bigBody = Buffer.alloc(10 * 1024 * 1024, "a");

  const axiosInstance = axios.create();

  axiosInstance.interceptors.request.use((config) => {
    config.transport = {
      request: (options, callback) => {
        const req = http.request(options, callback);

        req.on("socket", (socket) => {
          socket.once("connect", () => {
            setTimeout(() => {
              console.log("\n--- Manually emitting EPIPE on socket ---");

              const errorListeners = socket.listeners("error");
              console.log(
                `Socket has ${errorListeners.length} error listener(s):`,
                errorListeners.map((fn) => fn.name)
              );

              if (errorListeners.length === 0) {
                console.log(
                  "NO error listener found — this would crash the process on Linux!"
                );
              } else {
                console.log(
                  "Error listener found — fix is working correctly."
                );
              }

              socket.emit("error", Object.assign(new Error("write EPIPE"), {
                errno: -32,
                code: "EPIPE",
                syscall: "write",
              }));
            }, 10);
          });
        });

        return req;
      },
    };
    return config;
  });

  try {
    await axiosInstance.post(endpoint, bigBody);
  } catch (e) {
    console.log("\nResult:", e.message);
    console.log("isAxiosError:", e.isAxiosError);
  }

  server.close();
}

main().catch((e) => {
  console.error("Unhandled crash — fix is NOT working:", e.message);
  process.exit(1);
});

Summary by cubic

Prevents Node processes from crashing on Linux/macOS by handling socket error events (e.g., EPIPE) in the Node http adapter. Errors are now forwarded to the active keep‑alive request and cleanly reject in axios instead of triggering uncaughtException.

Description

  • Summary of changes

    • Added a guarded socket error listener via SOCKET_ERROR_HANDLED in the Node http adapter.
    • Tracked the active request per socket with SOCKET_CURRENT_REQUEST and forward errors using currentReq.destroy(err).
    • Cleaned up both guards on socket close.
  • Reasoning

  • Additional context

    • Internal reliability fix in lib/adapters/http.js; no API changes. fetch adapter is unaffected.

Testing

  • No tests added.
  • Recommend an adapter test that simulates a mid‑upload socket error on a reused keep‑alive socket and asserts the promise rejects with the socket error (e.g., EPIPE) without process crash.

Written for commit 85b4662. Summary will update on new commits.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Confidence score: 3/5

  • There is a concrete behavior risk in lib/adapters/http.js: the socket error listener closes over the first request, so on keep-alive socket reuse an error may be routed to a stale request instead of the current one.
  • This is a medium-severity, medium-confidence issue (5/10, 6/10) with potential user-facing impact in error propagation, which makes merge risk non-trivial but not clearly blocking.
  • Pay close attention to lib/adapters/http.js - socket error handling on reused connections may target the wrong request context.
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:890">
P2: Socket error listener is attached only once per socket but captures the first request in its closure, so errors on reused keep‑alive sockets can be forwarded to a stale request instead of the active one, preventing proper rejection and retaining the old request.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/adapters/http.js Outdated
@vitingr vitingr force-pushed the fix/issue-10558-failed-requestes-crashing-node-process-with-epipe branch from 3981468 to 8d7a518 Compare April 6, 2026 12:26
@vitingr
Copy link
Copy Markdown
Author

vitingr commented Apr 6, 2026

@cubic-dev-ai The stale closure issue has been addressed.
The fix now stores the active request on the socket itself via
socket[SOCKET_CURRENT_REQUEST], which is updated on every
req.on('socket', ...) callback. The error handler reads this
reference at error-time rather than closing over the original req,
ensuring correct rejection on keep-alive reused connections.

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 6, 2026

@cubic-dev-ai The stale closure issue has been addressed.
The fix now stores the active request on the socket itself via
socket[SOCKET_CURRENT_REQUEST], which is updated on every
req.on('socket', ...) callback. The error handler reads this
reference at error-time rather than closing over the original req,
...

@vitingr This updated approach effectively solves the stale closure problem. By moving the request reference to a property on the socket (socket[SOCKET_CURRENT_REQUEST]) and updating it every time the socket event fires, you've ensured that the singleton error listener always acts on the active request context.

Using Symbols is the right choice here to avoid name collisions on the socket object, which is especially important since these sockets are often managed by a global or shared http.Agent. This implementation safely handles the lifecycle of keep-alive connections where the same socket instance is reused across multiple Axios calls.

One detail to note: since SOCKET_CURRENT_REQUEST is only deleted on close, an error occurring on an idle socket sitting in the connection pool will still trigger currentReq.destroy(err) on the last request that used it. This is perfectly acceptable as it still prevents the process crash (the primary goal) and calling destroy on a completed request is a no-op in Node.js.

You can find more about how I track these types of patterns in my documentation or view your team's saved repository learnings.

@vitingr
Copy link
Copy Markdown
Author

vitingr commented Apr 6, 2026

@cubic-dev-ai please re-review the entire PR please.

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 6, 2026

@cubic-dev-ai please re-review the entire PR

@vitingr I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

@jasonsaayman jasonsaayman added priority::high A high priority issue commit::fix The PR is related to a bugfix commit::perf The PR is related to performance labels Apr 6, 2026
@jasonsaayman
Copy link
Copy Markdown
Member

Thanks closing in favour of #10576

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

Labels

commit::fix The PR is related to a bugfix commit::perf The PR is related to performance priority::high A high priority issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed requests can sometimes crash the Node.js process with EPIPE.

2 participants