-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
217 lines (199 loc) · 7.23 KB
/
Copy pathclient.go
File metadata and controls
217 lines (199 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// Copyright (c) 2026 dexpace and Omar Aljarrah.
// Licensed under the MIT License. See LICENSE in the repository root for details.
package dexpace
import (
"net/http"
"strconv"
"time"
"github.com/dexpace/go-sdk/auth"
cfgpkg "github.com/dexpace/go-sdk/config"
"github.com/dexpace/go-sdk/header"
"github.com/dexpace/go-sdk/httperr"
"github.com/dexpace/go-sdk/idempotency"
"github.com/dexpace/go-sdk/instrumentation"
"github.com/dexpace/go-sdk/logging"
"github.com/dexpace/go-sdk/pipeline"
"github.com/dexpace/go-sdk/redact"
"github.com/dexpace/go-sdk/retry"
"github.com/dexpace/go-sdk/transport"
)
// Client is a thin handle around a configured [pipeline.Pipeline]. It is safe
// for concurrent use; create one with [New] and reuse it for the lifetime of the
// process.
type Client struct {
pl pipeline.Pipeline
}
// New assembles a Client. Built-in policies are placed in stage order, outermost
// first:
//
// [errors] → client-identity → idempotency → retry → auth → [date] → [tracing] → [metrics] → logging → transport
//
// When WithErrors is supplied, an errors stage is prepended as the outermost
// policy, mapping the final result to the typed error model.
// When WithTracing or WithMetrics is supplied, a tracing or metrics stage is
// installed at StageTracing or StageMetrics (inside retry).
//
// Idempotency wraps retry, so a single key is minted once per logical call and
// reused across attempts; retry in turn wraps auth and logging, so auth re-runs
// (and may refresh its token) on every attempt and logging — innermost — records
// the request as actually sent.
// Idempotency-key stamping is on by default for POST (disable with
// WithoutIdempotency); set-date is opt-in (WithDate). Custom policies added with
// WithPolicies run just before the transport; use WithPolicyBefore /
// WithPolicyAfter to place a policy relative to a specific stage.
//
// Pass WithConfig to fill any unset defaults (User-Agent, retry settings,
// transport timeout) from DEXPACE_* environment variables.
func New(opts ...Option) *Client {
var cfg config
for _, opt := range opts {
opt(&cfg)
}
t := cfg.transport
if t == nil {
var topts []transport.Option
if cfg.cfgSource != nil {
if d := cfg.cfgSource.GetDuration(cfgpkg.EnvHTTPTimeout, 0); d > 0 {
topts = append(topts, transport.WithTimeout(d))
}
}
t = transport.New(topts...)
}
ua := cfg.userAgent
if ua == "" {
ua = userAgent
if cfg.cfgSource != nil {
ua = cfg.cfgSource.GetString(cfgpkg.EnvUserAgent, userAgent)
}
}
retryOpts := retry.Options{}
switch {
case cfg.retry != nil:
retryOpts = *cfg.retry
case cfg.cfgSource != nil:
retryOpts = retry.Options{
BaseDelay: cfg.cfgSource.GetDuration(cfgpkg.EnvRetryBaseDelay, 0),
}
// Lookup (not GetInt) so we can tell "absent" (keep the SDK default)
// from an explicit 0 or negative (disable retries).
if v, ok := cfg.cfgSource.Lookup(cfgpkg.EnvMaxRetries); ok {
if n, err := strconv.Atoi(v); err == nil {
if n <= 0 {
retryOpts.MaxRetries = -1 // explicit 0 or negative disables retries
} else {
retryOpts.MaxRetries = n
}
}
}
}
redactor := redact.Default
if len(cfg.redactAllow) > 0 {
redactor = redact.New(cfg.redactAllow...)
}
placements := []pipeline.Placement{
pipeline.At(pipeline.StageClientIdentity, userAgentPolicy(ua)),
pipeline.At(pipeline.StageRetry, retry.NewPolicy(retryOpts)),
}
if cfg.errorsEnabled {
placements = append(placements, pipeline.At(pipeline.StageErrors, errorsPolicy()))
}
if !cfg.noIdempotency {
iopts := idempotency.Options{}
if cfg.idempotency != nil {
iopts = *cfg.idempotency
}
placements = append(placements,
pipeline.At(pipeline.StageIdempotency, idempotency.NewPolicy(iopts)))
}
switch {
case cfg.credential != nil:
cache := cfg.tokenCache
if cache == nil {
cache = auth.NewInMemoryTokenCache()
}
placements = append(placements,
pipeline.At(pipeline.StageAuth, auth.NewBearerTokenPolicyWithCache(cfg.credential, cache, cfg.scopes...)))
case cfg.basicAuth != nil:
placements = append(placements,
pipeline.At(pipeline.StageAuth, auth.NewBasicAuthPolicy(*cfg.basicAuth)))
case cfg.apiKey.set:
placements = append(placements,
pipeline.At(pipeline.StageAuth, auth.NewAPIKeyPolicy(cfg.apiKey.header, cfg.apiKey.key)))
case cfg.digestAuth != nil:
placements = append(placements,
pipeline.At(pipeline.StageAuth, auth.NewDigestAuthPolicy(*cfg.digestAuth)))
}
if cfg.date {
placements = append(placements, pipeline.At(pipeline.StageDate, datePolicy()))
}
if cfg.logging {
placements = append(placements,
pipeline.At(pipeline.StageLogging, logging.NewPolicy(logging.Options{Logger: cfg.logger, Redactor: redactor})))
}
if cfg.tracer != nil {
placements = append(placements,
pipeline.At(pipeline.StageTracing, instrumentation.NewTracingPolicy(cfg.tracer, redactor)))
}
if cfg.meter != nil {
placements = append(placements,
pipeline.At(pipeline.StageMetrics, instrumentation.NewMetricsPolicy(cfg.meter)))
}
placements = append(placements, cfg.before...)
placements = append(placements, cfg.after...)
for _, p := range cfg.custom {
// Custom WithPolicies land innermost, just before transport — anchored
// after the innermost stage to preserve the previous behavior.
placements = append(placements, pipeline.After(pipeline.StageLogging, p))
}
return &Client{pl: pipeline.NewStaged(t, placements...)}
}
// Do sends req through the pipeline and returns the response. The caller owns
// the response body and must close it.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.pl.Do(req)
}
// Pipeline returns the underlying pipeline for advanced use (for example,
// embedding it in a higher-level pipeline or inspecting it in tests).
func (c *Client) Pipeline() pipeline.Pipeline {
return c.pl
}
// errorsPolicy maps the final result of the chain to the typed error model: a
// transport failure becomes a *httperr.TransportError (context errors pass
// through unchanged), and a non-2xx response becomes a *httperr.ResponseError.
// Callers place it at [pipeline.StageErrors], the outermost stage, so retry
// still operates on raw responses.
func errorsPolicy() pipeline.Policy {
return pipeline.PolicyFunc(func(req *pipeline.Request) (*http.Response, error) {
resp, err := req.Next()
if err != nil {
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
return nil, httperr.FromError(err, req.Raw())
}
if rerr := httperr.FromResponse(resp); rerr != nil {
return resp, rerr
}
return resp, nil
})
}
// datePolicy stamps the Date header in HTTP-date format (RFC 1123 with GMT, per
// RFC 7231) unless the caller already set one.
func datePolicy() pipeline.Policy {
return pipeline.PolicyFunc(func(req *pipeline.Request) (*http.Response, error) {
if req.Raw().Header.Get(header.Date) == "" {
req.Raw().Header.Set(header.Date, time.Now().UTC().Format(http.TimeFormat))
}
return req.Next()
})
}
// userAgentPolicy sets the User-Agent header unless the caller already provided
// one on the request.
func userAgentPolicy(ua string) pipeline.Policy {
return pipeline.PolicyFunc(func(req *pipeline.Request) (*http.Response, error) {
if req.Raw().Header.Get(header.UserAgent) == "" {
req.Raw().Header.Set(header.UserAgent, ua)
}
return req.Next()
})
}