Skip to content

[fix]: core - recover from closeBodyStream panic in idle-timeout timer goroutine - #4616

Merged
akshaydeo merged 3 commits into
maximhq:devfrom
KamilDziemba:fix/idle-timeout-timer-panic
Jun 23, 2026
Merged

akshaydeo merged 3 commits into
maximhq:devfrom
KamilDziemba:fix/idle-timeout-timer-panic

Conversation

@KamilDziemba

@KamilDziemba KamilDziemba commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

An orphaned idle-timeout timer can crash the whole process. When the
time.AfterFunc registered by NewIdleTimeoutReader fires after the
stream's connection has already been released to / reused from the fasthttp
pool, the closeBodyStream(r.bodyStream, ...) call invokes the body's
CloseWithError, which nil-dereferences in fasthttp's
(*HostClient).CloseConn. Because that runs in the timer goroutine, the panic
is unrecoverable by any caller and takes the entire process down.

We observed this crashing a gateway built on bifrost under sustained streaming
load — the crash fires minutes after the originating stream has completed, when
the stale idle timer finally elapses, so it is hard to correlate with any
single request.

PR #3677 ("fix idle timeout panic") added a recover() to the
idleTimeoutReader.Read() path, but the AfterFunc's own closeBodyStream
call has no such guard. This PR adds the companion recover() inside the timer
callback so a stale idle timer can never crash the process.

Changes

  • core/providers/utils/utils.go — wrap the closeBodyStream call inside
    NewIdleTimeoutReader's time.AfterFunc with defer func() { _ = recover() }().
    This is the timer-goroutine counterpart to the Read()-path recover from
    fix idle timeout panic #3677. The BifrostContextKeyConnectionClosed signal and timerDone close
    semantics are unchanged — the recover only prevents an unrecoverable
    cross-goroutine panic.
  • core/providers/utils/idle_timeout_reader_test.go — add
    TestIdleTimeoutReader_RecoversCloseStreamPanicOnTimerFire plus a
    timerPanicCloser stub whose CloseWithError panics (mimicking the fasthttp
    CloseConn nil-deref) and which implements streamCloserWithError so
    closeBodyStream takes the CloseWithError branch the idle timer actually
    hits.
  • core/changelog.md — changelog entry for the fix (per docs/contributing/raising-a-pr.mdx).

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

The new test fails (the test process crashes with
panic: simulated fasthttp CloseConn nil-deref) without the fix, and passes
with it.

cd core
go test ./providers/utils/ -run IdleTimeout -race -v
# => ok  github.com/maximhq/bifrost/core/providers/utils

To see it fail without the fix, temporarily remove the
defer func() { _ = recover() }() line and re-run — the timer goroutine panic
takes the test binary down:

panic: simulated fasthttp CloseConn nil-deref
goroutine NN [running]:
FAIL    github.com/maximhq/bifrost/core/providers/utils

Breaking changes

  • Yes
  • No

Related issues

Closes #4617.

Follow-up to #3677 (which guarded only the Read() path). This closes the
remaining timer-goroutine crash path.

Security considerations

None. The change only swallows a panic in a background timer goroutine; it does
not alter auth, secrets handling, or what data is read/written. The connection
is already being torn down on the timeout path, and BifrostContextKeyConnectionClosed
is still set before the recover.

Checklist

  • I added/updated tests where appropriate
  • I verified builds succeed (Go: go vet + go test ./providers/utils/ -race)
  • I followed the contribution guidelines (docs/contributing/raising-a-pr.mdx, code-conventions.mdx) and added a core/changelog.md entry
  • No documentation changes needed (behavioral bug fix in an internal teardown path)
  • I verified the full CI pipeline (make test-all / make lint) passes locally — only the affected package (core/providers/utils) was run + vetted + gofmt'd

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 526d0de4-e36a-4e36-9044-ca1549c7868f

📥 Commits

Reviewing files that changed from the base of the PR and between 4046ffd and 534aa97.

