A network failure is printed and recorded with its reason (closes #196)
check / check (push) Waiting to run
check / check (push) Waiting to run
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
This commit is contained in:
@@ -48,6 +48,78 @@ describe("run", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Node's `fetch` rejects every failure below HTTP with a TypeError that
|
||||
// says only "fetch failed", and hangs the reason, with its errno, off
|
||||
// `cause`. The printed line carries the reason, so a cron log tells a
|
||||
// refused connection from a name that did not resolve.
|
||||
it("prints the reason a fetch failed, from the error's cause", async () => {
|
||||
const refused = new TypeError("fetch failed", {
|
||||
cause: Object.assign(
|
||||
new Error("connect ECONNREFUSED 1.2.3.4:443"),
|
||||
{ code: "ECONNREFUSED" },
|
||||
),
|
||||
});
|
||||
const result = await runToExit(Promise.reject(refused));
|
||||
expect(result).toEqual({
|
||||
code: 1,
|
||||
stdout: "",
|
||||
stderr: "quak: fetch failed: connect ECONNREFUSED 1.2.3.4:443\n",
|
||||
});
|
||||
});
|
||||
|
||||
// When the host has several addresses and every one refuses, Node's
|
||||
// `fetch` gives as the cause an AggregateError with an empty message and
|
||||
// one error per address. The printed line gives each address's reason.
|
||||
it("prints the reason for each address a fetch could not reach", async () => {
|
||||
const refusedAt = (address: string): Error =>
|
||||
Object.assign(new Error(`connect ECONNREFUSED ${address}`), {
|
||||
code: "ECONNREFUSED",
|
||||
});
|
||||
const refused = new TypeError("fetch failed", {
|
||||
cause: Object.assign(
|
||||
new AggregateError([
|
||||
refusedAt("2001:db8::1:443"),
|
||||
refusedAt("1.2.3.4:443"),
|
||||
]),
|
||||
{ code: "ECONNREFUSED" },
|
||||
),
|
||||
});
|
||||
const result = await runToExit(Promise.reject(refused));
|
||||
expect(result).toEqual({
|
||||
code: 1,
|
||||
stdout: "",
|
||||
stderr: "quak: fetch failed: connect ECONNREFUSED 2001:db8::1:443, connect ECONNREFUSED 1.2.3.4:443\n",
|
||||
});
|
||||
});
|
||||
|
||||
// An error that already quotes its cause's message is printed as it is.
|
||||
it("does not print a cause's message twice", async () => {
|
||||
const reset = new Error("read ECONNRESET");
|
||||
const wrapped = new Error(`download: file 7: ${reset.message}`, {
|
||||
cause: reset,
|
||||
});
|
||||
const result = await runToExit(Promise.reject(wrapped));
|
||||
expect(result.stderr).toBe("quak: download: file 7: read ECONNRESET\n");
|
||||
});
|
||||
|
||||
// OpenSSL ends its message for a failed TLS connection, such as one to a
|
||||
// server that answers in plain HTTP, with a newline. The printed line is
|
||||
// still one line.
|
||||
it("prints a cause's message without the white space around it", async () => {
|
||||
const wrongVersion = new TypeError("fetch failed", {
|
||||
cause: Object.assign(
|
||||
new Error(
|
||||
"C0B73A55A47F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:\n",
|
||||
),
|
||||
{ code: "ERR_SSL_WRONG_VERSION_NUMBER" },
|
||||
),
|
||||
});
|
||||
const result = await runToExit(Promise.reject(wrongVersion));
|
||||
expect(result.stderr).toBe(
|
||||
"quak: fetch failed: C0B73A55A47F0000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:354:\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("prints a thrown value that is not an Error", async () => {
|
||||
const result = await runToExit(Promise.reject("offline"));
|
||||
expect(result).toEqual({
|
||||
@@ -57,6 +129,14 @@ describe("run", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// A cause that is not an Error, such as a thrown string, is printed as a
|
||||
// thrown value that is not an Error is.
|
||||
it("prints a cause that is not an Error", async () => {
|
||||
const wrapped = new Error("ML data", { cause: "offline" });
|
||||
const result = await runToExit(Promise.reject(wrapped));
|
||||
expect(result.stderr).toBe("quak: ML data: offline\n");
|
||||
});
|
||||
|
||||
it("on a 401 from the server says to run quak login, on one line, and exits 3", async () => {
|
||||
const result = await runToExit(
|
||||
Promise.reject(new ApiError("unauthorized", 401)),
|
||||
|
||||
Reference in New Issue
Block a user