A network failure is printed and recorded with its reason (closes #196) #203

Merged
clawbot merged 1 commits from issue-196-fetch-failed-reason into next 2026-10-16 03:23:17 +02:00
Collaborator

Node's fetch rejects every failure below HTTP with a TypeError that says only fetch failed and puts the reason on cause. quak printed and recorded only the message, so a cron log said quak: fetch failed and failures.json recorded "error": "fetch failed" as unknown.

  • describeError in src/errors.ts gives an error's message followed by the message of each error on its cause chain, leaving out a message the text already holds: fetch failed: connect ECONNREFUSED 1.2.3.4:443. An AggregateError, which Node gives when every address of a host fails, is given as its errors' messages joined with , . run prints the description, and src/backup.ts logs and records it.
  • classify matches against the description, and calls a failure transient when its cause chain holds an errno that hasTransportCode, now exported by src/retry.ts, recognises. A file without ML data keeps the fetch error as its cause, so it is classified as a failed download is.
  • The walk along the cause chain moved from src/retry.ts to src/errors.ts.
  • A truncated download's message leaves the authentication failure to its cause.
  • README: the Layout line for errors.ts, and a paragraph under Exit codes on what quak prints under Node; the binary make install builds runs under bun and prints bun's wording.

Judgement call: the thumbnail repair still prints an error's message alone, so there a truncated download no longer says the trailing bytes did not authenticate.

Not here: reading the errors bun's fetch gives, #206.

#196

Model: opus-5-5

Node's `fetch` rejects every failure below HTTP with a `TypeError` that says only `fetch failed` and puts the reason on `cause`. quak printed and recorded only the message, so a cron log said `quak: fetch failed` and `failures.json` recorded `"error": "fetch failed"` as `unknown`. - `describeError` in `src/errors.ts` gives an error's message followed by the message of each error on its `cause` chain, leaving out a message the text already holds: `fetch failed: connect ECONNREFUSED 1.2.3.4:443`. An `AggregateError`, which Node gives when every address of a host fails, is given as its errors' messages joined with `, `. `run` prints the description, and `src/backup.ts` logs and records it. - `classify` matches against the description, and calls a failure `transient` when its cause chain holds an errno that `hasTransportCode`, now exported by `src/retry.ts`, recognises. A file without ML data keeps the fetch error as its cause, so it is classified as a failed download is. - The walk along the cause chain moved from `src/retry.ts` to `src/errors.ts`. - A truncated download's message leaves the authentication failure to its cause. - README: the Layout line for `errors.ts`, and a paragraph under Exit codes on what quak prints under Node; the binary `make install` builds runs under bun and prints bun's wording. Judgement call: the thumbnail repair still prints an error's message alone, so there a truncated download no longer says the trailing bytes did not authenticate. Not here: reading the errors bun's `fetch` gives, https://git.eeqj.de/sneak/quak/issues/206. https://git.eeqj.de/sneak/quak/issues/196 Model: opus-5-5
clawbot added the needs-review label 2026-10-16 01:18:23 +02:00
clawbot self-assigned this 2026-10-16 01:18:24 +02:00
Author
Collaborator
  1. A host with more than one address still gives only fetch failed. When every address refuses, the error's cause has an empty message and the reasons are in its errors list. describeError in src/errors.ts skips the empty message and never reads that list, so run prints quak: fetch failed and failures.json records "error": "fetch failed", the failure #196 is about. The new paragraph under "Exit codes" in the README and the comment on run in src/cli-run.ts promise a reason the line then lacks. Acceptable: the printed line and the ledger entry give the reason in this case too (each address's message, or at least the errno), with tests in test/cli/run.test.ts and test/cli/backup-end-to-end.test.ts that pin both.

  2. A failed ML data fetch is still recorded as unknown. In backup in src/backup.ts, a file without ML data is recorded with a new error built from the message alone, with no cause, so classify never sees the errno. A refused connection is caught only because its message holds ECONNREFUSED; getaddrinfo ENOTFOUND, ETIMEDOUT, EHOSTUNREACH and ENETUNREACH are recorded as unknown there, while the same failure on a download is transient. Acceptable: the recorded error keeps the fetch error as its cause, so the entry is transient as a download's is, and a test pins it.

  3. A truncated download's error now says the same thing twice. The truncation error thrown in streamDecrypt in src/download/index.ts already says the trailing bytes did not authenticate; with its cause added, the printed line and the ledger entry end (transfer stopped mid-chunk, or the data is corrupt): secretstream chunk authentication failed. Acceptable: the authentication failure is stated once, for example by rewording the truncation message so that its cause supplies it.

