Hardening: bounded dedup maps, fetch timeouts, buildTaskRequest helper #35

Closed
opened 2026-04-18 10:33:39 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a maintainer, I want three small correctness/readability fixes batched into one PR — bounded in-memory maps, timeouts on every Forgejo fetch, and a single buildTaskRequest helper — so that the webhook stops leaking memory under high event volume, stops blocking for 30 s on Forgejo hangs, and stops duplicating the same 12-field object six times.

Context

Audit 2026-04-18 surfaced three pre-existing issues in src/webhook.ts:

  1. Unbounded maps. _fixCiDispatched (line ~460) and _reviewFallbacks (line ~470) grow indefinitely under high event volume; _fixCiDispatched only prunes after hitting a hard 200-entry cap.
  2. Four fetch() calls without AbortSignal.timeout. repoHasWorkflows and three inside requestReviewIfFresh. A Forgejo hang blocks the webhook handler until OS TCP timeout (~30 s).
  3. Six identical TaskRequest object literals across event handlers — 12 fields × 6 occurrences = ~72 lines of boilerplate.

Acceptance criteria

BoundedMap utility

  • Introduce a small BoundedMap<K, V>(maxSize, ttlMs?) class (can live in src/util/bounded-map.ts or inline in webhook-ci.ts if #A has already landed). On set, if size exceeds maxSize, evict the oldest entry. If ttlMs is provided, also evict entries older than ttlMs on write.
  • Replace _fixCiDispatched (plain Map) with BoundedMap<string, number>(500, 3_600_000) — cap matches the 1 h dedup window plus headroom.
  • Replace _reviewFallbacks (plain Map) with either BoundedMap or keep as-is if the existing arm-cancels-prior-timer logic is already sufficient. Document the choice in a comment.
  • Unit tests: BoundedMap evicts on size and on TTL; does not mis-evict entries that are still within TTL.

Fetch timeouts

  • Add signal: AbortSignal.timeout(5_000) to every fetch() call in src/webhook.ts (or the split files if #A landed first). If #A landed and introduced src/forgejo-api.ts, add the timeout there exactly once and verify all callers go through it.
  • Manual verification note in PR: curl https://httpbin.org/delay/10 takes ≥ 10 s; confirm our handler bails at 5 s with a clear log line, not a hang.

buildTaskRequest helper

  • Extract buildTaskRequest(agent: Agent, repo: string, issueOrPr: number, task: string, overrides?: Partial<TaskRequest>): TaskRequest into either src/webhook-handlers.ts (if #A landed) or src/webhook.ts. It must:
    • Populate forgejo_token, forgejo_mcp_command, forgejo_url, callback_url, role, git_name, git_email, branch_prefix, system_prompt, model, container from the config for the given agent.
    • Accept overrides to let handlers tweak stateless_session (review) and other per-event fields.
  • Replace all six in-line literals with calls to the helper. Each handler drops from ~15 lines to ~3.

Tests

  • BoundedMap has dedicated tests (size eviction, TTL eviction, get/set/has semantics).
  • Existing webhook tests continue to pass; no new assertions required beyond the BoundedMap tests.

Out of scope

  • The webhook.ts structural split (see #A). This issue lives in whatever the current file layout is — if #A lands first, do the work in the new files; otherwise do it in webhook.ts.
  • Deeper test coverage for CI gate (see #D).

References

  • Codebase audit 2026-04-18.

Dependencies

  • Not blocked by: anything (compatible with either pre- or post-#A layout).
  • Sibling: #A, #C, #D.
  • Branch off: main
## User story As a **maintainer**, I want three small correctness/readability fixes batched into one PR — bounded in-memory maps, timeouts on every Forgejo fetch, and a single `buildTaskRequest` helper — so that the webhook stops leaking memory under high event volume, stops blocking for 30 s on Forgejo hangs, and stops duplicating the same 12-field object six times. ## Context Audit 2026-04-18 surfaced three pre-existing issues in `src/webhook.ts`: 1. **Unbounded maps.** `_fixCiDispatched` (line ~460) and `_reviewFallbacks` (line ~470) grow indefinitely under high event volume; `_fixCiDispatched` only prunes after hitting a hard 200-entry cap. 2. **Four `fetch()` calls without `AbortSignal.timeout`.** `repoHasWorkflows` and three inside `requestReviewIfFresh`. A Forgejo hang blocks the webhook handler until OS TCP timeout (~30 s). 3. **Six identical `TaskRequest` object literals** across event handlers — 12 fields × 6 occurrences = ~72 lines of boilerplate. ## Acceptance criteria ### BoundedMap utility - [ ] Introduce a small `BoundedMap<K, V>(maxSize, ttlMs?)` class (can live in `src/util/bounded-map.ts` or inline in `webhook-ci.ts` if #A has already landed). On `set`, if size exceeds `maxSize`, evict the oldest entry. If `ttlMs` is provided, also evict entries older than `ttlMs` on write. - [ ] Replace `_fixCiDispatched` (plain `Map`) with `BoundedMap<string, number>(500, 3_600_000)` — cap matches the 1 h dedup window plus headroom. - [ ] Replace `_reviewFallbacks` (plain `Map`) with either `BoundedMap` or keep as-is if the existing arm-cancels-prior-timer logic is already sufficient. Document the choice in a comment. - [ ] Unit tests: `BoundedMap` evicts on size and on TTL; does not mis-evict entries that are still within TTL. ### Fetch timeouts - [ ] Add `signal: AbortSignal.timeout(5_000)` to every `fetch()` call in `src/webhook.ts` (or the split files if #A landed first). If #A landed and introduced `src/forgejo-api.ts`, add the timeout there exactly once and verify all callers go through it. - [ ] Manual verification note in PR: `curl https://httpbin.org/delay/10` takes ≥ 10 s; confirm our handler bails at 5 s with a clear log line, not a hang. ### buildTaskRequest helper - [ ] Extract `buildTaskRequest(agent: Agent, repo: string, issueOrPr: number, task: string, overrides?: Partial<TaskRequest>): TaskRequest` into either `src/webhook-handlers.ts` (if #A landed) or `src/webhook.ts`. It must: - Populate `forgejo_token`, `forgejo_mcp_command`, `forgejo_url`, `callback_url`, `role`, `git_name`, `git_email`, `branch_prefix`, `system_prompt`, `model`, `container` from the config for the given agent. - Accept overrides to let handlers tweak `stateless_session` (review) and other per-event fields. - [ ] Replace all six in-line literals with calls to the helper. Each handler drops from ~15 lines to ~3. ### Tests - [ ] `BoundedMap` has dedicated tests (size eviction, TTL eviction, get/set/has semantics). - [ ] Existing webhook tests continue to pass; no new assertions required beyond the `BoundedMap` tests. ## Out of scope - The `webhook.ts` structural split (see **#A**). This issue lives in whatever the current file layout is — if #A lands first, do the work in the new files; otherwise do it in `webhook.ts`. - Deeper test coverage for CI gate (see **#D**). ## References - Codebase audit 2026-04-18. ## Dependencies - **Not blocked by:** anything (compatible with either pre- or post-#A layout). - **Sibling:** `#A`, `#C`, `#D`. - **Branch off:** `main`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#35
No description provided.