Skip to content

Commit 285ee1e

Browse files
committed
test: pin the S3 Express session retrieval's caller context
Add TestExpressCreateSessionCallerCancel: a canceled caller aborts the in-flight CreateSession request, and the first-request "?session" query assertion pins that the express arm ran. State the Goexit residual precisely: a caller with no deadline waits indefinitely.
1 parent 9af7844 commit 285ee1e

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

api-cred-context_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"errors"
2323
"net/http"
2424
"net/http/httptest"
25+
"strings"
2526
"sync"
2627
"testing"
2728
"time"
@@ -113,6 +114,54 @@ func TestCredsCancelDoesNotPoisonWaiters(t *testing.T) {
113114
}
114115
}
115116

117+
// expressSessionTransport records the first request's query and blocks
118+
// every request until its context ends.
119+
type expressSessionTransport struct {
120+
started chan struct{}
121+
once sync.Once
122+
firstQuery string
123+
}
124+
125+
func (tr *expressSessionTransport) RoundTrip(req *http.Request) (*http.Response, error) {
126+
tr.once.Do(func() {
127+
tr.firstQuery = req.URL.RawQuery
128+
close(tr.started)
129+
})
130+
<-req.Context().Done()
131+
return nil, req.Context().Err()
132+
}
133+
134+
// TestExpressCreateSessionCallerCancel verifies that the S3 Express session
135+
// retrieval keeps the caller context: canceling the caller aborts the
136+
// in-flight CreateSession request. The first-request query assertion pins
137+
// that the express arm ran — only CreateSession sends "?session".
138+
func TestExpressCreateSessionCallerCancel(t *testing.T) {
139+
tr := &expressSessionTransport{started: make(chan struct{})}
140+
clnt, err := New("s3.amazonaws.com", &Options{
141+
Creds: credentials.NewStaticV4("k", "s", ""),
142+
Region: "us-east-1",
143+
Transport: tr,
144+
})
145+
if err != nil {
146+
t.Fatal(err)
147+
}
148+
149+
ctx, cancel := context.WithCancel(context.Background())
150+
defer cancel()
151+
go func() {
152+
<-tr.started
153+
cancel()
154+
}()
155+
156+
_, err = clnt.BucketExists(ctx, "mybucket--use1-az4--x-s3")
157+
if !errors.Is(err, context.Canceled) {
158+
t.Fatalf("Expected context.Canceled from the express session retrieval, got %v", err)
159+
}
160+
if !strings.Contains(tr.firstQuery, "session") {
161+
t.Fatalf("Expected the first request to be the CreateSession call (query %q lacks \"session\")", tr.firstQuery)
162+
}
163+
}
164+
116165
// panicProvider is a credentials.Provider whose retrieval always panics.
117166
type panicProvider struct{}
118167

api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,8 @@ func (c *Client) newRequest(ctx context.Context, method string, metadata request
912912
// operation's retries must stay cancellable, so its waiters
913913
// share the winner's fate. A retrieval panic resumes on each
914914
// waiting caller's goroutine (credsRetrievalPanic); a
915-
// runtime.Goexit is not propagated — waiters wait out their own
916-
// contexts.
915+
// runtime.Goexit is not propagated — each caller waits until its
916+
// own context ends, indefinitely when it has none.
917917
resCh := c.credsGroup.DoChan(metadata.bucketName, func() (v credentials.Value, rerr error) {
918918
defer func() {
919919
if r := recover(); r != nil {

0 commit comments

Comments
 (0)