From bf1b7ffe0c1f6319dbe14543e062a47f84886b03 Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:45:36 +0900 Subject: [PATCH] fix(orb): guard the trailing webhook-event record write in handleOrbWebhook The status:"received" recordOrbWebhookEvent write ran outside the try/catch that wraps the upsert/outcome writes above it. If it threw, execution never reached the 202 response, the delivery went unrecorded (so a GitHub redelivery re-ran the already-successful upsert/outcome processing) and no structured log was emitted for this specific failure mode. Wrap the trailing write in its own try/catch with a distinct orb_webhook_event_record_failed error log and a clean 500 { error: "event_record_failed" }, so GitHub redelivers and retries the row -- mirroring the processing-failed handling above while keeping the two failure modes distinguishable. Closes #8883 --- src/orb/webhook.ts | 11 ++++++++++- test/integration/orb-webhook.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/orb/webhook.ts b/src/orb/webhook.ts index a1c2e23e05..2333ed9364 100644 --- a/src/orb/webhook.ts +++ b/src/orb/webhook.ts @@ -82,7 +82,16 @@ export async function handleOrbWebhook(c: Context<{ Bindings: Env }>): Promise { errors.mockRestore(); }); + it("500 + distinct log when only the trailing webhook-event record write fails (#8883)", async () => { + const e = env(); + const real = e.DB; + // The upsert/outcome writes succeed (so the earlier catch is NOT entered); throw only on the trailing + // orb_webhook_events INSERT (the status:"received" record), leaving the dedup SELECT read on the real DB. + (e as { DB: unknown }).DB = { + prepare: (sql: string) => + sql.includes("orb_webhook_events") && sql.includes("INSERT") + ? { bind: () => ({ run: () => Promise.reject(new Error("boom")) }) } + : real.prepare(sql), + }; + const errors = vi.spyOn(console, "error").mockImplementation(() => undefined); + const res = await post(e, INSTALL, { delivery: "rec-err" }); + expect(res.status).toBe(500); + expect(await res.json()).toMatchObject({ error: "event_record_failed", deliveryId: "rec-err" }); + // Distinct from orb_webhook_processing_failed: the processing succeeded, only the event-row write failed. + const processingLog = errors.mock.calls.map((c) => String(c[0])).find((line) => line.includes("orb_webhook_processing_failed")); + expect(processingLog).toBeUndefined(); + const recordLog = errors.mock.calls.map((c) => String(c[0])).find((line) => line.includes("orb_webhook_event_record_failed")); + expect(recordLog).toBeDefined(); + expect(JSON.parse(recordLog!)).toMatchObject({ level: "error", event: "orb_webhook_event_record_failed", deliveryId: "rec-err", eventName: "installation", installationId: 42, message: "Error: boom" }); + errors.mockRestore(); + }); + it("400 when the GitHub delivery or event header is missing", async () => { expect((await post(env(), INSTALL, { delivery: null as unknown as string })).status).toBe(400); expect((await post(env(), INSTALL, { event: null as unknown as string })).status).toBe(400);