Skip to content

fix(http): timeout not honored during TCP connect phase when maxRedirects=0 #10818

@jasonsaayman

Description

@jasonsaayman

Describe the bug

With the http adapter and maxRedirects: 0, configured timeout values are ignored while the TCP connect phase is still pending. If the host's SYN is blackholed or the address is unroutable, the request only settles when the kernel's TCP retry gives up — roughly 5s on macOS, longer on Linux — regardless of how short timeout is.

maxRedirects > 0 is unaffected, because the follow-redirects transport starts its own timer at request construction.

Reproduction

Tested against current v1.x HEAD:

import axios from 'axios';

// 192.0.2.1 is TEST-NET-1 (RFC 5737); kernel drops the SYN.
const url = 'http://192.0.2.1/';

async function attempt(label, config) {
  const t0 = Date.now();
  try { await axios.get(url, config); }
  catch (err) {
    console.log(`[${label}] ${Date.now() - t0}ms code=${err.code}`);
  }
}

await attempt('maxRedirects=0, timeout=500',  { maxRedirects: 0, timeout: 500 });
await attempt('maxRedirects=0, timeout=2000', { maxRedirects: 0, timeout: 2000 });
await attempt('maxRedirects=5, timeout=500',  { maxRedirects: 5, timeout: 500 });

Output (Node 24, macOS):

[maxRedirects=0, timeout=500]  5006ms code=ECONNABORTED
[maxRedirects=0, timeout=2000] 5002ms code=ECONNABORTED
[maxRedirects=5, timeout=500]   506ms code=ECONNABORTED

Expected

The configured timeout should fire during the connect phase too, not only after the socket is established.

Root cause

ClientRequest.setTimeout(ms, cb) from Node's native http module arms a socket-inactivity timer. That timer doesn't tick until the socket is bound and connected. If connect never completes, the timer never starts and the reject is driven by whatever connect error the kernel surfaces.

follow-redirects runs its own wall-clock timer that starts at request construction, which is why maxRedirects > 0 honors the configured value immediately.

Suggested fix

For the maxRedirects === 0 path in lib/adapters/http.js, attach a wall-clock setTimeout(config.timeout) from the moment the request is created, fire the existing timeout error when it elapses, and clear it on close / response / successful settle. The existing message and code resolution can be reused so timeoutErrorMessage and transitional.clarifyTimeoutError keep working.

Related

Axios version

v1.x HEAD (verified at 25387ae)

Adapter

http

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