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
11 changes: 10 additions & 1 deletion src/orb/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,16 @@ export async function handleOrbWebhook(c: Context<{ Bindings: Env }>): Promise<R
return c.json({ error: "processing_failed", deliveryId }, 500);
}

await recordOrbWebhookEvent(c.env, { ...eventMeta, status: "received" });
try {
await recordOrbWebhookEvent(c.env, { ...eventMeta, status: "received" });
} catch (error) {
// Distinct from orb_webhook_processing_failed above: the upsert/outcome writes DID succeed, only the
// webhook-event row failed to persist. Left unguarded this threw past the 202 below, leaving the delivery
// unrecorded (so a GitHub redelivery re-runs the already-successful processing) with no structured log for
// this specific mode. Return 500 so GitHub redelivers and the row is retried, with its own error event (#8883).
console.error(JSON.stringify({ level: "error", event: "orb_webhook_event_record_failed", ...eventMeta, message: String(error).slice(0, 200) }));
return c.json({ error: "event_record_failed", deliveryId }, 500);
}
// Forward to a brokered self-host registered for this installation — but NEVER block the 202 we owe GitHub on it.
// A push-mode forward POSTs to the container's relay URL with a 10s timeout; a slow (e.g. tailnet) container would
// otherwise delay our response past GitHub's ~10s delivery deadline, so GitHub marks the delivery FAILED even
Expand Down
24 changes: 24 additions & 0 deletions test/integration/orb-webhook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ describe("handleOrbWebhook (POST /v1/orb/webhook)", () => {
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);
Expand Down