Every outbound call goes through one door, and unmapped errors carry an id - #118
Merged
Conversation
…an id Three failures in a row reached users as "Internal server error" or as a parser naming a doctype, each fixed where it was found and each found by somebody hitting it. This is the sweep for the rest of the class. The class is precise: a `TypeError` from a request that never left, or a `SyntaxError` from a 2xx whose body is a web page. Neither is an HttpException, so Nest's default filter reduces both to a sentence that tells nobody anything about the VPN, the certificate or the SSO portal it always turns out to be. `common/http-failures.ts` now owns both, plus the URL normalizer that was sitting in vcs.types (it was never VCS-specific — every integration takes a host somebody configured). Everything outbound routes through `fetchOrExplain`/`readJsonOrExplain`, and a test walks apps/api asserting that module is the only file calling fetch directly, because grep is the only enforcement that survives the next integration. Four sites were unguarded and reachable by a user. Jira had the same triple GitLab did — no URL normalization, no transport guard, no JSON guard — and `connectTracker` catching everything meant a schemeless site URL was reported as "Jira rejected that credential", blaming the one thing that was fine. The GitHub App's token minting and its manifest exchange were bare on both counts. So was the embeddings provider, whose default points at a local Ollama that is very often not running — the likeliest transport failure in the codebase and the one whose TypeError says least. GitLab's tree pagination had its own fetch that the earlier fix had missed entirely. Seven other sites were already guarded and are left alone; the audit is in the PR rather than in more code. Last, the safety net: there was no exception filter at all. An unmapped error still answers 500 — that is the right status for a bug nobody typed a message for — but now with a short reference that is also in the log beside the stack, and the error's class name, since TypeError and SyntaxError are exactly the two that mean "an outbound call failed in a way nobody wrapped". The next one costs a message instead of a session.
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.
Three failures in a row reached users as
Internal server erroror as a parsernaming a doctype. Each was fixed where it was found, and each was found by
somebody hitting it. This is the sweep for the rest of the class.
Stacked on #117 — review that first; this PR's diff is only the sweep.
The class, precisely
TypeError: fetch failedInternal server errorSyntaxError: Unexpected token '<'Neither is an
HttpException, so Nest's default filter flattens both.The audit
Every
fetch,res.json(),JSON.parseandnew URLinapps/api/src:Already guarded — left alone (7).
anthropic.service.ts(schema parse, ina try with a real message),
claude-code.provider.ts×2 (envelope parse, same),jira.adapter.tsdescribeJiraError,runs.service.ts(LISTEN payload,deliberately swallowed),
github.controller.tswebhook body → 400,github-app.service.tsisPubliclyReachable,gitlab-webhook.service.tshostOf.Unguarded and user-reachable — fixed (4 files, 6 sites).
guard, no JSON guard. Because
connectTrackercatches everything, aschemeless site URL was reported as "Jira rejected that credential" —
blaming the one thing that was fine. Wizard step 4, so the next thing you'd
have hit.
fetch+ bareres.json(), reached fromevery repository listing, onboarding run and build.
blank page at the end of a flow you cannot restart without a new code. Wrapped
as
BadRequestExceptionfor that reason.local Ollama that is very often not running: the likeliest transport failure
in the codebase, and the one whose bare
TypeErrorsays least.fetchthat the earlier fix missedentirely — exactly the kind of second call site a per-symptom fix leaves
behind.
One door, and grep as the enforcement
common/http-failures.tsownsfetchOrExplain,readJsonOrExplain,describeTransportFailure,describeNonJsonBodyand the URL normalizer (whichwas in
vcs.types.tsand never was VCS-specific — every integration takes ahost somebody configured).
A test walks
apps/api/srcand asserts that module is the only file callingfetchdirectly. A new integration that callsfetchitself re-opens theclass, and this is what says so at review time rather than after a bug report.
Two invariants fell out and are now pinned:
VcsError,JiraError); anything untyped escaping one is the bug.''fails exactlythe way a login page does, so every site that forgot reported a portal on a
successful branch deletion. Handled centrally now.
The safety net
There was no exception filter at all.
UnhandledExceptionFilterstillanswers 500 for an unmapped error — the right status for a bug nobody typed a
message for — but with a short reference id that is also in the log beside the
stack, and the error's class name, since
TypeErrorandSyntaxErrorareprecisely the two that mean an outbound call failed in a way nobody wrapped.
HttpExceptions pass through untouched; a 400 that already says what is wrongis doing its job.
This is the part that ends the pattern. It does not prevent the next one — it
makes it cost one message instead of a session.
Verify
pnpm typecheck && pnpm test(43 API files),pnpm build,pnpm site:check.12 new tests on the shared module, including the grep rule.
knowledge/conventions.mdcarries the convention.
For the reviewer
fetch. Reading the body astext rather than
res.json()means a stub supplying onlyjson()now returnsundefined. Three test files were updated; a real server is unaffected, but
it's the thing to look for if something downstream misbehaves.
meaningfully, and the suite has no app-level harness. The logic is small and
the failure mode is cosmetic, but it is asserted by reading, not by running.