fix(e2e): abort kitchen sink retry waits

This commit is contained in:
Vincent Koc
2026-06-19 04:49:43 +02:00
parent d1ab308f5c
commit ad715dfdc9
2 changed files with 54 additions and 7 deletions
+30 -7
View File
@@ -892,14 +892,10 @@ export async function fetchJson(url, options = {}) {
let removeExternalAbort = () => {};
const abortPromise = externalSignal
? new Promise((_, reject) => {
const abortError = () =>
externalSignal.reason instanceof Error
? externalSignal.reason
: new Error("fetch aborted");
const onAbort = () => {
const error = abortError();
const error = getExternalAbortReason(externalSignal);
controller.abort(error);
reject(new Error(error.message, { cause: error }));
reject(createExternalAbortError(externalSignal));
};
if (externalSignal.aborted) {
onAbort();
@@ -939,7 +935,7 @@ export async function fetchJson(url, options = {}) {
if (attempt >= attempts || !isRetryableTransientNetworkError(error)) {
throw error;
}
await delay(options.retryDelayMs ?? 250);
await delayWithAbort(options.retryDelayMs ?? 250, externalSignal);
} finally {
removeExternalAbort();
if (timeout) {
@@ -950,6 +946,33 @@ export async function fetchJson(url, options = {}) {
throw toLintErrorObject(lastError ?? new Error(`fetch ${url} failed`), "Non-Error thrown");
}
function getExternalAbortReason(signal) {
return signal?.reason instanceof Error ? signal.reason : new Error("fetch aborted");
}
function createExternalAbortError(signal) {
const reason = getExternalAbortReason(signal);
return new Error(reason.message, { cause: reason });
}
async function delayWithAbort(delayMs, signal) {
if (!signal) {
await delay(delayMs);
return;
}
if (signal.aborted) {
throw createExternalAbortError(signal);
}
try {
await delay(delayMs, undefined, { signal });
} catch (error) {
if (signal.aborted) {
throw createExternalAbortError(signal);
}
throw error;
}
}
export async function readBoundedResponseText(response, byteLimit, timeoutPromise) {
const resolvedByteLimit = byteLimit ?? resolveKitchenSinkRpcConfig().fetchBodyMaxBytes;
const contentLength = response.headers?.get?.("content-length");
@@ -1815,6 +1815,30 @@ describe("kitchen-sink RPC process sampling", () => {
expect(fetchImpl).toHaveBeenCalledTimes(2);
});
it("aborts HTTP probe retry backoff when the external signal fires", async () => {
const controller = new AbortController();
const reset = new TypeError("fetch failed", {
cause: Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" }),
});
const fetchImpl = vi.fn().mockRejectedValue(reset);
const startedAt = Date.now();
setTimeout(() => {
controller.abort(new Error("gateway exited before ready"));
}, 25);
await expect(
fetchJson("http://127.0.0.1:19680/healthz", {
attempts: 2,
fetchImpl,
retryDelayMs: 5_000,
signal: controller.signal,
}),
).rejects.toThrow("gateway exited before ready");
expect(fetchImpl).toHaveBeenCalledOnce();
expect(Date.now() - startedAt).toBeLessThan(500);
});
it("bounds HTTP probe response bodies", async () => {
const fetchImpl = vi.fn().mockResolvedValue(new Response("x".repeat(1025), { status: 200 }));