📒 Files selected for processing (1)
  • core/providers/utils/idle_timeout_reader_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/providers/utils/idle_timeout_reader_test.go

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved stability when an idle-timeout triggers: any panic during internal stream-closing is now recovered, preventing rare process crashes. The incident is recorded in debug logs while the timeout behavior continues as expected.
  • Tests

    • Added coverage for recovery from panics on the idle-timeout close path, including verification that the recovered panic is logged.
  • Chores

    • Updated the changelog with the idle-timeout panic fix.

Walkthrough

A deferred recover() is added inside the time.AfterFunc callback in NewIdleTimeoutReader to catch panics from closeBodyStream when a stale timer fires on an already-released fasthttp connection. Test helpers timerPanicCloser and captureLogger are introduced, with two regression tests verifying panic containment and logging. The changelog is updated.

Changes

Idle timeout timer panic recovery

Layer / File(s) Summary
Timer callback panic recovery implementation
core/providers/utils/utils.go
NewIdleTimeoutReader's AfterFunc goroutine now defers a recover() before calling closeBodyStream, preventing process crashes from orphaned timers firing on already-released connections.
Regression tests, test helpers, and changelog
core/providers/utils/idle_timeout_reader_test.go, core/changelog.md
timerPanicCloser simulates a fasthttp nil-deref panic on CloseWithError, and captureLogger records debug output. TestIdleTimeoutReader_RecoversCloseStreamPanicOnTimerFire wires the panic-throwing closer through NewIdleTimeoutReader to confirm containment. TestIdleTimeoutReader_LogsRecoveredTimerPanic verifies recovered panic details are logged. Changelog entry documents the fix.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • maximhq/bifrost#3677: Added the original recover() on the Read() path of idleTimeoutReader; this PR adds the companion guard for the timer-callback path that was missed.
  • maximhq/bifrost#3595: Modifies the same NewIdleTimeoutReader idle-timeout/connection-close ordering in core/providers/utils/utils.go, adjacent to the code this PR changes.

Suggested reviewers

  • danpiths
  • akshaydeo

Poem

🐇 A timer once fired on a stream long gone,
A nil-deref lurked and the process was done.
But now with a recover() set neatly in place,
The panic is caught with exceptional grace.
No crash in the night, no goroutine astray —
The rabbit patched it and hops on its way! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding panic recovery to the idle-timeout timer goroutine's closeBodyStream call, which is the primary objective of the PR.
Description check ✅ Passed The PR description is comprehensive, covering summary, detailed changes, test validation, and checklist items. It aligns well with the template structure and provides thorough context about the bug and fix.
Linked Issues check ✅ Passed The PR directly addresses issue #4617 by implementing the proposed fix: wrapping closeBodyStream with defer recover() in the timer callback, adding regression tests with timerPanicCloser, and updating the changelog.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the idle-timeout timer panic issue. The changelog entry, test additions, and utils.go modification all serve the stated objective with no extraneous changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CLAassistant

CLAassistant commented Jun 22, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@KamilDziemba KamilDziemba changed the title fix(core): recover from closeBodyStream panic in idle-timeout timer goroutine [fix]: core - recover from closeBodyStream panic in idle-timeout timer goroutine Jun 22, 2026
…r goroutine

An orphaned idle-timeout timer can fire after the stream's connection has
already been released to / reused from the fasthttp pool. closeBodyStream
then calls CloseWithError, which nil-derefs in (*HostClient).CloseConn and
panics. Because this runs in the time.AfterFunc timer goroutine, the panic
is unrecoverable by callers and crashes the whole process -- observed taking
down a gateway under sustained streaming load (idle timer firing ~minutes
after a stream completed).

maximhq#3677 added a recover() to the idleTimeoutReader.Read() path but not to the
AfterFunc's own closeBodyStream call. This adds the companion recover() in
the timer callback so a stale idle timer can never crash the process.

Adds TestIdleTimeoutReader_RecoversCloseStreamPanicOnTimerFire, which crashes
the test process without the fix and passes with it.

Affected packages:
- core/providers/utils/ - the fix + regression test
- core/changelog.md - changelog entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 22, 2026
@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the change is a narrow, well-scoped addition of a recovery guard inside an existing timer goroutine, with no modifications to data flow, auth, or connection lifecycle semantics.

