diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b38f10fba..8dbb0620f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -79,6 +79,9 @@ jobs: - run: go test -v -run TestRaceyPatternSchema -race ./... env: CGO_ENABLED: '1' + - run: go test -v -run TestIssue741 -race ./... + env: + CGO_ENABLED: '1' - run: git --no-pager diff --exit-code - if: runner.os == 'Linux' diff --git a/openapi3/issue741_test.go b/openapi3/issue741_test.go new file mode 100644 index 000000000..aad522023 --- /dev/null +++ b/openapi3/issue741_test.go @@ -0,0 +1,43 @@ +package openapi3 + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue741(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + body := `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Foo":{"type":"string"}}}}` + _, err := w.Write([]byte(body)) + if err != nil { + panic(err) + } + })) + defer ts.Close() + + rootSpec := []byte(fmt.Sprintf( + `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Bar1":{"$ref":"%s#/components/schemas/Foo"}}}}`, + ts.URL, + )) + + wg := &sync.WaitGroup{} + n := 10 + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + loader := NewLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadFromData(rootSpec) + require.NoError(t, err) + require.NotNil(t, doc) + }() + } + wg.Wait() +} diff --git a/openapi3/loader_uri_reader.go b/openapi3/loader_uri_reader.go index 8357a980d..92ac043f9 100644 --- a/openapi3/loader_uri_reader.go +++ b/openapi3/loader_uri_reader.go @@ -7,12 +7,15 @@ import ( "net/http" "net/url" "path/filepath" + "sync" ) // ReadFromURIFunc defines a function which reads the contents of a resource // located at a URI. type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error) +var uriMu = &sync.RWMutex{} + // ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a // given URI. var ErrURINotSupported = errors.New("unsupported URI") @@ -92,12 +95,17 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc { } uri := location.String() var ok bool + uriMu.RLock() if buf, ok = cache[uri]; ok { + uriMu.RUnlock() return } + uriMu.RUnlock() if buf, err = reader(loader, location); err != nil { return } + uriMu.Lock() + defer uriMu.Unlock() cache[uri] = buf return }