55 "encoding/json"
66 "encoding/xml"
77 "errors"
8- "fmt"
98 "io"
109 "net/http"
1110 "net/url"
@@ -15,12 +14,9 @@ import (
1514 "time"
1615
1716 // Package imports
18- oauth "github.com/mutablelogic/go-client/pkg/oauth"
19- otel "github.com/mutablelogic/go-client/pkg/otel"
2017 transport "github.com/mutablelogic/go-client/pkg/transport"
2118 httpresponse "github.com/mutablelogic/go-server/pkg/httpresponse"
2219 types "github.com/mutablelogic/go-server/pkg/types"
23- oauth2 "golang.org/x/oauth2"
2420)
2521
2622///////////////////////////////////////////////////////////////////////////////
@@ -49,7 +45,6 @@ type Client struct {
4945 atomicToken atomic.Value // stores Token — lock-free; written by setToken, read by AccessToken
5046 headers map [string ]string // setup-only: consumed by New() into HeadersTransport
5147 transports []func (http.RoundTripper ) http.RoundTripper // setup-only: consumed by New()
52- oauth * oauth.OAuthCredentials // OAuth credentials for automatic token refresh on requests
5348}
5449
5550type ClientOpt func (* Client ) error
@@ -143,23 +138,6 @@ func (client *Client) AccessToken() string {
143138 return ""
144139}
145140
146- // setToken stores t atomically so AccessToken() is always consistent.
147- // Uses atomic.Value so no mutex is required.
148- func (client * Client ) setToken (t Token ) {
149- client .atomicToken .Store (t )
150- }
151-
152- func (client * Client ) String () string {
153- str := "<client"
154- if client .endpoint != nil {
155- str += fmt .Sprintf (" endpoint=%q" , otel .RedactedURL (client .endpoint ))
156- }
157- if client .Client .Timeout > 0 {
158- str += fmt .Sprint (" timeout=" , client .Client .Timeout )
159- }
160- return str + ">"
161- }
162-
163141///////////////////////////////////////////////////////////////////////////////
164142// PUBLIC METHODS
165143
@@ -191,59 +169,19 @@ func (client *Client) DoWithContext(ctx context.Context, in Payload, out any, op
191169 return err
192170 }
193171
194- // Narrow critical section: serialise the OAuth check+refresh so that only
195- // one goroutine refreshes the token at a time (prevents thundering-herd).
196- // The lock is released before the main HTTP round-trip so concurrent
197- // requests can proceed in parallel once the token is known to be valid.
198- client .RWMutex .Lock ()
199- err = client .refreshOAuth (ctx )
200- client .RWMutex .Unlock ()
201- if err != nil {
202- return err
203- }
204-
205172 // HTTP round-trip (including redirect follows) runs without any lock.
206173 // http.Client and its transport stack are safe for concurrent use.
207174 return do (client .Client , req , accept , client .strict , out , opts ... )
208175}
209176
210177// Do a HTTP request and decode it into an object
211178func (client * Client ) Request (req * http.Request , out any , opts ... RequestOpt ) error {
212- client .RWMutex .Lock ()
213- err := client .refreshOAuth (req .Context ())
214- client .RWMutex .Unlock ()
215- if err != nil {
216- return err
217- }
218-
219179 return do (client .Client , req , "" , false , out , opts ... )
220180}
221181
222182///////////////////////////////////////////////////////////////////////////////
223183// PRIVATE METHODS
224184
225- // refreshOAuth refreshes the OAuth token if credentials are set and the token
226- // is expired. It injects the client's own HTTP transport into the context so
227- // the refresh request honours the same proxy/TLS/logging configuration.
228- // It is a no-op when no OAuth credentials are configured or the token is still valid.
229- func (client * Client ) refreshOAuth (ctx context.Context ) error {
230- if client .oauth == nil {
231- return nil
232- }
233- if err := client .oauth .Refresh (context .WithValue (ctx , oauth2 .HTTPClient , client .Client )); err != nil {
234- return err
235- }
236- scheme := client .oauth .Token .TokenType
237- if scheme == "" {
238- scheme = Bearer
239- }
240- client .setToken (Token {
241- Scheme : scheme ,
242- Value : client .oauth .Token .AccessToken ,
243- })
244- return nil
245- }
246-
247185// request creates a request which can be used to return responses. The accept
248186// parameter is the accepted mime-type of the response. If the accept parameter is empty,
249187// then the default is application/json.
@@ -300,7 +238,7 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
300238 // Work on a shallow copy so we never mutate the shared *http.Client.
301239 // Per-request timeout and transport changes are therefore safe without
302240 // needing a mutex or deferred restoration.
303- localCl := * client
241+ localCl := types . Value ( client )
304242 if reqopts .noTimeout {
305243 localCl .Timeout = 0
306244 }
@@ -311,6 +249,7 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
311249 }
312250 localCl .Transport = t
313251 }
252+
314253 // Disable the standard client's redirect-following so that our manual
315254 // redirect loop below actually sees 3xx responses and can enforce the
316255 // method/header-preservation and cross-origin stripping rules.
@@ -392,16 +331,31 @@ func do(client *http.Client, req *http.Request, accept string, strict bool, out
392331
393332 // Check status code
394333 if response .StatusCode < 200 || response .StatusCode > 299 {
334+ var httpErr httpresponse.ErrResponse
335+
395336 // Read any information from the body
396337 data , err := io .ReadAll (response .Body )
397338 if err != nil {
398339 return err
399340 }
341+
342+ // If there's no body, return an error with just the status code
400343 if len (data ) == 0 {
401344 return httpresponse .Err (response .StatusCode ).With (response .Status )
402- } else {
345+ }
346+
347+ // Preserve non-JSON error bodies so callers can surface useful server detail.
348+ if err := json .Unmarshal (data , & httpErr ); err != nil {
403349 return httpresponse .Err (response .StatusCode ).Withf ("%s: %s" , response .Status , string (data ))
404350 }
351+
352+ // If the response code doesn't match, fall back to the raw body text.
353+ if httpErr .Code != response .StatusCode {
354+ return httpresponse .Err (response .StatusCode ).Withf ("%s: %s" , response .Status , string (data ))
355+ }
356+
357+ // Return the error response with any detail from the body
358+ return httpErr
405359 }
406360
407361 // When in strict mode, check content type returned is as expected.
0 commit comments