Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cloudfoundry/go-cfclient
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: cloudfoundry/go-cfclient
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: retry-optimization
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
  • 1 commit
  • 4 files changed
  • 1 contributor

Commits on Dec 21, 2023

  1. Unverified

    This user has not yet uploaded their public signing key.
    Copy the full SHA
    86464cf View commit details
Showing with 44 additions and 16 deletions.
  1. +27 −8 internal/http/client.go
  2. +9 −6 internal/http/client_test.go
  3. +3 −1 internal/http/request.go
  4. +5 −1 testutil/api_mock.go
35 changes: 27 additions & 8 deletions internal/http/client.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,15 @@ type OAuthTokenSourceCreator interface {
CreateOAuth2TokenSource(ctx context.Context) (oauth2.TokenSource, error)
}

// BytesBufferReadCloser allows direct access to the underlying buffer
// so that re-reading ops do not incur a performance penalty.
type BytesBufferReadCloser struct {
*bytes.Buffer
}

// Close is no-op - avoids http.NewRequest wrapping the body in an io.NopCloser
func (BytesBufferReadCloser) Close() error { return nil }

// retryableAuthTransport wraps a http.RoundTripper and combines it with an OAuthTokenSourceCreator so
// that any 401s cause a re-authentication and request retry
type retryableAuthTransport struct {
@@ -97,16 +106,26 @@ func (t *retryableAuthTransport) RoundTrip(req *http.Request) (*http.Response, e

func backupRequestBody(req *http.Request) error {
if req.Body != nil && req.GetBody == nil {
bodyBytes, err := io.ReadAll(req.Body)
ios.Close(req.Body) // Ensure the body is always closed
if err != nil {
return err
}
// TODO handle bytes.Reader and strings.Reader
// optimization - we can re-read bytes.Buffer over and over again
if r, ok := req.Body.(BytesBufferReadCloser); ok {
buf := r.Buffer
req.GetBody = func() (io.ReadCloser, error) {
return BytesBufferReadCloser{buf}, nil
}
req.Body, _ = req.GetBody()
} else {
bodyBytes, err := io.ReadAll(req.Body)
ios.Close(req.Body) // Ensure the body is always closed
if err != nil {
return err
}

req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewBuffer(bodyBytes)), nil
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewBuffer(bodyBytes)), nil
}
req.Body, _ = req.GetBody()
}
req.Body, _ = req.GetBody()
}
return nil
}
15 changes: 9 additions & 6 deletions internal/http/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package http_test

import (
"bytes"
"context"
"github.com/cloudfoundry-community/go-cfclient/v3/internal/http"
"github.com/cloudfoundry-community/go-cfclient/v3/testutil"
@@ -32,12 +33,13 @@ func (ts *MockedOAuthTokenSource) Token() (*oauth2.Token, error) {
}

func TestOAuthSessionManager(t *testing.T) {
g := testutil.NewObjectJSONGenerator(1)
serverURL := testutil.SetupMultiple([]testutil.MockRoute{
{
Method: "GET",
Method: "POST",
Endpoint: "/v3/organizations",
Output: []string{"organizations[]"},
Statuses: []int{200},
Output: []string{"auth error", g.Organization().JSON},
Statuses: []int{401, 201},
UserAgent: "Go-http-client/1.1",
},
{
@@ -65,14 +67,15 @@ func TestOAuthSessionManager(t *testing.T) {
client, err := http.NewAuthenticatedClient(context.Background(), gohttp.DefaultClient, tokenSrcCreator)
require.NoError(t, err)

resp, err := client.Get(serverURL + "/v3/organizations")
body := http.BytesBufferReadCloser{Buffer: bytes.NewBufferString(g.Organization().JSON)}
resp, err := client.Post(serverURL+"/v3/organizations", "application/json", body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.Equal(t, 201, resp.StatusCode)

// to the caller the retry is transparent on 401
resp, err = client.Get(serverURL + "/v3/spaces")
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)

tokenSrcCreator.AssertNumberOfCalls(t, "CreateOAuth2TokenSource", 2)
tokenSrcCreator.AssertNumberOfCalls(t, "CreateOAuth2TokenSource", 3)
}
4 changes: 3 additions & 1 deletion internal/http/request.go
Original file line number Diff line number Diff line change
@@ -57,5 +57,7 @@ func EncodeBody(obj any) (io.Reader, error) {
if err := json.NewEncoder(buf).Encode(obj); err != nil {
return nil, fmt.Errorf("error encoding object to JSON: %w", err)
}
return buf, nil
return &BytesBufferReadCloser{
buf,
}, nil
}
6 changes: 5 additions & 1 deletion testutil/api_mock.go
Original file line number Diff line number Diff line change
@@ -115,14 +115,18 @@ func SetupMultiple(mockEndpoints []MockRoute, t *testing.T) string {
return status, singleOutput
})
case "POST":
count := 0
r.Post(endpoint, func(res http.ResponseWriter, req *http.Request) (int, string) {
testUserAgent(req.Header.Get("User-Agent"), userAgent, t)
testQueryString(req.URL.RawQuery, queryString, t)
testReqBody(req, postFormBody, t)
if redirectLocation != "" {
res.Header().Add("Location", redirectLocation)
}
return status, output[0]
singleOutput := output[count]
status = statuses[count]
count++
return status, singleOutput
})
case "DELETE":
r.Delete(endpoint, func(res http.ResponseWriter, req *http.Request) (int, string) {