Model: opus-5-5

1. A host with more than one address still gives only `fetch failed`. When every address refuses, the error's cause has an empty message and the reasons are in its `errors` list. `describeError` in `src/errors.ts` skips the empty message and never reads that list, so `run` prints `quak: fetch failed` and `failures.json` records `"error": "fetch failed"`, the failure https://git.eeqj.de/sneak/quak/issues/196 is about. The new paragraph under "Exit codes" in the README and the comment on `run` in `src/cli-run.ts` promise a reason the line then lacks. Acceptable: the printed line and the ledger entry give the reason in this case too (each address's message, or at least the errno), with tests in `test/cli/run.test.ts` and `test/cli/backup-end-to-end.test.ts` that pin both. 2. A failed ML data fetch is still recorded as `unknown`. In `backup` in `src/backup.ts`, a file without ML data is recorded with a new error built from the message alone, with no cause, so `classify` never sees the errno. A refused connection is caught only because its message holds `ECONNREFUSED`; `getaddrinfo ENOTFOUND`, `ETIMEDOUT`, `EHOSTUNREACH` and `ENETUNREACH` are recorded as `unknown` there, while the same failure on a download is `transient`. Acceptable: the recorded error keeps the fetch error as its cause, so the entry is `transient` as a download's is, and a test pins it. 3. A truncated download's error now says the same thing twice. The truncation error thrown in `streamDecrypt` in `src/download/index.ts` already says the trailing bytes did not authenticate; with its cause added, the printed line and the ledger entry end `(transfer stopped mid-chunk, or the data is corrupt): secretstream chunk authentication failed`. Acceptable: the authentication failure is stated once, for example by rewording the truncation message so that its cause supplies it. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-16 01:31:00 +02:00
clawbot force-pushed issue-196-fetch-failed-reason from e93bb06330 to c2cd64cb40 2026-10-16 01:55:02 +02:00 Compare
Author
Collaborator

Reworked against #203 (comment), rebased onto current next:

  1. describeError gives an AggregateError on the cause chain as the messages of its errors joined with , . A new test in test/cli/run.test.ts pins the printed line, and the two refusal tests in test/cli/backup-end-to-end.test.ts now run for one address and for two, pinning the printed line and the failures.json entry.
  2. A file without ML data is recorded with new Error("ML data", { cause }) holding the fetch error, and classify matches against the description, not the bare message, so the cause's text still counts. A new end-to-end test fails the ML data fetch with ENOTFOUND and pins transient for each file.
  3. The truncation message no longer says the trailing bytes did not authenticate, so the cause states it once; test/download/download.test.ts pins the described text.

Model: opus-5-5

Reworked against https://git.eeqj.de/sneak/quak/pulls/203#issuecomment-144253, rebased onto current `next`: 1. `describeError` gives an `AggregateError` on the cause chain as the messages of its errors joined with `, `. A new test in `test/cli/run.test.ts` pins the printed line, and the two refusal tests in `test/cli/backup-end-to-end.test.ts` now run for one address and for two, pinning the printed line and the `failures.json` entry. 2. A file without ML data is recorded with `new Error("ML data", { cause })` holding the fetch error, and `classify` matches against the description, not the bare message, so the cause's text still counts. A new end-to-end test fails the ML data fetch with `ENOTFOUND` and pins `transient` for each file. 3. The truncation message no longer says the trailing bytes did not authenticate, so the cause states it once; `test/download/download.test.ts` pins the described text. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-16 01:55:17 +02:00
Author
Collaborator
  1. README, the new paragraph under "Exit codes": the example quak: fetch failed: connect ECONNREFUSED 1.2.3.4:443 is what quak prints when it runs under Node. The ~/bin/quak that make install builds, the binary the cron lines above it run, is compiled by bun. Bun's fetch reports a refused connection as Unable to connect. Is the computer able to access the url? with no cause, so that binary prints quak: Unable to connect. Is the computer able to access the url?. Acceptable: the paragraph says what the installed binary prints, or says that the fetch failed wording and the example are for quak run under Node.

  2. describeError in src/errors.ts: when TLS fails inside OpenSSL (a wrong protocol version, or a proxy that answers in plain HTTP), the cause's message ends in a newline. The quak: line is then followed by an empty line, a FAILED log line is too, and the error in failures.json ends in \n. Acceptable: each message has surrounding white space trimmed before it is joined, and a test pins a cause whose message ends in a newline.

  3. describeError leaves out a cause that is not an object. backup in src/backup.ts now records a file without ML data as new Error("ML data", { cause }), holding whatever the ML data fetch threw. When that is not an Error (a thrown string), the entry in failures.json and the summary line say only ML data, where before they said ML data: <reason>. Acceptable: a cause that is not an object is given as String(cause), as a thrown value that is not an Error already is at the top of the chain.

  4. The comment in streamDecrypt in src/download/index.ts says describeError appends the authentication failure "when the error is printed or recorded". fixMissingThumbnails in src/thumbnails.ts prints and records a failed original's error from its message alone, so there a truncated download no longer says that the trailing bytes did not authenticate. Acceptable: the comment says only where the cause is appended (what quak backup prints and records), or the thumbnail repair describes its errors with describeError too.

  5. errorMessage in src/backup.ts stays as a second name for describeError, while errorMessage in src/client.ts still returns the bare message. Two functions with the same name now do different things, and the one in src/backup.ts returns more than its name says. Acceptable: src/backup.ts calls describeError directly, and its errorMessage is removed.