The fix is structurally sound: the recover is deferred inside the once.Do closure so timerDone (deferred at the outer goroutine level) is still correctly closed after recovery, BifrostContextKeyConnectionClosed is set before the guarded call, and the recovered value is logged rather than silently discarded. The two new tests are deterministic (channel-based, not sleep-based) and cover both process-survival and observability. No pre-existing invariants are altered.

No files require special attention.

Important Files Changed

Filename Overview
core/providers/utils/utils.go Adds defer/recover guard inside AfterFunc's once.Do closure; BifrostContextKeyConnectionClosed is set before the guarded call and timerDone is closed by the outer defer regardless of recovery — logic is correct.
core/providers/utils/idle_timeout_reader_test.go Two new deterministic tests using a channel-signalled panic stub; captureLogger validates panic is logged; non-parallel logger-swap test correctly sequenced and uses atomic SetLogger/getLogger — no data races.
core/changelog.md Single changelog entry added for the bug fix, following existing format.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Timer as time.AfterFunc goroutine
    participant Once as r.once.Do closure
    participant CBS as closeBodyStream
    participant HC as fasthttp HostClient
    participant TD as timerDone (chan)

    Note over Timer: idle timeout elapses
    Timer->>Once: "r.once.Do(func(){})"
    Once->>Once: r.fired.Store(true)
    Once->>Once: ctx.SetValue(ConnectionClosed, true)
    Once->>Once: defer recover() installed
    Once->>CBS: closeBodyStream(r.bodyStream, ErrStreamIdleTimeout)
    CBS->>HC: CloseWithError(err)
    HC-->>Once: panic: nil-deref in CloseConn (pool reuse)
    Once->>Once: recover() catches panic
    Once->>Once: getLogger().Debug(recovered panic...)
    Once-->>Timer: once.Do returns normally
    Timer->>TD: timerDoneOnce.Do(close(timerDone))
    Note over TD: cleanup() unblocks
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Timer as time.AfterFunc goroutine
    participant Once as r.once.Do closure
    participant CBS as closeBodyStream
    participant HC as fasthttp HostClient
    participant TD as timerDone (chan)

    Note over Timer: idle timeout elapses
    Timer->>Once: "r.once.Do(func(){})"
    Once->>Once: r.fired.Store(true)
    Once->>Once: ctx.SetValue(ConnectionClosed, true)
    Once->>Once: defer recover() installed
    Once->>CBS: closeBodyStream(r.bodyStream, ErrStreamIdleTimeout)
    CBS->>HC: CloseWithError(err)
    HC-->>Once: panic: nil-deref in CloseConn (pool reuse)
    Once->>Once: recover() catches panic
    Once->>Once: getLogger().Debug(recovered panic...)
    Once-->>Timer: once.Do returns normally
    Timer->>TD: timerDoneOnce.Do(close(timerDone))
    Note over TD: cleanup() unblocks
Loading

Reviews (3): Last reviewed commit: "[test]: core - make idle-timeout timer-p..." | Re-trigger Greptile

Comment thread core/providers/utils/utils.go Outdated
Comment thread core/providers/utils/idle_timeout_reader_test.go
Address review feedback: the bare `_ = recover()` swallowed the panic value
with no diagnostics. Unlike the Read() path, the timer goroutine cannot
re-panic an unexpected value (that would crash the process — the bug being
fixed), so log the recovered value at debug level via getLogger(), matching
the existing teardown-recover pattern in this file (EnsureStreamFinalizerCalled,
ReleaseStreamingResponse). This leaves a forensic trace if a future, unrelated
panic is ever introduced into closeBodyStream or its callees.

Adds TestIdleTimeoutReader_LogsRecoveredTimerPanic, which installs a capturing
logger and asserts the recovered value is logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback: both timer-panic tests slept a fixed 50ms then
called cleanup(). On a slow runner the 10ms timer might not have fired yet,
so cleanup()'s timer.Stop() returns true, the test returns without any panic
occurring, and it passes while silently skipping the recover path it guards.

