The binary make install builds retries a refused or unresolvable connection (closes #206)
check / check (push) Canceled after 0s

Bun's `fetch`, which `~/bin/quak` runs under, rejects with a plain `Error`
that has no `cause` and a code of bun's own. Running the built binary against
local listeners gave `ConnectionRefused` for a refused connection and for a
failed TLS handshake, and `ConnectionRefused` or `FailedToOpenSocket` for a
name that does not resolve. Both codes join `TRANSPORT_CODES` and
`CONNECT_CODES` in `src/retry.ts`, so they are retried, replayed, and called
transient by `classify`. A reset already came as `ECONNRESET` and a deadline
as a `TimeoutError`; the tests pin every shape found.

Judgement call: a failed TLS handshake is replayed, as no request byte was
sent.
Not changed: a certificate bun rejects is not retried.

Model: opus-5-5
This commit is contained in:
2026-10-16 01:43:48 +00:00
parent d016dbfaa7
commit 4c9a42db49
5 changed files with 201 additions and 31 deletions
+28 -12
View File
@@ -151,7 +151,9 @@ for each, separated by `, `. A file whose download failed that way is in
`failures.json` with that error, as a transient failure. So is each file with no
ML data cached when the ML data fetch failed that way, its error starting
`ML data: `. The `~/bin/quak` that `make install` builds runs under bun, and
prints bun's own wording for such a failure.
prints bun's own wording for such a failure, with no reason after it, such as
`quak: Unable to connect. Is the computer able to access the url?` for a refused
connection. It records such a failure in `failures.json` as transient too.
After a `1`, the next run fetches each missing original again, fetches the ML
data that is not cached, and rebuilds the album links. An album quak could not
@@ -494,6 +496,17 @@ request is repeated only when repeating it could produce a different answer:
declining to retry a transient one costs a single file that the next run picks
up.
The `~/bin/quak` that `make install` builds runs under bun, whose `fetch`
rejects with a plain `Error` that has no `cause` and carries a code of bun's
own. quak retries a refused connection, a name that does not resolve, a reset
connection and a deadline under bun as it does under Node. Bun reports a refused
connection, and a TLS handshake that failed, as `ConnectionRefused`. For a name
that does not resolve it gives `ConnectionRefused` on some requests and
`FailedToOpenSocket` on others. A connection reset or closed before the response
ended is `ECONNRESET`, as under Node, and a deadline is the same `TimeoutError`.
A certificate bun rejects, reported with a code such as
`DEPTH_ZERO_SELF_SIGNED_CERT`, is not retried.
Backoff is exponential with full jitter: the delay before retry _n_ is
`random() * min(maxDelayMs, baseDelayMs * 2 ** (n - 1))`. The exponential term
is the ceiling and the wait is drawn below it, so a client that lost many
@@ -541,17 +554,20 @@ second-factor attempts. They are retried only when every errno in the error's
`cause` chain is one of the three that establish no TCP connection to the server
ever existed, so no request byte can have been transmitted: `ENOTFOUND` and
`EAI_AGAIN` (name resolution produced no address) and `ECONNREFUSED` (the peer
refused the connection). A 5xx, a mid-flight reset and a deadline are all left
to the caller, because each of them can happen after the server has already
acted. These two do not follow redirects either: a redirect means the server
already received the request, so it is reported as an error and not retried. The
routing errnos `EHOSTUNREACH`, `ENETUNREACH` and `ENETDOWN` are excluded for the
same reason, despite looking like connect-time failures: on Linux an ICMP
unreachable arriving mid-flight, or a local interface going down after the
request was written, delivers them on an already-established socket. They stay
retryable for the idempotent calls. `putFile` is exempt: a presigned PUT stores
one whole object at one key in one request, so replaying it has no partial state
to damage.
refused the connection). Under bun these failures are `ConnectionRefused` and
`FailedToOpenSocket`. Bun also gives `ConnectionRefused` for a TLS handshake
that failed; the TCP connection existed then, but the request is only sent once
the handshake is done. A 5xx, a mid-flight reset and a deadline are all left to
the caller, because each of them can happen after the server has already acted.
These two do not follow redirects either: a redirect means the server already
received the request, so it is reported as an error and not retried. The routing
errnos `EHOSTUNREACH`, `ENETUNREACH` and `ENETDOWN` are excluded for the same
reason, despite looking like connect-time failures: on Linux an ICMP unreachable
arriving mid-flight, or a local interface going down after the request was
written, delivers them on an already-established socket. They stay retryable for
the idempotent calls. `putFile` is exempt: a presigned PUT stores one whole
object at one key in one request, so replaying it has no partial state to
damage.
A download is retried as a whole — request, stream consumption, and decryption —
because a socket reset after the response headers have arrived surfaces in the