Skip to content

feat(import): keep the bank's own code when a request is refused - #188

Merged
sars merged 1 commit into
mainfrom
feat/refusal-reason
Oct 21, 2026
Merged

sars merged 1 commit into
mainfrom
feat/refusal-reason

Conversation

@sars

@sars sars commented Oct 21, 2026

Copy link
Copy Markdown
Owner

What

A refused bank request was traced by its HTTP status alone, which cannot tell an expired aggregator session from a revoked one, or a one-off 400 from a fault. Two connections latched for review on 20–21 October on the same path and the trace could not say why.

  • src/connectors/http.ts: on a non-OK response, read at most 4 KB of the body before cancelling it; if it parses as JSON with a string error field, report it as reason beside status and code. sanitizeReason upper-cases, cuts to 40 characters and keeps the value only when it matches ^[A-Z0-9_]+$; anything else (a sentence, an identifier with dashes) is dropped whole, never stripped. message and detail are never read. A body that is empty, non-JSON, past the bound or unreadable gives no reason, and the status-based classification is unchanged either way. Nothing from the body reaches stderr or the raised error.
  • src/import-runs.ts: AttemptStep.reason (optional). src/sync-cli.ts: the recorder forwarder carries it into bank_sync_attempts.steps. src/sync-failure.ts needs no change: it reads only code from the bank_sync_failed line, and the reason is not written there.
  • frontend/src/ImportRun.tsx: the run screen shows "the bank said EXPIRED_SESSION" beside the error code.
  • docs/import-runs.md and docs/STATUS.md describe the field and its bounds.

Tests

test/import-runs.test.ts: allowed code (expired_session → EXPIRED_SESSION, message absent from the event), free-text value dropped whole, non-JSON body, empty body, body past the 4 KB bound, and sanitizeReason unit cases (bound at 40, non-strings, empty string).

Commands run

pnpm check

Result: exit 0.

  • scripts/check_repository.py: Foundation checks passed (561 Git-visible files).
  • prettier --check: All matched files use Prettier code style!
  • pnpm test: tests 735, pass 730, fail 0 (remaining are skipped/todo, unchanged).
  • pnpm build:frontend: built in 1.03s.
  • scripts/check_frontend.py: entry 114 KB gzip; design rules hold.

Not released; nothing deployed.

🤖 Generated with Claude Code

A refused request was traced by its HTTP status alone, which cannot tell an
expired aggregator session from a revoked one, or a one-off 400 from a fault.
The requester now reads at most 4 KB of a refusal's body before cancelling it
and, when it is JSON with a string `error` field, reports that value as
`reason` beside `status` and `code`. `sanitizeReason` upper-cases it, cuts it
to forty characters and keeps it only when nothing but `[A-Z0-9_]` remains,
dropping anything else whole; `message` and `detail` are never read and the
body is never written to stderr or to the raised error. The step type, the
recorder's forwarder and the run screen carry the field.

Tests cover an allowed code, a free-text value dropped whole, a non-JSON body,
an empty body and a body past the bound.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sars
sars merged commit aaf9f2c into main Oct 21, 2026
3 checks passed
@sars
sars deleted the feat/refusal-reason branch October 21, 2026 14:29
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