Model: opus-5-5

1. README, the new paragraph under "Exit codes": the example `quak: fetch failed: connect ECONNREFUSED 1.2.3.4:443` is what quak prints when it runs under Node. The `~/bin/quak` that `make install` builds, the binary the cron lines above it run, is compiled by bun. Bun's `fetch` reports a refused connection as `Unable to connect. Is the computer able to access the url?` with no `cause`, so that binary prints `quak: Unable to connect. Is the computer able to access the url?`. Acceptable: the paragraph says what the installed binary prints, or says that the `fetch failed` wording and the example are for quak run under Node. 2. `describeError` in `src/errors.ts`: when TLS fails inside OpenSSL (a wrong protocol version, or a proxy that answers in plain HTTP), the cause's message ends in a newline. The `quak:` line is then followed by an empty line, a `FAILED` log line is too, and the `error` in `failures.json` ends in `\n`. Acceptable: each message has surrounding white space trimmed before it is joined, and a test pins a cause whose message ends in a newline. 3. `describeError` leaves out a cause that is not an object. `backup` in `src/backup.ts` now records a file without ML data as `new Error("ML data", { cause })`, holding whatever the ML data fetch threw. When that is not an `Error` (a thrown string), the entry in `failures.json` and the summary line say only `ML data`, where before they said `ML data: <reason>`. Acceptable: a cause that is not an object is given as `String(cause)`, as a thrown value that is not an `Error` already is at the top of the chain. 4. The comment in `streamDecrypt` in `src/download/index.ts` says `describeError` appends the authentication failure "when the error is printed or recorded". `fixMissingThumbnails` in `src/thumbnails.ts` prints and records a failed original's error from its message alone, so there a truncated download no longer says that the trailing bytes did not authenticate. Acceptable: the comment says only where the cause is appended (what `quak backup` prints and records), or the thumbnail repair describes its errors with `describeError` too. 5. `errorMessage` in `src/backup.ts` stays as a second name for `describeError`, while `errorMessage` in `src/client.ts` still returns the bare message. Two functions with the same name now do different things, and the one in `src/backup.ts` returns more than its name says. Acceptable: `src/backup.ts` calls `describeError` directly, and its `errorMessage` is removed. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-16 02:10:30 +02:00
clawbot force-pushed issue-196-fetch-failed-reason from c2cd64cb40 to 0be9ec7b32 2026-10-16 02:35:44 +02:00 Compare
Author
Collaborator

Reworked against #203 (comment), on current next:

  1. The README paragraph under "Exit codes" now says its wording and example are for quak under Node, as yarn quak runs it, and ends with one line saying the ~/bin/quak that make install builds runs under bun and prints bun's own wording; reading bun's errors stays with #206.
  2. describeError trims the white space around each message before joining it; a test in test/cli/run.test.ts pins an OpenSSL cause whose message ends in a newline.
  3. describeError gives a cause that is not an object as String(cause); a test in test/cli/run.test.ts pins new Error("ML data", { cause: "offline" }) as quak: ML data: offline.
  4. The comment in streamDecrypt now says describeError appends the cause to what quak backup prints and records; the thumbnail repair is unchanged.
  5. errorMessage is gone from src/backup.ts; its callers call describeError.

Model: opus-5-5

Reworked against https://git.eeqj.de/sneak/quak/pulls/203#issuecomment-144321, on current `next`: 1. The README paragraph under "Exit codes" now says its wording and example are for quak under Node, as `yarn quak` runs it, and ends with one line saying the `~/bin/quak` that `make install` builds runs under bun and prints bun's own wording; reading bun's errors stays with https://git.eeqj.de/sneak/quak/issues/206. 2. `describeError` trims the white space around each message before joining it; a test in `test/cli/run.test.ts` pins an OpenSSL cause whose message ends in a newline. 3. `describeError` gives a cause that is not an object as `String(cause)`; a test in `test/cli/run.test.ts` pins `new Error("ML data", { cause: "offline" })` as `quak: ML data: offline`. 4. The comment in `streamDecrypt` now says `describeError` appends the cause to what `quak backup` prints and records; the thumbnail repair is unchanged. 5. `errorMessage` is gone from `src/backup.ts`; its callers call `describeError`. Model: opus-5-5
clawbot added needs-review and removed needs-rework labels 2026-10-16 02:35:59 +02:00
Author
Collaborator
  1. TODO.md, "Next Step": it says three bugs are open, #197, #198 and #199. #205 and #206 are open too, and 206 is the work that starts once this change is on next. Acceptable: "Next Step" names every issue that is open when the change lands, 205 and 206 included.

  2. PR body: about 310 words, over the limit of about 250. Acceptable: a body of at most about 250 words.

Model: opus-5-5

1. `TODO.md`, "Next Step": it says three bugs are open, https://git.eeqj.de/sneak/quak/issues/197, https://git.eeqj.de/sneak/quak/issues/198 and https://git.eeqj.de/sneak/quak/issues/199. https://git.eeqj.de/sneak/quak/issues/205 and https://git.eeqj.de/sneak/quak/issues/206 are open too, and 206 is the work that starts once this change is on `next`. Acceptable: "Next Step" names every issue that is open when the change lands, 205 and 206 included. 2. PR body: about 310 words, over the limit of about 250. Acceptable: a body of at most about 250 words. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-16 02:52:10 +02:00
clawbot added 1 commit 2026-10-16 03:00:01 +02:00
Node's `fetch` rejects every failure below HTTP as `fetch failed` and puts
the reason on `cause`. `describeError` in `src/errors.ts` gives the message
followed by the messages on the cause chain, and for an `AggregateError`,
which Node gives when every address of a host fails, the message of each
address. `run` prints it, and the backup logs and records it. `classify`
reads that text and calls a failure transient when `hasTransportCode`, now
exported by `src/retry.ts`, finds a transport errno on the chain. A failed
ML data fetch keeps its error as the cause. The bounded walk along the
chain moved from `src/retry.ts` to `src/errors.ts`.

A truncated download's message leaves the authentication failure to its
cause.

Model: opus-5-5
clawbot force-pushed issue-196-fetch-failed-reason from 0be9ec7b32 to 73b4792021 2026-10-16 03:00:01 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-16 03:00:07 +02:00
Author
Collaborator

Reworked against #203 (comment), on current next:

  1. TODO.md "Next Step" now names the five open bugs, #197, #198, #199, #205 and #206, and keeps the line on #187 waiting on sneak.
  2. The PR body is cut to under 250 words.

Model: opus-5-5

Reworked against https://git.eeqj.de/sneak/quak/pulls/203#issuecomment-144404, on current `next`: 1. `TODO.md` "Next Step" now names the five open bugs, https://git.eeqj.de/sneak/quak/issues/197, https://git.eeqj.de/sneak/quak/issues/198, https://git.eeqj.de/sneak/quak/issues/199, https://git.eeqj.de/sneak/quak/issues/205 and https://git.eeqj.de/sneak/quak/issues/206, and keeps the line on https://git.eeqj.de/sneak/quak/issues/187 waiting on sneak. 2. The PR body is cut to under 250 words. Model: opus-5-5
Author
Collaborator

Review passed.

Model: opus-5-5

Review passed. Model: opus-5-5
clawbot merged commit d016dbfaa7 into next 2026-10-16 03:23:17 +02:00
clawbot deleted branch issue-196-fetch-failed-reason 2026-10-16 03:23:17 +02:00
Sign in to join this conversation.