fix(proxy): log why a connect failed instead of an empty line - #183
Open
ak2k wants to merge 1 commit into
Open
Conversation
Node's happy-eyeballs dialer (`autoSelectFamily`, on by default across the versions this package supports; `package.json` declares `node >=20`, measured here on 24) reports a connect where every address failed as an AggregateError. Node builds that error with an empty `message`; the per-address reasons are in `.errors`. A refused `localhost:1`, both ::1 and 127.0.0.1, gives name: AggregateError, message: '' (empty), code: 'ECONNREFUSED' errors: ['connect ECONNREFUSED ::1:1', 'connect ECONNREFUSED 127.0.0.1:1'] The upstream is multi-address, so that is the ordinary shape of a real network event, and both sites that report one printed `err.message`. An outage produced pages of [TeamClaude] Upstream error (account "alice"): with nothing after the colon, at the moment the log is worth reading. One helper, used at every connect-failure log rather than copied into each, since copies would drift. All of them printed `err.message`: the proxied path, the HTTP forward proxy, both Remote Control relays, the raw relay and the MITM tunnel. Its `err.message` fallback is required: with the dialer off, and on every single-address failure, the reason arrives as a plain Error in `message`. It also looks one level down, because `TEAMCLAUDE_UPSTREAM_GLOBAL_FETCH` is a supported path and changes the shape: global fetch wraps the same failure in a TypeError whose own message is the equally uninformative "fetch failed", with the AggregateError in `.cause`. Without that, the escape hatch trades a blank line for "fetch failed". The 502 body interpolates the same expression, on the same error, in the same catch, so it is converted with them rather than left as the one inconsistency a reader has to ask about. It is not held by a test and is not claimed as a fix: the failures that reach this catch today are classified transient a few lines above it, so they destroy the connection and never get as far as the 502. What would reach it is a multi-address host failing with a code outside that list, and simulating one would prove nothing about the real path. Seven tests, each establishing the shape it is working with before asserting anything about the text, because a single-address failure carries its reason in `message` and would pass against the unfixed code either way. The ones that need an aggregated failure skip when the host cannot produce one, since an IPv4-only machine is not a defect in this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a connect to the upstream fails, the log line that should say why is empty. The same request against the same refused upstream, before and after, quoted so the trailing space is visible:
Why
Node's happy-eyeballs dialer (
autoSelectFamily, on by default across the versions this package supports;package.jsondeclaresnode >=20, measured here on 24) reports a connect where every address failed as anAggregateError. Node builds that error with an emptymessage; the per-address reasons are in.errors. Dialinglocalhost:1:Every connect-failure log printed
err.message. The upstream is multi-address, so the ordinary failure printed nothing.The fix
One exported helper, used at every connect-failure log in
server.jsand at the MITM tunnel's inmitm.js:The
|| err.messagefallback is required, not defensive: withautoSelectFamily: false, and on every single-address failure, the reason arrives as a plainErrorinmessage.The
.causedescent coversTEAMCLAUDE_UPSTREAM_GLOBAL_FETCH, where global fetch wraps the same failure in aTypeErrorwhose own message isfetch failed:The client-visible 502 body interpolates the same expression in the same catch, so it is converted too. That site is not held by a test and fixes nothing observable today: a refused multi-address connect carries
code: 'ECONNREFUSED', which is classified transient a few lines above, so the connection is destroyed before that 502 is built. Measured on master, the client gets a transport-levelECONNRESET, not a blank 502.Tests
Seven, in
test/connect-error-message.test.js, all against a real refused connect rather than a constructed error. Each establishes the error shape it received before asserting on the text, because a single-address failure carries its reason inmessageand would satisfy a substring check against the unfixed code; the end-to-end tests assert all the per-address reasons, joined.Where the shape depends on the host, tests skip rather than assert: an IPv4-only or IPv6-disabled machine resolves
localhostto one address and never produces the aggregated shape. The shape is probed once at load and the five dependent tests are gated on it; the two that hold the fallback always run.The end-to-end tests cover the proxied path, the MITM tunnel and the HTTP forward proxy. The remaining conversions are the same one-word substitution on handlers of the same shape and are not separately anchored.
525 before, 532 after.
npx eslint .clean.Compatibility
Log and error text only. No routing, selection, retry or transport changes; every failure that produced a line before produces one now, with the reason filled in.