Skip to content

Name bridge transport failures without the bridge URL and close its other exits (B04) - #19

Closed
AsyncAssassin wants to merge 2 commits into
mainfrom
fix/bridge-transport-errors
Closed

AsyncAssassin wants to merge 2 commits into
mainfrom
fix/bridge-transport-errors

Conversation

@AsyncAssassin

@AsyncAssassin AsyncAssassin commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

B04 from the merged review of v0.4.0; 0.4.1 is released once all fixes are merged. The HTTP bridge has no credential setting of its own, so a bridge token can only live in the path or the query of base-url. Every transport failure, such as a refused connection or a timeout, passed Spring's ResourceAccessException text on, and that text quotes the request URL. The path reached the health details, the WARN log, and the lastError of sync runs that the READ role sees.

  • Fixed messages: a transport failure names the kind of the I/O error RestClient wraps (timeout, cannot connect, unknown host, TLS, I/O) with its class, such as Provider transport failure: cannot connect (ConnectException). Any other failure of the request says Provider request failed (<class>). The exception carries no cause, because the cause quotes the URL.
  • Diagnosis: the WARN line http_provider_page_fetch_failed adds the cause chain with every URL cut out, such as ResourceAccessException: I/O error on GET request for "<url>": Connection refused.
  • userinfo: HttpURLConnection never sends a user name or password from the URL, so one in base-url only looked configured, and a bridge that needs it could only answer 401. It now stops startup with a message that leaves the URL out. This is marked Breaking in CHANGELOG.md; a header credential for the bridge belongs to the bridge contract in batch 2.
  • Loggers: at DEBUG and TRACE, the JDK's HttpURLConnection and Spring's URI parser printed the URL with its token. Both are pinned to INFO next to the existing org.springframework.web.client pin.
  • On the same catch path:
    • a null element in events is a data error for its address, not a provider outage;
    • a Retry-After beyond Instant's range is ignored instead of turning the 429 into a failed request without its backoff.

docs/api.md, docs/architecture.md, docs/failure-modes.md, the README, and docs/testing.md describe it. CHANGELOG.md lists the changes under [Unreleased] → Security, Changed, and Fixed.

Verification

  • ./gradlew clean check: 279 tests, 0 failures, 2 skipped (the env-gated Alchemy live smoke).
  • HttpChainProviderTransportFailureTests builds the bridge client with ProviderConfiguration, as in production, with a token in the path and the query of base-url:
    • a closed port gives cannot connect and no cause. The health error carries no token, and the WARN line has the cause chain with "<url>" in place of the URL;
    • a socket that accepts and never answers gives timeout;
    • the JDK HttpClient, which wraps the ConnectException around a ClosedChannelException, gets the same label;
    • a base-url with a user name or password is refused without quoting it.
  • HttpChainProviderHealthIndicatorTests: a page with "events":[null] fails as a data error with health UP, and a 429 with Retry-After: 99999999999999999 stays a rate limit.
  • BridgeCredentialsE2ETests boots e2e with such a base-url on a real Tomcat. It registers and syncs an address with the root log level at TRACE, then reads the run as READ: lastError is the fixed message, and neither the health details nor the captured log contain the token.
  • On the first commit the e2e test failed with lastError = I/O error on GET request for "http://…/PATH_SECRET/v1/chains/…".
  • Mutation check: each change, removed on its own, turns its test red. That covers the logger pins (the token then leaks at TRACE), the userinfo check, the Retry-After guard, the null event check, and a URL pattern that swallows the closing quote.

A regression review of the first commit (/code-review) found the label order, the DEBUG-only diagnosis, the userinfo that is never sent, the logger leaks, and the two catch-path failures; all are fixed in the second commit. Moved to the bridge contract in batch 2:

  • a 2xx body that Jackson rejects with a non-JSON I/O error counts as a transport failure;
  • the page-size cap does not stop the read, because closing the response drains the rest of the body;
  • the Alchemy adapter keeps the endpoint in its message (its key is scrubbed);
  • a header credential for the bridge.

This PR and #15 to #18 and #20 each add a section under [Unreleased] in CHANGELOG.md, so each later merge needs a one-file rebase. The other files merge cleanly with all of them.

The HTTP bridge has no credential setting of its own, so its
credentials can only live in base-url, in the userinfo or the path.
Every transport failure passed Spring's I/O error text on, and that
text quotes the request URL: the userinfo and the path reached the
health details, the WARN log, and the lastError of sync runs that the
READ role sees.

A transport failure now names its kind, taken from the first known
exception in the cause chain (connection refused, timeout, unknown
host, TLS, I/O), with that exception's class, and carries no cause.
The cause chain goes to the DEBUG log with every URL cut out.
…s other exits

Review of the transport message fix found that it labelled a connect
timeout "connection refused" whenever a ConnectException sat anywhere
in the cause chain, and that it moved the only diagnosis to a DEBUG
line. The kind now comes from the I/O error RestClient wraps, with a
neutral "cannot connect" for a ConnectException, and the WARN line
carries the cause chain with every URL cut out, the closing quote kept.

The review also found where the URL still got out. HttpURLConnection
never sends the userinfo of base-url, so a user name or password there
only looked configured; it now stops startup with a message that leaves
the URL out, and the docs say a token goes in the path or the query.
At DEBUG and TRACE the JDK's HttpURLConnection and Spring's URI parser
printed the URL with its token; both loggers are pinned to INFO, which
the e2e test checks at a TRACE root level.

Two failures on the same catch path are fixed on the way: a null
element in events was an outage and is now a data error for the
address, and a Retry-After beyond Instant's range turned a 429 into a
failed request without its backoff.
@AsyncAssassin AsyncAssassin changed the title Name bridge transport failures without the bridge URL (B04) Name bridge transport failures without the bridge URL and close its other exits (B04) Sep 23, 2026
@AsyncAssassin

Copy link
Copy Markdown
Owner Author

Combined into #23 together with the other batch-1 fixes, so they merge without a rebase per PR. This description keeps the detailed evidence for its fix.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant