Skip to content

[Hackathon] Target URL preflight leaves DNS lookup outside its timeout and abort handling #502

Open

Description

@lxcario

Verified against (commit SHA or released version)

988c035 (v0.14.0), Node 24.19.0, Windows 11

Discord Username / User ID

resquwue

What does this improvement do?

The pre-charge --target-url reachability check awaits DNS before creating its 8-second HTTP timeout or observing shutdownSignal. A stalled resolver therefore leaves probeTargetUrl pending beyond the stated preflight budget. Aborting the supplied signal while DNS is pending also does not settle the operation.

This can stall a run command before its trigger request on a machine with a slow/broken resolver. The HTTP timeout cannot help because fetch has not started. I reproduced both outcomes using the public DNS injection seam in the real built CLI module, with no external network or paid test runs.

Details / implementation notes

Offline reproduction

Build this commit with npm ci and npm run build. Save as repro-dns.mjs at the repository root and run node repro-dns.mjs:

import { probeTargetUrl } from './dist/lib/target-url-preflight.js';
import { InterruptError } from './dist/lib/errors.js';
const controller = new AbortController();
let settled = false, fetchCalls = 0;
const start = performance.now();
const probe = probeTargetUrl('https://example.test', {
  dnsLookup: () => new Promise(() => {}),
  fetchImpl: async () => { fetchCalls++; return new Response('ok'); },
  shutdownSignal: controller.signal,
  proxyActive: false,
}).then(() => { settled = true; }, () => { settled = true; });
controller.abort(new InterruptError('SIGINT'));
await new Promise(r => setTimeout(r, 100));
console.log('after abort', { settled, aborted: controller.signal.aborted, fetchCalls });
await Promise.race([probe, new Promise(r => setTimeout(r, 8100))]);
console.log('after budget', { settled, fetchCalls,
  elapsedMs: Math.round(performance.now() - start) });

Actual on my machine:

{"afterAbort":{"settled":false,"aborted":true,"fetchCalls":0},"afterPreflightBudget":{"elapsedMs":8219,"settled":false,"fetchCalls":0}}

The unresolved fake DNS promise models a resolver that has not answered. It creates no sockets and does not keep the probe process alive. A bounded real-resolver delay has the same control-flow omission until the lookup settles. This repro demonstrates the preflight module contract; it does not claim the process signal handler can never force-exit the CLI.

Expected: DNS is included in a bounded preflight lifetime. A DNS timeout should follow the existing narrow refuse-versus-advisory policy; caller interruption should propagate promptly rather than wait for DNS or be converted into an advisory. No trigger should be dispatched after interruption.

Root cause and proposed scope

In src/lib/target-url-preflight.ts, probeTargetUrl awaits dnsLookup(host) at line 152. The shutdown signal, critical-operation tracking and AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS) are only established for the later HTTP phase (lines 202 onward). The current timeout therefore bounds HTTP only, leaving DNS unbounded.

Propose a bounded, interruptible DNS phase using the existing shutdown/deadline conventions, with timer/listener cleanup and explicit handling of a late resolver result/rejection. Account for the fact that an underlying system lookup may not itself be cancellable. Preserve literal-IP DNS skipping, existing network classification and --skip-preflight behavior. No runtime dependency addition is proposed.

Regression cases: never-settling DNS hits the deadline; interrupt and an already-aborted signal settle promptly without fetch/trigger calls; late DNS resolution/rejection cannot start HTTP or cause an unhandled rejection; successful DNS and existing advisory/refusal cases remain unchanged. Use fake timers for fast deterministic tests, then lint/format/typecheck/full tests/coverage >=80%/build/local e2e for the eventual PR.

Duplicate check

Searched the current open/closed issue and PR registry through #501. #397 is classification of the environment API preflight timeout. #403/#406 concern an HTTP retry backoff losing its caller signal. #437 concerns run/plan polling transport retries. #390 concerns DNS64 address-family fallback. This report is specifically the DNS phase of the target URL reachability probe, before any HTTP request or retry has begun.

No production source changes have been made. Implementation will wait for maintainer triage and assignment.

Season 4 CLI Improvement Bonus

  • Entering the Season 4 CLI Improvement Bonus; Discord: resquwue.

Confirmations

  • I have searched existing issues and this is not a duplicate.
  • I have provided my Discord identity above for reward coordination.

Activity

  1. fyZhang66 commented on Oct 20, 2026

    @fyZhang66
    Contributor

    Confirmed on v0.15.0, thanks @lxcario. probeTargetUrl awaits the DNS lookup before creating the 8 s timeout and before observing the shutdown signal. Accepted, scope:

    • Arm the timeout and the abort before the lookup, and make the lookup itself abortable (signal-aware, or raced against the timeout).
    • Tests (must fail before the fix): a stalled resolver → the preflight fails within its budget; an abort during the lookup settles immediately.

    No new dependency. Comment /assign to claim it.

  2. added
    acceptedTriaged and accepted — a PR for this will be reviewed
    and removed
    needs-triageNew issue/PR awaiting first triage
    on Oct 20, 2026
  3. lxcario commented on Oct 20, 2026

    @lxcario
    ContributorAuthor

    /assign

  4. testsprite-hob commented on Oct 20, 2026

    @testsprite-hob

    Assigned to @lxcario. Thanks for taking this on. (2 more slots available.)

    Your open assigned issues (1/3):
    (none)

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

Metadata

Metadata

Assignees

Labels

acceptedTriaged and accepted — a PR for this will be reviewedhackathonCLI hackathon submissionsin-progressAssigned and actively being worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions