Skip to content

Part of #1611: request-path panic gate (CI robustness)#1848

Merged
madmax983 merged 8 commits into
trunk-devfrom
feat/1611-panic-gate
Jul 13, 2026
Merged

Part of #1611: request-path panic gate (CI robustness)#1848
madmax983 merged 8 commits into
trunk-devfrom
feat/1611-panic-gate

Conversation

@madmax983

Copy link
Copy Markdown
Owner

Before / After

Before: a .unwrap() / .expect() / panic! added to a request-path module (middleware, extractors, stores, scheduler/jobs, channels) compiled and passed CI silently — nothing enforced the "panic-free request path" invariant that CONTRIBUTING.md already described. A poisoned lock in one request permanently broke framework-owned shared state for every later request.

After: production code in 25 request-path modules is gated by clippy restriction lints in the existing CI lint job — an unannotated panic-capable call turns CI red. Poison-prone locks now recover instead of propagating (unwrap_or_else(PoisonError::into_inner)), and a regression test proves an induced panic no longer breaks the idempotency store for subsequent requests.

How

  • Gate header #[cfg_attr(not(test), deny(clippy::unwrap_used, expect_used, panic, unreachable, todo, unimplemented, indexing_slicing))] on each gated module — scopes the deny to the production (lib) target, auto-exempting tests/benches.
  • scripts/check-panic-gate.sh enforces the canonical request-path module manifest (fails if a gated module loses its header); wired as a step in the lint job alongside fmt/clippy — no new external services.
  • Burn-down: poison locks → PoisonError::into_inner (the established circuit_breaker idiom); provably-infallible builder/crypto/bounds-checked sites → narrow #[allow(…, reason="…")]; two real fixes (error_page_filter find('[') → let-else fallback, extract markup sniff rewritten to avoid slicing).
  • CONTRIBUTING.md documents the invariant, the "request path" definition vs exempt surfaces, the denied lint set, and the per-site justification syntax.

