-
Notifications
You must be signed in to change notification settings - Fork 94
fix(#7240): detect caller deadlines via ctx.Err() helper #7241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fullsend-ai-coder
wants to merge
2
commits into
main
Choose a base branch
from
agent/7240-ctxerr-deadline-helper
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // Package ctxerr provides a single predicate for detecting whether an | ||
| // operation failed because the caller's own context was canceled or | ||
| // exceeded its deadline. | ||
| // | ||
| // Prefer this over errors.Is against context.DeadlineExceeded. Go's | ||
| // net/http Client.Timeout error unwraps to context.DeadlineExceeded | ||
| // from an internal context even when the caller's context is still | ||
| // live, so errors.Is cannot tell a caller-context expiry from a | ||
| // transport timeout. The same false positive appears once an | ||
| // intermediate type (for example mintclient.retryableError) implements | ||
| // Unwrap. See #6424, #6425, and #7240. | ||
| package ctxerr | ||
|
|
||
| import "context" | ||
|
|
||
| // IsDeadlineExceededOrCanceled reports whether err is non-nil and ctx | ||
| // itself is done (canceled or past its deadline). | ||
| // | ||
| // A nil err is never a context failure. A live ctx is never reported | ||
| // as expired, even when err wraps context.DeadlineExceeded or | ||
| // implements Timeout() bool — those are nested timeouts (for example | ||
| // net/http Client.Timeout), not this call's context. | ||
| func IsDeadlineExceededOrCanceled(ctx context.Context, err error) bool { | ||
| if err == nil { | ||
| return false | ||
| } | ||
| return ctx.Err() != nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package ctxerr | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // httpTimeoutErr mimics net/http's Client.Timeout error: it unwraps to | ||
| // context.DeadlineExceeded and implements Timeout() bool. errors.Is | ||
| // against DeadlineExceeded matches it even when the caller's context | ||
| // is still live — the false positive this helper exists to prevent. | ||
| type httpTimeoutErr struct { | ||
| err error | ||
| } | ||
|
|
||
| func (e *httpTimeoutErr) Error() string { | ||
| return e.err.Error() + " (Client.Timeout exceeded while awaiting headers)" | ||
| } | ||
|
|
||
| func (e *httpTimeoutErr) Unwrap() error { return e.err } | ||
|
|
||
| func (e *httpTimeoutErr) Timeout() bool { return true } | ||
|
|
||
| // retryableWrapper mimics mintclient.retryableError: Unwrap only, no | ||
| // Timeout() method. After Unwrap is added, errors.Is against | ||
| // DeadlineExceeded matches a wrapped transport timeout. | ||
| type retryableWrapper struct { | ||
| error | ||
| } | ||
|
|
||
| func (e *retryableWrapper) Unwrap() error { return e.error } | ||
|
|
||
| func TestIsDeadlineExceededOrCanceled(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| cancelledCtx, cancel := context.WithCancel(context.Background()) | ||
| cancel() | ||
|
|
||
| expiredCtx, expire := context.WithDeadline(context.Background(), time.Now().Add(-time.Second)) | ||
| t.Cleanup(expire) | ||
|
|
||
| liveDE := context.DeadlineExceeded | ||
| wrappedDE := fmt.Errorf("request failed: %w", context.DeadlineExceeded) | ||
| httpTimeout := &httpTimeoutErr{err: context.DeadlineExceeded} | ||
| retryableTimeout := &retryableWrapper{error: httpTimeout} | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| ctx context.Context | ||
| err error | ||
| want bool | ||
| }{ | ||
| { | ||
| name: "nil error with live context", | ||
| ctx: context.Background(), | ||
| err: nil, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "nil error with cancelled context", | ||
| ctx: cancelledCtx, | ||
| err: nil, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "generic error with live context", | ||
| ctx: context.Background(), | ||
| err: errors.New("mint rejected"), | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "generic error with cancelled context is still context-done", | ||
| ctx: cancelledCtx, | ||
| err: errors.New("mint rejected"), | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "DeadlineExceeded with live context is not this call's expiry", | ||
| ctx: context.Background(), | ||
| err: liveDE, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "wrapped DeadlineExceeded with live context is not this call's expiry", | ||
| ctx: context.Background(), | ||
| err: wrappedDE, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "http Client.Timeout wrapping DeadlineExceeded with live context", | ||
| ctx: context.Background(), | ||
| err: httpTimeout, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "retryable Unwrap of http Client.Timeout with live context", | ||
| ctx: context.Background(), | ||
| err: retryableTimeout, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "Canceled with live context is not this call's expiry", | ||
| ctx: context.Background(), | ||
| err: context.Canceled, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "DeadlineExceeded with expired context", | ||
| ctx: expiredCtx, | ||
| err: liveDE, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "wrapped DeadlineExceeded with expired context", | ||
| ctx: expiredCtx, | ||
| err: wrappedDE, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "http Client.Timeout with expired context is this call's expiry", | ||
| ctx: expiredCtx, | ||
| err: httpTimeout, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "Canceled with cancelled context", | ||
| ctx: cancelledCtx, | ||
| err: context.Canceled, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "DeadlineExceeded with cancelled context", | ||
| ctx: cancelledCtx, | ||
| err: liveDE, | ||
| want: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| assert.Equal(t, tt.want, IsDeadlineExceededOrCanceled(tt.ctx, tt.err)) | ||
| }) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low] authorization-tier
The PR widens the surface of an exported function (forge.IsTransient) by adding a required ctx parameter — a larger authorization footprint than issue #7240 explicitly requested (it only asked to migrate call sites to a new helper). This repo's COMMITS.md breaking-change criteria are framed around user-visible CLI/API/config behavior, not internal Go package signatures, and the fix is not achievable without the signature change; the PR body discloses it explicitly ('forge.IsTransient now takes ctx').
Suggested fix: No action needed — confirming this was a deliberate, disclosed choice, which it is. No BREAKING CHANGE marker required.