Skip to content

Commit bfd9635

Browse files
Merge pull request #365 from rest-sh/fix/release-qa-discovery-race
fix: wait for canceled discovery probes
2 parents 06efa65 + b24a1ab commit bfd9635

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

internal/cli/api_manage_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ func TestAPISyncDiscoverySendsAuthForShorthandBaseURL(t *testing.T) {
699699
}
700700

701701
func TestAPISyncDiscoveryFollowsLinkWithShorthandBaseURL(t *testing.T) {
702+
var mu sync.Mutex
702703
var linkedSpecAuth string
703704

704705
c, _, _ := newAPISyncCLI(t, protectedAPI("api.example.com", "", bearerAuth("test-token")))
@@ -709,7 +710,9 @@ func TestAPISyncDiscoveryFollowsLinkWithShorthandBaseURL(t *testing.T) {
709710
resp.Header.Set("Link", `</openapi.yaml>; rel="service-desc"`)
710711
return resp, nil
711712
case r.URL.Host == "api.example.com" && r.URL.Path == "/openapi.yaml":
713+
mu.Lock()
712714
linkedSpecAuth = r.Header.Get("Authorization")
715+
mu.Unlock()
713716
return jsonResponse(200, minimalOpenAPI), nil
714717
default:
715718
return textResponse(404, "text/plain", "not found", r), nil
@@ -719,6 +722,8 @@ func TestAPISyncDiscoveryFollowsLinkWithShorthandBaseURL(t *testing.T) {
719722
if err := runProtectedAPISync(t, c); err != nil {
720723
t.Fatalf("api sync: %v", err)
721724
}
725+
mu.Lock()
726+
defer mu.Unlock()
722727
if linkedSpecAuth != "Bearer test-token" {
723728
t.Fatalf("linked spec Authorization = %q, want %q", linkedSpecAuth, "Bearer test-token")
724729
}

internal/spec/discover.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,10 @@ func discoverFromNetwork(ctx context.Context, cfg DiscoverConfig, loaders []Load
413413
wg.Wait()
414414
close(ch)
415415
}()
416-
return collectDiscoveryResults(ctx, cancel, ch, cfg.BaseURL)
416+
spec, ttl, err := collectDiscoveryResults(ctx, cancel, ch, cfg.BaseURL)
417+
cancel()
418+
wg.Wait()
419+
return spec, ttl, err
417420
}
418421

419422
// Probe base URL: extract Link headers and try the body itself.
@@ -450,7 +453,10 @@ func discoverFromNetwork(ctx context.Context, cfg DiscoverConfig, loaders []Load
450453
close(ch)
451454
}()
452455

453-
return collectDiscoveryResults(ctx, cancel, ch, cfg.BaseURL)
456+
spec, ttl, err := collectDiscoveryResults(ctx, cancel, ch, cfg.BaseURL)
457+
cancel()
458+
wg.Wait()
459+
return spec, ttl, err
454460
}
455461

456462
var wellKnownSpecPaths = []string{"/openapi.json", "/openapi.yaml"}

internal/spec/discover_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,60 @@ func TestDiscover_ExplicitSpecURL(t *testing.T) {
884884
}
885885
}
886886

887+
func TestDiscoverWaitsForCanceledNetworkProbes(t *testing.T) {
888+
specBody := `{"openapi":"3.1.0","info":{"title":"Direct","version":"1.0.0"},"paths":{}}`
889+
slowEntered := make(chan struct{})
890+
cancelSeen := make(chan struct{})
891+
releaseSlow := make(chan struct{})
892+
done := make(chan error, 1)
893+
894+
cfg := DiscoverConfig{
895+
APIName: "testapi",
896+
BaseURL: "https://api.example.com",
897+
Fetch: func(ctx context.Context, rawURL string, tr http.RoundTripper) (*http.Response, error) {
898+
switch rawURL {
899+
case "https://api.example.com":
900+
close(slowEntered)
901+
<-ctx.Done()
902+
close(cancelSeen)
903+
<-releaseSlow
904+
return nil, ctx.Err()
905+
case "https://api.example.com/openapi.json":
906+
<-slowEntered
907+
return httpResponse(200, "application/json", specBody, nil), nil
908+
default:
909+
return httpResponse(404, "text/plain", "not found", nil), nil
910+
}
911+
},
912+
}
913+
914+
go func() {
915+
_, err := Discover(context.Background(), cfg, DefaultLoaders())
916+
done <- err
917+
}()
918+
919+
select {
920+
case <-cancelSeen:
921+
case <-time.After(2 * time.Second):
922+
t.Fatal("Discover did not cancel the slower probe")
923+
}
924+
select {
925+
case err := <-done:
926+
t.Fatalf("Discover returned before canceled probes exited: %v", err)
927+
default:
928+
}
929+
930+
close(releaseSlow)
931+
select {
932+
case err := <-done:
933+
if err != nil {
934+
t.Fatalf("Discover: %v", err)
935+
}
936+
case <-time.After(2 * time.Second):
937+
t.Fatal("Discover did not return after canceled probe exited")
938+
}
939+
}
940+
887941
func TestDiscoverCleansCredentialURLMetadataInCache(t *testing.T) {
888942
raw := `{"openapi":"3.1.0","info":{"title":"Direct","version":"1.0.0"},"paths":{"/items":{"get":{"operationId":"listItems","responses":{"200":{"description":"OK"}}}}}}`
889943
tr := roundTripperFunc(func(r *http.Request) (*http.Response, error) {

0 commit comments

Comments
 (0)