timerPanicCloser now closes a `called` channel the instant CloseWithError is
entered (just before the panic). The tests wait on that channel (with a 2s
deadline) before cleanup(), so the timer is only stopped after it has
demonstrably fired — a definitive signal the guarded path ran, and it also
removes the arbitrary sleep. Verified stable across -race -count=20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/providers/utils/idle_timeout_reader_test.go`:
- Around line 469-470: Replace the fixed time.Sleep(50 * time.Millisecond) call
at line 469 with a condition-based synchronization mechanism that waits
deterministically for the timer callback to complete. Instead of relying on a
hardcoded sleep duration that may be insufficient under slow CI scheduling, use
a synchronization primitive such as a sync.WaitGroup, atomic flag, or channel to
signal when the callback execution has finished. This ensures the test waits
until the actual callback (recover + log) has returned before cleanup() is
called, making the assertion deterministic and eliminating the race condition
where cleanup() might stop the timer before the callback executes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 812e0b84-9746-4c59-ab98-ca1b8b962b94

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3aa06 and 4046ffd.

📒 Files selected for processing (2)
  • core/providers/utils/idle_timeout_reader_test.go
  • core/providers/utils/utils.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/providers/utils/utils.go

Comment thread core/providers/utils/idle_timeout_reader_test.go Outdated
@akshaydeo
akshaydeo merged commit ac1dbec into maximhq:dev Jun 23, 2026
6 checks passed
akshaydeo pushed a commit that referenced this pull request Jun 24, 2026
…r goroutine (#4616)

* [fix]: core - recover from closeBodyStream panic in idle-timeout timer goroutine

An orphaned idle-timeout timer can fire after the stream's connection has
already been released to / reused from the fasthttp pool. closeBodyStream
then calls CloseWithError, which nil-derefs in (*HostClient).CloseConn and
panics. Because this runs in the time.AfterFunc timer goroutine, the panic
is unrecoverable by callers and crashes the whole process -- observed taking
down a gateway under sustained streaming load (idle timer firing ~minutes
after a stream completed).

#3677 added a recover() to the idleTimeoutReader.Read() path but not to the
AfterFunc's own closeBodyStream call. This adds the companion recover() in
the timer callback so a stale idle timer can never crash the process.

Adds TestIdleTimeoutReader_RecoversCloseStreamPanicOnTimerFire, which crashes
the test process without the fix and passes with it.

Affected packages:
- core/providers/utils/ - the fix + regression test
- core/changelog.md - changelog entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [fix]: core - log recovered panic value in idle-timeout timer goroutine

Address review feedback: the bare `_ = recover()` swallowed the panic value
with no diagnostics. Unlike the Read() path, the timer goroutine cannot
re-panic an unexpected value (that would crash the process — the bug being
fixed), so log the recovered value at debug level via getLogger(), matching
the existing teardown-recover pattern in this file (EnsureStreamFinalizerCalled,
ReleaseStreamingResponse). This leaves a forensic trace if a future, unrelated
panic is ever introduced into closeBodyStream or its callees.

Adds TestIdleTimeoutReader_LogsRecoveredTimerPanic, which installs a capturing
logger and asserts the recovered value is logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [test]: core - make idle-timeout timer-panic tests deterministic

Address review feedback: both timer-panic tests slept a fixed 50ms then
called cleanup(). On a slow runner the 10ms timer might not have fired yet,
so cleanup()'s timer.Stop() returns true, the test returns without any panic
occurring, and it passes while silently skipping the recover path it guards.

timerPanicCloser now closes a `called` channel the instant CloseWithError is
entered (just before the panic). The tests wait on that channel (with a 2s
deadline) before cleanup(), so the timer is only stopped after it has
demonstrably fired — a definitive signal the guarded path ran, and it also
removes the arbitrary sleep. Verified stable across -race -count=20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jun 24, 2026
17 tasks
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
…r goroutine (maximhq#4616)

* [fix]: core - recover from closeBodyStream panic in idle-timeout timer goroutine

An orphaned idle-timeout timer can fire after the stream's connection has
already been released to / reused from the fasthttp pool. closeBodyStream
then calls CloseWithError, which nil-derefs in (*HostClient).CloseConn and
panics. Because this runs in the time.AfterFunc timer goroutine, the panic
is unrecoverable by callers and crashes the whole process -- observed taking
down a gateway under sustained streaming load (idle timer firing ~minutes
after a stream completed).

maximhq#3677 added a recover() to the idleTimeoutReader.Read() path but not to the
AfterFunc's own closeBodyStream call. This adds the companion recover() in
the timer callback so a stale idle timer can never crash the process.

Adds TestIdleTimeoutReader_RecoversCloseStreamPanicOnTimerFire, which crashes
the test process without the fix and passes with it.

Affected packages:
- core/providers/utils/ - the fix + regression test
- core/changelog.md - changelog entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [fix]: core - log recovered panic value in idle-timeout timer goroutine

Address review feedback: the bare `_ = recover()` swallowed the panic value
with no diagnostics. Unlike the Read() path, the timer goroutine cannot
re-panic an unexpected value (that would crash the process — the bug being
fixed), so log the recovered value at debug level via getLogger(), matching
the existing teardown-recover pattern in this file (EnsureStreamFinalizerCalled,
ReleaseStreamingResponse). This leaves a forensic trace if a future, unrelated
panic is ever introduced into closeBodyStream or its callees.

Adds TestIdleTimeoutReader_LogsRecoveredTimerPanic, which installs a capturing
logger and asserts the recovered value is logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [test]: core - make idle-timeout timer-panic tests deterministic

Address review feedback: both timer-panic tests slept a fixed 50ms then
called cleanup(). On a slow runner the 10ms timer might not have fired yet,
so cleanup()'s timer.Stop() returns true, the test returns without any panic
occurring, and it passes while silently skipping the recover path it guards.

timerPanicCloser now closes a `called` channel the instant CloseWithError is
entered (just before the panic). The tests wait on that channel (with a 2s
deadline) before cleanup(), so the timer is only stopped after it has
demonstrably fired — a definitive signal the guarded path ran, and it also
removes the arbitrary sleep. Verified stable across -race -count=20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
…r goroutine (maximhq#4616)

* [fix]: core - recover from closeBodyStream panic in idle-timeout timer goroutine

An orphaned idle-timeout timer can fire after the stream's connection has
already been released to / reused from the fasthttp pool. closeBodyStream
then calls CloseWithError, which nil-derefs in (*HostClient).CloseConn and
panics. Because this runs in the time.AfterFunc timer goroutine, the panic
is unrecoverable by callers and crashes the whole process -- observed taking
down a gateway under sustained streaming load (idle timer firing ~minutes
after a stream completed).

maximhq#3677 added a recover() to the idleTimeoutReader.Read() path but not to the
AfterFunc's own closeBodyStream call. This adds the companion recover() in
the timer callback so a stale idle timer can never crash the process.

Adds TestIdleTimeoutReader_RecoversCloseStreamPanicOnTimerFire, which crashes
the test process without the fix and passes with it.

Affected packages:
- core/providers/utils/ - the fix + regression test
- core/changelog.md - changelog entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [fix]: core - log recovered panic value in idle-timeout timer goroutine

Address review feedback: the bare `_ = recover()` swallowed the panic value
with no diagnostics. Unlike the Read() path, the timer goroutine cannot
re-panic an unexpected value (that would crash the process — the bug being
fixed), so log the recovered value at debug level via getLogger(), matching
the existing teardown-recover pattern in this file (EnsureStreamFinalizerCalled,
ReleaseStreamingResponse). This leaves a forensic trace if a future, unrelated
panic is ever introduced into closeBodyStream or its callees.

Adds TestIdleTimeoutReader_LogsRecoveredTimerPanic, which installs a capturing
logger and asserts the recovered value is logged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [test]: core - make idle-timeout timer-panic tests deterministic

Address review feedback: both timer-panic tests slept a fixed 50ms then
called cleanup(). On a slow runner the 10ms timer might not have fired yet,
so cleanup()'s timer.Stop() returns true, the test returns without any panic
occurring, and it passes while silently skipping the recover path it guards.

timerPanicCloser now closes a `called` channel the instant CloseWithError is
entered (just before the panic). The tests wait on that channel (with a 2s
deadline) before cleanup(), so the timer is only stopped after it has
demonstrably fired — a definitive signal the guarded path ran, and it also
removes the arbitrary sleep. Verified stable across -race -count=20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

[Bug]: idle-timeout timer goroutine can panic in closeBodyStream and crash the process

3 participants