Skip to content

Commit 0164bc0

Browse files
authored
Remove OAuth implementation from client, improved error handling, SSE buffer adjustment (#63)
* Removed OAuth from client * Updated go.mod dependencies * Updates after test fixes * Upgraded go-server version
1 parent db9eeff commit 0164bc0

34 files changed

Lines changed: 91 additions & 4466 deletions

README.md

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ This repository contains a generic HTTP client which can be adapted to provide:
99
* Debugging capabilities to see the request and response data
1010
* Streaming text and JSON events
1111
* OpenTelemetry tracing for distributed observability
12-
* OAuth 2.0 client flows including discovery, dynamic client registration, authorization code, device authorization, and client credentials
1312

1413
API Documentation: <https://pkg.go.dev/github.com/mutablelogic/go-client>
1514

@@ -21,7 +20,6 @@ There are also some example clients which use this library:
2120

2221
There are also utility packages for working with multipart file uploads, transport middleware, and OpenTelemetry:
2322

24-
* [OAuth 2.0 Package](https://github.com/mutablelogic/go-client/tree/main/pkg/oauth)
2523
* [OpenTelemetry Package](https://github.com/mutablelogic/go-client/tree/main/pkg/otel)
2624
* [Transport Middleware Package](https://github.com/mutablelogic/go-client/tree/main/pkg/transport)
2725
* [Multipart Package](https://github.com/mutablelogic/go-client/tree/main/pkg/multipart)
@@ -240,67 +238,6 @@ func main() {
240238

241239
You can also set the token on a per-request basis using the `OptToken` option in call to the `Do` method.
242240

243-
## OAuth 2.0
244-
245-
The `pkg/oauth` package provides a full OAuth 2.0 client supporting discovery (RFC 8414), dynamic
246-
client registration (RFC 7591), authorization code + PKCE, device authorization, and client
247-
credentials flows. Use `c.OAuth()` to run the full flow using the client's own HTTP transport —
248-
the `Authorize*` methods attach the resulting credentials to the client automatically, and the
249-
token is refreshed transparently before each request:
250-
251-
```go
252-
import (
253-
"context"
254-
255-
client "github.com/mutablelogic/go-client"
256-
oauth "github.com/mutablelogic/go-client/pkg/oauth"
257-
)
258-
259-
func main() {
260-
ctx := context.Background()
261-
262-
// Create the API client first so its HTTP transport is reused for all OAuth calls.
263-
c, err := client.New(client.OptEndpoint("https://api.example.com"))
264-
if err != nil {
265-
log.Fatal(err)
266-
}
267-
268-
// c.OAuth() returns a flow that injects c's HTTP transport automatically.
269-
flow := c.OAuth()
270-
271-
// 1. Discover server metadata
272-
metadata, err := flow.Discover(ctx, "https://example.com")
273-
if err != nil {
274-
log.Fatal(err)
275-
}
276-
277-
// 2. Authorize (client credentials shown; see pkg/oauth for browser/device flows)
278-
creds, err := flow.AuthorizeWithCredentials(ctx,
279-
&oauth.OAuthCredentials{
280-
Metadata: metadata,
281-
ClientID: "my-client",
282-
ClientSecret: "my-secret",
283-
},
284-
)
285-
if err != nil {
286-
log.Fatal(err)
287-
}
288-
289-
// Persist rotated refresh tokens
290-
creds.OnRefresh = func(c *oauth.OAuthCredentials) error {
291-
return saveToDisk(c) // your persistence logic
292-
}
293-
294-
// 3. Attach credentials; the client refreshes the token automatically on expiry.
295-
// AuthorizeWithCredentials (and all other Authorize* methods) attach credentials
296-
// to the client automatically — no further setup needed.
297-
}
298-
```
299-
300-
See [pkg/oauth/README.md](https://github.com/mutablelogic/go-client/tree/main/pkg/oauth) for
301-
the full API including browser-based, device, and manual code-paste flows, token introspection,
302-
and revocation.
303-
304241
## Form submission
305242

306243
You can create a payload with form data:

client.go

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
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

5550
type 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
211178
func (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.

client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func Test_OptTimeout_sets_value(t *testing.T) {
9797
)
9898
require.NoError(t, err)
9999
// Roundtrip through String() to make sure it surface the timeout
100-
assert.Contains(t, c.String(), "5s")
100+
assert.Contains(t, c.Timeout.String(), "5s")
101101
}
102102

103103
func Test_OptTimeout_zero(t *testing.T) {

clientopts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func OptRateLimit(value float32) ClientOpt {
102102
// overridden by the client for individual requests using OptToken.
103103
func OptReqToken(value Token) ClientOpt {
104104
return func(client *Client) error {
105-
client.setToken(value)
105+
client.atomicToken.Store(value)
106106
return nil
107107
}
108108
}

go.mod

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,20 @@ module github.com/mutablelogic/go-client
33
go 1.25.0
44

55
require (
6-
github.com/alecthomas/kong v1.14.0
6+
github.com/alecthomas/kong v1.15.0
77
github.com/andreburgaud/crypt2go v1.8.0
88
github.com/djthorpe/go-errors v1.0.3
99
github.com/djthorpe/go-tablewriter v0.0.11
10-
github.com/mutablelogic/go-server v1.6.18
10+
github.com/mutablelogic/go-server v1.6.22
1111
github.com/stretchr/testify v1.11.1
1212
github.com/xdg-go/pbkdf2 v1.0.0
13-
go.opentelemetry.io/otel v1.42.0
14-
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0
15-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
16-
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
17-
go.opentelemetry.io/otel/sdk v1.42.0
18-
go.opentelemetry.io/otel/trace v1.42.0
13+
go.opentelemetry.io/otel v1.43.0
14+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
15+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
16+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
17+
go.opentelemetry.io/otel/sdk v1.43.0
18+
go.opentelemetry.io/otel/trace v1.43.0
1919
golang.org/x/crypto v0.49.0
20-
golang.org/x/oauth2 v0.36.0
2120
golang.org/x/term v0.41.0
2221
)
2322

@@ -28,20 +27,19 @@ require (
2827
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2928
github.com/go-logr/logr v1.4.3 // indirect
3029
github.com/go-logr/stdr v1.2.2 // indirect
31-
github.com/google/jsonschema-go v0.4.2 // indirect
3230
github.com/google/uuid v1.6.0 // indirect
3331
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
34-
github.com/mattn/go-runewidth v0.0.21 // indirect
32+
github.com/mattn/go-runewidth v0.0.22 // indirect
3533
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3634
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
37-
go.opentelemetry.io/otel/metric v1.42.0 // indirect
35+
go.opentelemetry.io/otel/metric v1.43.0 // indirect
3836
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
3937
golang.org/x/net v0.52.0 // indirect
4038
golang.org/x/sys v0.42.0 // indirect
4139
golang.org/x/text v0.35.0 // indirect
42-
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
43-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
44-
google.golang.org/grpc v1.79.3 // indirect
40+
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
41+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
42+
google.golang.org/grpc v1.80.0 // indirect
4543
google.golang.org/protobuf v1.36.11 // indirect
4644
gopkg.in/yaml.v3 v3.0.1 // indirect
4745
)

0 commit comments

Comments
 (0)