A network failure is logged and recorded as fetch failed, with the reason dropped #196

Closed
opened 2026-10-14 09:29:21 +02:00 by clawbot · 2 comments
Collaborator

Severity: medium

Where: run in src/cli-run.ts prints err.message; errorMessage, recordFailure and classify in src/backup.ts read only the message too.

Problem: Node's fetch rejects every failure below HTTP (no route, name not found, connection refused, TLS, a reset) with a TypeError whose message is just fetch failed; the reason (ECONNREFUSED, ENOTFOUND, ...) is on its cause. The retry policy looks there (README, "Retries and timeouts"), but what is printed and recorded does not. After the retries, a run from cron leaves:

  • when the refresh fails: the one line quak: fetch failed, exit 1, and nothing says whether the name, the route or the server was the problem;
  • when a download fails: FAILED original a.jpg: fetch failed, and in failures.json "error": "fetch failed" with "classification": "unknown", because the classifier searches the message for the errno, where it is not.

An operator reading the log cannot tell a dead network from a bug, and the ledger records the wrong kind of failure for the most common transient one.

Reproduced: a backup through the fake Ente server of test/ente-server.ts with a fetch that rejects with new TypeError("fetch failed", { cause: Object.assign(new Error("connect ECONNREFUSED 1.2.3.4:443"), { code: "ECONNREFUSED" }) }), once for every request and once for the download only. Output as above; ECONNREFUSED appears nowhere in the log or the ledger.

Done when: the printed line and the ledger entry carry the messages of the error's cause chain, for example fetch failed: connect ECONNREFUSED 1.2.3.4:443, and classify sees the errno, so this failure is transient. Tests in test/cli/run.test.ts and test/cli/backup-end-to-end.test.ts pin the printed line and the ledger entry for a TypeError with an errno in cause.

Model: fable-5-1

Severity: medium **Where:** `run` in `src/cli-run.ts` prints `err.message`; `errorMessage`, `recordFailure` and `classify` in `src/backup.ts` read only the message too. **Problem:** Node's `fetch` rejects every failure below HTTP (no route, name not found, connection refused, TLS, a reset) with a `TypeError` whose message is just `fetch failed`; the reason (`ECONNREFUSED`, `ENOTFOUND`, ...) is on its `cause`. The retry policy looks there (README, "Retries and timeouts"), but what is printed and recorded does not. After the retries, a run from cron leaves: - when the refresh fails: the one line `quak: fetch failed`, exit 1, and nothing says whether the name, the route or the server was the problem; - when a download fails: `FAILED original a.jpg: fetch failed`, and in `failures.json` `"error": "fetch failed"` with `"classification": "unknown"`, because the classifier searches the message for the errno, where it is not. An operator reading the log cannot tell a dead network from a bug, and the ledger records the wrong kind of failure for the most common transient one. **Reproduced:** a backup through the fake Ente server of `test/ente-server.ts` with a `fetch` that rejects with `new TypeError("fetch failed", { cause: Object.assign(new Error("connect ECONNREFUSED 1.2.3.4:443"), { code: "ECONNREFUSED" }) })`, once for every request and once for the download only. Output as above; `ECONNREFUSED` appears nowhere in the log or the ledger. **Done when:** the printed line and the ledger entry carry the messages of the error's `cause` chain, for example `fetch failed: connect ECONNREFUSED 1.2.3.4:443`, and `classify` sees the errno, so this failure is `transient`. Tests in `test/cli/run.test.ts` and `test/cli/backup-end-to-end.test.ts` pin the printed line and the ledger entry for a `TypeError` with an errno in `cause`. Model: fable-5-1
clawbot added this to the 1.0.0 milestone 2026-10-14 09:29:21 +02:00
Author
Collaborator

Plan.

  • One function describes an error for the log and the ledger: its message, then the message of each error in its cause chain, joined with : , for example fetch failed: connect ECONNREFUSED 1.2.3.4:443. A cause whose message is already in the text before it is not repeated.
  • It goes in src/errors.ts; its header comment and the README Layout line for it become "error types shared across layers, and how an error is described". src/retry.ts already walks the cause chain with a depth limit; that walk moves to src/errors.ts and src/retry.ts uses it, so there is one walk, not two.
  • run in src/cli-run.ts and errorMessage in src/backup.ts print and record that description.
  • classify in src/backup.ts calls a failure transient when its cause chain holds an errno that src/retry.ts already retries as a network failure (ENOTFOUND and ECONNREFUSED among them). src/retry.ts exports that check, since it is the retry classifier.
  • Tests as the issue says, in test/cli/run.test.ts and test/cli/backup-end-to-end.test.ts. Where the README quotes a printed error or a ledger error, it matches.

Model: opus-5-5

Plan. - One function describes an error for the log and the ledger: its message, then the message of each error in its `cause` chain, joined with `: `, for example `fetch failed: connect ECONNREFUSED 1.2.3.4:443`. A cause whose message is already in the text before it is not repeated. - It goes in `src/errors.ts`; its header comment and the README Layout line for it become "error types shared across layers, and how an error is described". `src/retry.ts` already walks the cause chain with a depth limit; that walk moves to `src/errors.ts` and `src/retry.ts` uses it, so there is one walk, not two. - `run` in `src/cli-run.ts` and `errorMessage` in `src/backup.ts` print and record that description. - `classify` in `src/backup.ts` calls a failure `transient` when its cause chain holds an errno that `src/retry.ts` already retries as a network failure (`ENOTFOUND` and `ECONNREFUSED` among them). `src/retry.ts` exports that check, since it is the retry classifier. - Tests as the issue says, in `test/cli/run.test.ts` and `test/cli/backup-end-to-end.test.ts`. Where the README quotes a printed error or a ledger `error`, it matches. Model: opus-5-5
Author
Collaborator

Built as the plan above says, in #203. The function is describeError in src/errors.ts, and the check src/retry.ts exports is hasTransportCode. One gap the PR names and leaves open: when Node tries several addresses for a host and every one refuses, the cause is an AggregateError with an empty message, so the printed line would still be fetch failed, though the failure is recorded as transient.

Model: opus-5-5

Built as the plan above says, in https://git.eeqj.de/sneak/quak/pulls/203. The function is `describeError` in `src/errors.ts`, and the check `src/retry.ts` exports is `hasTransportCode`. One gap the PR names and leaves open: when Node tries several addresses for a host and every one refuses, the cause is an `AggregateError` with an empty message, so the printed line would still be `fetch failed`, though the failure is recorded as transient. Model: opus-5-5
Sign in to join this conversation.