Skip to content

Commit 94920b4

Browse files
committed
fix(gcsartifact): recognize wrapped storage.ErrObjectNotExist
Load and GetArtifactVersion compared blob.attrs' error against storage.ErrObjectNotExist with ==. cloud.google.com/go/storage always wraps that sentinel (formatObjectErr: fmt.Errorf("%w: %w", ErrObjectNotExist, err)), so the check never matched in production and callers doing errors.Is(err, fs.ErrNotExist) got a generic "could not get blob attributes" error instead of a not-found signal. ErrVersionConflict a few lines up in this same file already documents the fix: "It is always wrapped, so test for it with [errors.Is]". Apply that same rule to storage.ErrObjectNotExist at both call sites.
1 parent 0a51e15 commit 94920b4

2 files changed

Lines changed: 63 additions & 2 deletions

File tree

artifact/gcsartifact/gcs_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,53 @@ func TestSaveZeroByteArtifact(t *testing.T) {
202202
}
203203
}
204204

205+
// TestNotFoundIsWrappedSentinel checks that Load and GetArtifactVersion
206+
// recognize storage.ErrObjectNotExist even when the storage client wraps it,
207+
// which is how cloud.google.com/go/storage actually returns it in production
208+
// (see storage.formatObjectErr, which always wraps NotFound as
209+
// fmt.Errorf("%w: %w", ErrObjectNotExist, err)). A caller checking
210+
// errors.Is(err, fs.ErrNotExist) must still get a match.
211+
func TestNotFoundIsWrappedSentinel(t *testing.T) {
212+
wrapped := fmt.Errorf("%w: %w", storage.ErrObjectNotExist, errors.New("googleapi: Error 404: Not Found"))
213+
214+
for _, tc := range []struct {
215+
name string
216+
call func(svc *gcsService) error
217+
}{
218+
{
219+
name: "Load",
220+
call: func(svc *gcsService) error {
221+
_, err := svc.Load(t.Context(), &artifact.LoadRequest{
222+
AppName: "app", UserID: "user", SessionID: "session", FileName: "file",
223+
Version: 1,
224+
})
225+
return err
226+
},
227+
},
228+
{
229+
name: "GetArtifactVersion",
230+
call: func(svc *gcsService) error {
231+
_, err := svc.GetArtifactVersion(t.Context(), &artifact.GetArtifactVersionRequest{
232+
AppName: "app", UserID: "user", SessionID: "session", FileName: "file",
233+
Version: 1,
234+
})
235+
return err
236+
},
237+
},
238+
} {
239+
t.Run(tc.name, func(t *testing.T) {
240+
svc := newGCSServiceForTesting("wrapped-not-exist")
241+
fb := svc.bucket.(*fakeBucket)
242+
fb.attrsErr = wrapped
243+
244+
err := tc.call(svc)
245+
if !errors.Is(err, fs.ErrNotExist) {
246+
t.Errorf("err = %v, want errors.Is(err, fs.ErrNotExist) = true", err)
247+
}
248+
})
249+
}
250+
}
251+
205252
// TestBackoffDelayBounds checks the jittered backoff stays within [0, saveRetryMaxDelay].
206253
func TestBackoffDelayBounds(t *testing.T) {
207254
for attempt := range maxSaveAttempts {
@@ -290,6 +337,12 @@ type fakeBucket struct {
290337
// return it (a simulated write failure); closeCalls counts Close calls.
291338
closeErr error
292339
closeCalls int
340+
341+
// attrsErr, when set, is returned by every object's attrs() call in place
342+
// of the default not-found/found behavior. Used to simulate the GCS
343+
// client library's wrapped storage.ErrObjectNotExist (see
344+
// TestNotFoundIsWrappedSentinel).
345+
attrsErr error
293346
}
294347

295348
// object returns a handle to the named blob, creating an empty backing store on
@@ -358,6 +411,14 @@ func (o *fakeObject) ifNotExist() gcsObject {
358411

359412
// attrs returns fake attributes for the object.
360413
func (o *fakeObject) attrs(ctx context.Context) (*storage.ObjectAttrs, error) {
414+
if o.bucket != nil {
415+
o.bucket.mu.Lock()
416+
forced := o.bucket.attrsErr
417+
o.bucket.mu.Unlock()
418+
if forced != nil {
419+
return nil, forced
420+
}
421+
}
361422
b := o.blob
362423
b.mu.Lock()
363424
defer b.mu.Unlock()

artifact/gcsartifact/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func (s *gcsService) Load(ctx context.Context, req *artifact.LoadRequest) (_ *ar
298298
// Check if the blob exists before trying to read it
299299
attrs, err := blob.attrs(ctx)
300300
if err != nil {
301-
if err == storage.ErrObjectNotExist {
301+
if errors.Is(err, storage.ErrObjectNotExist) {
302302
return nil, fmt.Errorf("artifact '%s' not found: %w", blobName, fs.ErrNotExist)
303303
}
304304
return nil, fmt.Errorf("could not get blob attributes: %w", err)
@@ -474,7 +474,7 @@ func (s *gcsService) GetArtifactVersion(ctx context.Context, req *artifact.GetAr
474474

475475
attrs, err := blob.attrs(ctx)
476476
if err != nil {
477-
if err == storage.ErrObjectNotExist {
477+
if errors.Is(err, storage.ErrObjectNotExist) {
478478
return nil, fmt.Errorf("artifact '%s' not found: %w", blobName, fs.ErrNotExist)
479479
}
480480
return nil, fmt.Errorf("could not get blob attributes: %w", err)

0 commit comments

Comments
 (0)