The binary make install builds retries a refused or unresolvable connection (closes #206)
check / check (push) Waiting to run
check / check (push) Waiting to run
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 a failed TLS handshake, and `ConnectionRefused` or `FailedToOpenSocket` for a name that does not resolve. Both join `TRANSPORT_CODES` in `src/retry.ts`, so they are retried and called transient. They stay out of `CONNECT_CODES`, so a `POST` or `PUT` is not replayed on them, because bun also gives them when it resends a request on a new connection. `classify` in `src/backup.ts` records a `TimeoutError` as transient, so a stalled download is no longer `unknown`. Not changed: a certificate bun rejects is not retried. Model: opus-5-5
This commit was merged in pull request #207.
This commit is contained in:
+27
-7
@@ -58,6 +58,14 @@ export const resolveRetryOptions = (
|
||||
});
|
||||
|
||||
// Transport failures: the request did not complete, for reasons below HTTP.
|
||||
//
|
||||
// The errnos are Node's. The binary `make install` builds runs under bun,
|
||||
// whose `fetch` rejects with a plain `Error` that has no `cause` and a `code`
|
||||
// of bun's own. Bun gives `ConnectionRefused` for a refused connection and for
|
||||
// a TLS handshake that failed, and `ConnectionRefused` or `FailedToOpenSocket`
|
||||
// for a name that does not resolve. For a connection reset or closed before
|
||||
// the response ended, it gives Node's `ECONNRESET`. These were found by
|
||||
// running that binary, built with bun 1.3.13, against listeners on 127.0.0.1.
|
||||
const TRANSPORT_CODES = new Set([
|
||||
"ECONNRESET",
|
||||
"ECONNABORTED",
|
||||
@@ -70,6 +78,9 @@ const TRANSPORT_CODES = new Set([
|
||||
"ENETUNREACH",
|
||||
"ENETRESET",
|
||||
"ENETDOWN",
|
||||
// Bun's.
|
||||
"ConnectionRefused",
|
||||
"FailedToOpenSocket",
|
||||
]);
|
||||
|
||||
// The subset of the above that can only be reported before a TCP connection
|
||||
@@ -87,6 +98,12 @@ const TRANSPORT_CODES = new Set([
|
||||
// ambiguity this set exists to exclude. They stay in `TRANSPORT_CODES`, so
|
||||
// they remain retryable for idempotent calls; only replay eligibility is
|
||||
// narrowed. See `isSafeToReplay`.
|
||||
//
|
||||
// Bun's `ConnectionRefused` and `FailedToOpenSocket` are absent too, so a
|
||||
// `POST` or `PUT` is not replayed on them. When a connection bun kept open from
|
||||
// an earlier request closes before any answer, bun sends the request again on
|
||||
// a new connection, and if that one fails it gives one of these codes although
|
||||
// the server may already have the request.
|
||||
const CONNECT_CODES = new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED"]);
|
||||
|
||||
// Collect every `code` in an error's cause chain. undici does not put the
|
||||
@@ -105,7 +122,7 @@ const causeCodes = (err: unknown): { codes: string[]; complete: boolean } => {
|
||||
return { codes, complete };
|
||||
};
|
||||
|
||||
// Does the error's cause chain hold the errno of a transport failure, one of
|
||||
// Does the error's cause chain hold the code of a transport failure, one of
|
||||
// `TRANSPORT_CODES`? `isRetryable` retries such an error, and `classify` in
|
||||
// `src/backup.ts` calls it transient.
|
||||
export const hasTransportCode = (err: unknown): boolean =>
|
||||
@@ -152,7 +169,8 @@ export const isRetryable = (err: unknown): boolean => {
|
||||
// deliberately literal. Demanding a recognised `cause` instead would
|
||||
// classify real network failures as permanent and fail backups that should
|
||||
// have succeeded; the cost of the imprecision is bounded by the attempt
|
||||
// count.
|
||||
// count. Bun's fetch rejects with a plain `Error` instead, which is
|
||||
// recognised by its code (see `TRANSPORT_CODES`).
|
||||
if (err instanceof TypeError) return true;
|
||||
return hasTransportCode(err);
|
||||
};
|
||||
@@ -170,11 +188,13 @@ export const isRetryable = (err: unknown): boolean => {
|
||||
//
|
||||
// Everything else is ambiguous. A 5xx proves the server did process the
|
||||
// request. A reset or a broken pipe can arrive after it was fully sent and
|
||||
// acted on. A routing errno can be delivered on an established socket. A
|
||||
// deadline says nothing at all about the server's state. So every errno in the
|
||||
// cause chain must be a connect errno: one other errno anywhere in the chain
|
||||
// is doubt, and doubt is not replayed. A chain longer than the walk is doubt
|
||||
// too: the links below the limit were never read.
|
||||
// acted on. A routing errno can be delivered on an established socket. Bun's
|
||||
// `ConnectionRefused` and `FailedToOpenSocket` can come from a new connection
|
||||
// bun opened to resend a request that already went out on a connection it kept
|
||||
// open. A deadline says nothing at all about the server's state. So every code in the
|
||||
// cause chain must be one of `CONNECT_CODES`: one other code anywhere in the
|
||||
// chain is doubt, and doubt is not replayed. A chain longer than the walk is
|
||||
// doubt too: the links below the limit were never read.
|
||||
export const isSafeToReplay = (err: unknown): boolean => {
|
||||
const { codes, complete } = causeCodes(err);
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user