Acceptance criteria (#1611)

  • AC1 CI reddens on a new unannotated panic-capable call — done; verified by injecting an unannotated .unwrap() into a gated module and confirming clippy fails, then reverting.
  • AC2 request path defined + documented — done (manifest script + CONTRIBUTING).
  • AC3 existing findings burned down — done for idempotency (the named module, incl. the near-Duration::MAX TTL path already fixed on trunk) + 23 other modules; inbound_mail deferred (below).
  • AC4 lock-poisoning contract — done: idempotency::tests::poisoned_lock_does_not_break_subsequent_requests + into_inner conversions.
  • AC5 contributor docs — done.
  • AC6 same workflow as fmt/clippy, no new services — done (step in the lint job).

Deferred (why "Part of" not "Closes")

  • inbound_mail.rs — its MIME parser has ~57 index/slice sites over untrusted input; blanket-annotating them as safe would be unsound. Needs a dedicated audit. Header removed; excluded from the manifest.
  • arithmetic_side_effects (unchecked time/duration/integer arithmetic named in AC1) — too noisy to enable now; currently guarded by the saturating-arithmetic idiom (see idempotency::saturating_deadline) + review.
  • Feature-gated modules (mail/channels/offline-sync/ws) carry the gate header and were burned down + verified with their features enabled, but the default CI lint job doesn't build those features, so clippy actively enforces them only when built with features. The manifest script still enforces header presence unconditionally.

Test evidence

  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --all-targets -- -D warnings green
  • ./scripts/check-panic-gate.shpanic-gate: 25 request-path modules gated
  • cargo test -p autumn-web --lib idempotency → 16 passed (incl. the new poison-recovery test)

Generated by Claude Code

claude added 4 commits July 13, 2026 02:50
Introduce the #1611 request-path panic gate: a per-module header that opts
each runtime module's production target into a set of panic-class clippy
denials (unwrap_used, expect_used, panic, unreachable, todo, unimplemented,
indexing_slicing) via cfg_attr(not(test), deny(...)), auto-exempting the
module's own test submodule.

- scripts/check-panic-gate.sh holds the canonical REQUEST_PATH_MODULES
  manifest and fails if any gated file is missing or has lost its gate
  header, so the gate cannot be silently dropped. Panic detection itself is
  done by clippy in the same lint job.
- Wire the manifest check into the existing CI lint job, right after clippy
  (same workflow as fmt/clippy, no new services).
- Add the gate header to the request-path modules that were already clean.
Gate the idempotency middleware and eliminate its production panic sites:

- Recover from lock poisoning with unwrap_or_else(PoisonError::into_inner)
  on the in-memory store's entries/in_flight locks and the deferred-commit
  lock, mirroring the circuit_breaker idiom, so one panicking lock holder
  cannot poison shared state for every later request (AC4 contract).
- Justify the infallible http::Response builder unwraps (responses built
  from static status/body) with narrowly scoped allow(clippy::unwrap_used).
- Add poisoned_lock_does_not_break_subsequent_requests: poisons an internal
  lock via a panic in catch_unwind, then asserts a normal store round-trip
  still succeeds, proving into_inner recovery.
Gate the remaining request-path modules and resolve their panic-class sites:

- Convert lock-poisoning expects to unwrap_or_else(PoisonError::into_inner)
  in job, job_tracking, channels, and mail (queue-slot, registry, buffer,
  suppression, and metrics locks).
- Justify provably-infallible sites at the narrowest scope with
  allow(..., reason = "..."): crypto/RNG and base64 in mail; the reqwest
  client build in sync/engine; static 503 responses and future-poll-after-
  Ready expects in maintenance/load_shed; bounds-checked indexing in
  scheduler, metrics, channels, job's weighted round-robin, and mail's
  token decode; the eager SessionLayer misconfiguration expect in session.
- error_page_filter: replace an unchecked find('[').unwrap() with a let-else
  fallback and justify the bounds-checked path indexing.
- extract: rewrite prefix_looks_like_markup to find the first non-whitespace
  byte without slicing, removing the panic surface entirely.

inbound_mail is intentionally deferred (not gated): its MIME parser has an
extensive untrusted-input indexing surface beyond mechanical justification;
gating it is tracked as follow-up so CI stays green over completeness.
Add a "Request-path panic gate" section describing the #1611 invariant: the
definition of the request path and its exempt surfaces (tests, benches,
examples, build scripts, autumn-cli, app-author code), the denied clippy lint
set and how cfg_attr(not(test), ...) scopes it, enforcement via clippy plus
scripts/check-panic-gate.sh in the CI lint job, how to justify an exception or
recover poisoned locks, and a note that arithmetic_side_effects remains a
deferred follow-up guarded by the saturating-arithmetic idiom and review.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@madmax983 madmax983 marked this pull request as ready for review July 13, 2026 06:49
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22efa8a3b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/check-panic-gate.sh Outdated
Comment on lines +66 to +67
&& grep -q 'clippy::unwrap_used' "$module" \
&& grep -q 'clippy::indexing_slicing' "$module" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate every denied panic lint in the gate

If a gated module later drops clippy::panic, clippy::expect_used, clippy::unreachable, clippy::todo, or clippy::unimplemented from its header but keeps unwrap_used and indexing_slicing, this script still exits successfully even though that panic class is no longer enforced by Clippy. That defeats the new “missing or incomplete gate” check for those cases, so please validate the full lint set or match a canonical header rather than only these two lints.

Useful? React with 👍 / 👎.

check-panic-gate.sh now requires every canonical clippy panic lint
(unwrap_used, expect_used, panic, unreachable, todo, unimplemented,
indexing_slicing) to be present in each gated module's deny header,
rather than spot-checking a two-lint subset. A gated module that drops
any single panic lint while keeping the others now fails the gate with a
message naming the module and the specific missing lint, closing the
silent-weakening gap flagged in PR review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7a7a4e09f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +85 to +87
for lint in "${REQUIRED_PANIC_LINTS[@]}"; do
grep -qF "$lint" "$module" \
|| die "gate header in $module is missing required panic lint '$lint'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require lints in the deny header

In this revision the script checks every lint token, but grep -qF "$lint" "$module" scans the entire module rather than the #![cfg_attr(not(test), deny(...))] header. Modules in this commit now contain legitimate #[allow(clippy::expect_used, ...)] / #[allow(clippy::indexing_slicing, ...)] annotations, so removing one of those lints from the deny header would still satisfy this check while Clippy would stop enforcing that panic class for new production code. Please validate a canonical header block, or otherwise ensure each token is inside the deny list.

Useful? React with 👍 / 👎.

check-panic-gate.sh previously validated each required panic lint with a
whole-file `grep -qF "clippy::<lint>"`. Now that gated modules carry
legitimate per-site `#[allow(clippy::expect_used, reason = "…")]` /
`#[allow(clippy::indexing_slicing, …)]` annotations, a whole-file scan
could let such an `#[allow(...)]` spoof a lint that was actually dropped
from the module's `#![cfg_attr(not(test), deny(...))]` header — a false
pass.

Extract the `#![cfg_attr(` … `)]` deny block per module (buffered so an
unterminated header fails hard instead of running to EOF) and assert each
`clippy::<lint>` from REQUIRED_PANIC_LINTS appears WITHIN that block only.
Still require the `autumn-panic-gate:` marker and the deny() opener, and
die naming the module plus the specific missing lint. Per PR review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3285bbc4c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread autumn/src/idempotency.rs
Comment on lines +1835 to +1838
#[allow(
clippy::unwrap_used,
reason = "infallible: response built from stored record status/headers/body"
)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid stored replay records

This exception makes the panic gate accept response_from_record as infallible, but IdempotencyRecord exposes status: u16 and raw (String, Vec<u8>) headers, so a custom or corrupted idempotency store can return an invalid status or header bytes. In that replay path Response::builder() stores the error and the existing .body(...).unwrap() panics during request handling; please validate/skip invalid stored records or return a recovery response instead of exempting this unwrap.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5837c55eec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Panic-free request-path gate
run: ./scripts/check-panic-gate.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run clippy for feature-gated request modules

I checked the lint job here: this new gate only runs the manifest script after a default-feature cargo clippy, but several files in the manifest are not compiled by default (for example channels is behind ws, mail behind mail, and sync behind offline-sync in autumn/src/lib.rs/autumn/Cargo.toml). In those contexts, adding an unannotated unwrap() to one of those request-path modules still leaves the header intact so this step passes, and rustc builds in later all-features jobs do not enforce Clippy lints; please add a feature/all-features Clippy pass for the gated modules or split feature-specific lint runs.

Useful? React with 👍 / 👎.

@madmax983 madmax983 merged commit 4f6d6f1 into trunk-dev Jul 13, 2026
28 of 29 checks passed
@madmax983 madmax983 deleted the feat/1611-panic-gate branch July 13, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants