guardedFetch now hands the preflight's validated DNS answer to the connect-time lookup instead of resolving the same host again, halving DNS queries per hop.
Stacked on #18.
Why
Every hop resolved its host twice: once in assertUrlIsSafeToFetch and again in the guarded dispatcher's connect.lookup. Both answers come from the URL owner's nameserver, so each request cost twice the DNS work it needed. A slow or hostile nameserver got two chances per hop to stall it, up to 12 with the default 5 redirects. The second resolution also gave a rebinding nameserver a second answer to vary, which the connect-time check exists to catch.
Approach
The connector only sees { hostname, port, ... }, with no per-request context. Passing the validated answer through AsyncLocalStorage around the fetch call should let the connect-time lookup reuse it without changing undici's connector contract.
validateFetchTarget(internal) runs the same checks asassertUrlIsSafeToFetchand also returns the resolved addresses. The public function's return shape is unchanged.executeFetchruns the fetch insidewithPinnedAddresses(hostname, addresses). Each redirect hop pins its own preflight answer.createGuardedLookupuses the pinned answer when one exists for that hostname, narrowed to the requested family. It still runs every connect-time check on it, and falls back to resolving when nothing usable is pinned.
Why this is safe:
- The socket connects only to addresses that passed both checks, which is stricter than today, where the connect-time answer could differ from the preflight answer.
- If undici opens the connection in a different async context, the lookup either resolves on its own (today's behavior) or sees another request's pin. That pin can only be for the same hostname, since pools are per origin, and was validated too.
- Direct users of
createGuardedLookupor the guarded dispatchers are unaffected: nothing is pinned, so they resolve as before.
Validation
- New
src/resolve-once.test.tsruns real HTTP through a fresh guarded dispatcher, withresolveHostmocked and loopback treated as public:- one fetch → one resolution (was two)
- a redirect chain → one resolution per hop
- plain undici fetch through the same dispatcher still resolves at connect time
- a pinned private address is still rejected with
hostname_unsafe, without resolving - unit tests for family narrowing and hostname matching
- The three behavioral tests fail with the pinned lookup disabled.
pnpm validate: typecheck, lint, and format clean; 285 tests passing.
Activity
2This pull request is a draft
Merging is unavailable until it is marked ready for review.