Skip to content

Harden existing isTimeoutError against context.DeadlineExceeded without caller obligation #6425

Description

@fullsend-ai-retro

What happened

While investigating why the code agent produced the context.DeadlineExceeded bug in PR #6217, analysis revealed that the existing isTimeoutError function in internal/forge/github/github.go (around line 295) has the same vulnerability. It uses the Timeout() bool interface check without guarding against context errors.

The function's doc comment states "Callers must check ctx.Err() first," which shifts the burden to callers. The code agent replicated this pattern into the new public forge.IsTransient() function but did not replicate the caller obligation — creating the bug that the review agent caught.

The new IsTransient function now has the correct guard (errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) before the Timeout() check). But isTimeoutError still relies on caller discipline.

What could go better

The caller-must-check-first contract on isTimeoutError is fragile. It already caused a bug by proxy — the code agent treated the function's implementation as a reusable pattern without carrying over the documented caller obligation. Any future caller (human or agent) that uses isTimeoutError without checking ctx.Err() first would silently retry context cancellations.

Confidence: Medium-high. The fragility is demonstrated by the PR #6217 bug. Making isTimeoutError self-contained (adding the context-error guard internally) eliminates the caller obligation and prevents the pattern from propagating incorrectly again.

Uncertainty: The function may have few callers, all of which correctly check ctx.Err() first. In that case the risk is low but the fix is also low-cost. There may also be a performance reason for the current design (avoiding redundant errors.Is checks), though this seems unlikely to matter in practice.

Proposed change

Add context-error guards to isTimeoutError in internal/forge/github/github.go, matching the pattern now used in forge.IsTransient():

func isTimeoutError(err error) bool {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
        return false
    }
    var te interface{ Timeout() bool }
    return errors.As(err, &te) && te.Timeout()
}

Update the doc comment to remove the "Callers must check ctx.Err() first" obligation, since the function is now self-contained. Verify that existing callers still behave correctly (the added guard should be a no-op for callers that already check ctx.Err()).

Validation criteria

  1. All existing tests for isTimeoutError and its callers continue to pass.
  2. A new test case verifies that isTimeoutError(context.DeadlineExceeded) returns false.
  3. The doc comment no longer requires callers to pre-check ctx.Err().
  4. go vet and the race detector report no issues.

Generated by retro agent from #6217

Metadata

Metadata

Assignees

No one assigned

    Labels

    buggoPull requests that update go codepriority/lowNice to have, address when convenientready-for-triageTriggers triage agent dispatchready-to-codeTriaged and ready for the code agent

    Type

    No type

    Projects

    Status
    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions