Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sdk/typescript/src/cloud-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ async function publishCloudPayload(
signal,
});
} catch {
dependencies.signal?.throwIfAborted();
// A lost response does not establish whether the server accepted the POST.
throw new CodexSecurityError(
"Cloud publication was not confirmed. The request was not retried; check whether it was accepted before submitting again.",
Expand All @@ -475,7 +476,10 @@ async function publishCloudPayload(
);
}
const receipt = receiptSchema.safeParse(
await response.json().catch(() => undefined),
await response.json().catch(() => {
dependencies.signal?.throwIfAborted();
return undefined;
}),
);
// Cloud assigns opaque IDs in request order, so they cannot be compared to
// local finding IDs. The authenticated response must still preserve the
Expand Down
50 changes: 50 additions & 0 deletions sdk/typescript/tests-ts/cloud-publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,56 @@ describe("Cloud publication", () => {
expect(requests).toBe(1);
});

test("preserves caller cancellation during the publication request", async () => {
const { scan, environment } = await fixture();
const controller = new AbortController();
const cancellation = new Error("publication cancelled");

await expect(
publishScanToCloud(scan, {
environment,
signal: controller.signal,
fetch: async (_url, options) => {
const signal = options.signal as AbortSignal;
return await new Promise<Response>((_resolve, reject) => {
signal.addEventListener("abort", () => reject(signal.reason), {
once: true,
});
controller.abort(cancellation);
});
},
}),
).rejects.toBe(cancellation);
});

test("preserves caller cancellation while parsing the acceptance receipt", async () => {
const { scan, environment } = await fixture();
const controller = new AbortController();
const cancellation = new Error("receipt parsing cancelled");
const response = {
ok: true,
status: 201,
body: null,
json: async () =>
await new Promise<never>((_resolve, reject) => {
controller.signal.addEventListener(
"abort",
() => reject(controller.signal.reason),
{ once: true },
);
controller.abort(cancellation);
}),
} as unknown as Response;

await expect(
publishScanToCloud(scan, {
environment,
signal: controller.signal,
fetch: async () => response,
}),
).rejects.toBe(cancellation);
});

test("requires a complete acceptance receipt instead of treating any 2xx as success", async () => {
const { scan, environment } = await fixture();
for (const body of [
Expand Down
Loading