1// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
3package requestconfig
4
5import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "fmt"
10 "io"
11 "math"
12 "math/rand"
13 "mime"
14 "net/http"
15 "net/url"
16 "runtime"
17 "strconv"
18 "strings"
19 "time"
20
21 "github.com/anthropics/anthropic-sdk-go/internal"
22 "github.com/anthropics/anthropic-sdk-go/internal/apierror"
23 "github.com/anthropics/anthropic-sdk-go/internal/apiform"
24 "github.com/anthropics/anthropic-sdk-go/internal/apiquery"
25)
26
27func getDefaultHeaders() map[string]string {
28 return map[string]string{
29 "User-Agent": fmt.Sprintf("Anthropic/Go %s", internal.PackageVersion),
30 }
31}
32
33func getNormalizedOS() string {
34 switch runtime.GOOS {
35 case "ios":
36 return "iOS"
37 case "android":
38 return "Android"
39 case "darwin":
40 return "MacOS"
41 case "window":
42 return "Windows"
43 case "freebsd":
44 return "FreeBSD"
45 case "openbsd":
46 return "OpenBSD"
47 case "linux":
48 return "Linux"
49 default:
50 return fmt.Sprintf("Other:%s", runtime.GOOS)
51 }
52}
53
54func getNormalizedArchitecture() string {
55 switch runtime.GOARCH {
56 case "386":
57 return "x32"
58 case "amd64":
59 return "x64"
60 case "arm":
61 return "arm"
62 case "arm64":
63 return "arm64"
64 default:
65 return fmt.Sprintf("other:%s", runtime.GOARCH)
66 }
67}
68
69func getPlatformProperties() map[string]string {
70 return map[string]string{
71 "X-Stainless-Lang": "go",
72 "X-Stainless-Package-Version": internal.PackageVersion,
73 "X-Stainless-OS": getNormalizedOS(),
74 "X-Stainless-Arch": getNormalizedArchitecture(),
75 "X-Stainless-Runtime": "go",
76 "X-Stainless-Runtime-Version": runtime.Version(),
77 }
78}
79
80type RequestOption interface {
81 Apply(*RequestConfig) error
82}
83
84type RequestOptionFunc func(*RequestConfig) error
85type PreRequestOptionFunc func(*RequestConfig) error
86
87func (s RequestOptionFunc) Apply(r *RequestConfig) error { return s(r) }
88func (s PreRequestOptionFunc) Apply(r *RequestConfig) error { return s(r) }
89
90func NewRequestConfig(ctx context.Context, method string, u string, body any, dst any, opts ...RequestOption) (*RequestConfig, error) {
91 var reader io.Reader
92
93 contentType := "application/json"
94 hasSerializationFunc := false
95
96 if body, ok := body.(json.Marshaler); ok {
97 content, err := body.MarshalJSON()
98 if err != nil {
99 return nil, err
100 }
101 reader = bytes.NewBuffer(content)
102 hasSerializationFunc = true
103 }
104 if body, ok := body.(apiform.Marshaler); ok {
105 var (
106 content []byte
107 err error
108 )
109 content, contentType, err = body.MarshalMultipart()
110 if err != nil {
111 return nil, err
112 }
113 reader = bytes.NewBuffer(content)
114 hasSerializationFunc = true
115 }
116 if body, ok := body.(apiquery.Queryer); ok {
117 hasSerializationFunc = true
118 q, err := body.URLQuery()
119 if err != nil {
120 return nil, err
121 }
122 params := q.Encode()
123 if params != "" {
124 u = u + "?" + params
125 }
126 }
127 if body, ok := body.([]byte); ok {
128 reader = bytes.NewBuffer(body)
129 hasSerializationFunc = true
130 }
131 if body, ok := body.(io.Reader); ok {
132 reader = body
133 hasSerializationFunc = true
134 }
135
136 // Fallback to json serialization if none of the serialization functions that we expect
137 // to see is present.
138 if body != nil && !hasSerializationFunc {
139 content, err := json.Marshal(body)
140 if err != nil {
141 return nil, err
142 }
143 reader = bytes.NewBuffer(content)
144 }
145
146 req, err := http.NewRequestWithContext(ctx, method, u, nil)
147 if err != nil {
148 return nil, err
149 }
150 if reader != nil {
151 req.Header.Set("Content-Type", contentType)
152 }
153
154 req.Header.Set("Accept", "application/json")
155 req.Header.Set("X-Stainless-Retry-Count", "0")
156 req.Header.Set("X-Stainless-Timeout", "0")
157 for k, v := range getDefaultHeaders() {
158 req.Header.Add(k, v)
159 }
160 req.Header.Set("anthropic-version", "2023-06-01")
161 for k, v := range getPlatformProperties() {
162 req.Header.Add(k, v)
163 }
164 cfg := RequestConfig{
165 MaxRetries: 2,
166 Context: ctx,
167 Request: req,
168 HTTPClient: http.DefaultClient,
169 Body: reader,
170 }
171 cfg.ResponseBodyInto = dst
172 err = cfg.Apply(opts...)
173 if err != nil {
174 return nil, err
175 }
176
177 // This must run after `cfg.Apply(...)` above in case the request timeout gets modified. We also only
178 // apply our own logic for it if it's still "0" from above. If it's not, then it was deleted or modified
179 // by the user and we should respect that.
180 if req.Header.Get("X-Stainless-Timeout") == "0" {
181 if cfg.RequestTimeout == time.Duration(0) {
182 req.Header.Del("X-Stainless-Timeout")
183 } else {
184 req.Header.Set("X-Stainless-Timeout", strconv.Itoa(int(cfg.RequestTimeout.Seconds())))
185 }
186 }
187
188 return &cfg, nil
189}
190
191// This interface is primarily used to describe an [*http.Client], but also
192// supports custom HTTP implementations.
193type HTTPDoer interface {
194 Do(req *http.Request) (*http.Response, error)
195}
196
197// RequestConfig represents all the state related to one request.
198//
199// Editing the variables inside RequestConfig directly is unstable api. Prefer
200// composing the RequestOption instead if possible.
201type RequestConfig struct {
202 MaxRetries int
203 RequestTimeout time.Duration
204 Context context.Context
205 Request *http.Request
206 BaseURL *url.URL
207 // DefaultBaseURL will be used if BaseURL is not explicitly overridden using
208 // WithBaseURL.
209 DefaultBaseURL *url.URL
210 CustomHTTPDoer HTTPDoer
211 HTTPClient *http.Client
212 Middlewares []middleware
213 APIKey string
214 AuthToken string
215 // If ResponseBodyInto not nil, then we will attempt to deserialize into
216 // ResponseBodyInto. If Destination is a []byte, then it will return the body as
217 // is.
218 ResponseBodyInto any
219 // ResponseInto copies the \*http.Response of the corresponding request into the
220 // given address
221 ResponseInto **http.Response
222 Body io.Reader
223}
224
225// middleware is exactly the same type as the Middleware type found in the [option] package,
226// but it is redeclared here for circular dependency issues.
227type middleware = func(*http.Request, middlewareNext) (*http.Response, error)
228
229// middlewareNext is exactly the same type as the MiddlewareNext type found in the [option] package,
230// but it is redeclared here for circular dependency issues.
231type middlewareNext = func(*http.Request) (*http.Response, error)
232
233func applyMiddleware(middleware middleware, next middlewareNext) middlewareNext {
234 return func(req *http.Request) (res *http.Response, err error) {
235 return middleware(req, next)
236 }
237}
238
239func shouldRetry(req *http.Request, res *http.Response) bool {
240 // If there is no way to recover the Body, then we shouldn't retry.
241 if req.Body != nil && req.GetBody == nil {
242 return false
243 }
244
245 // If there is no response, that indicates that there is a connection error
246 // so we retry the request.
247 if res == nil {
248 return true
249 }
250
251 // If the header explicitly wants a retry behavior, respect that over the
252 // http status code.
253 if res.Header.Get("x-should-retry") == "true" {
254 return true
255 }
256 if res.Header.Get("x-should-retry") == "false" {
257 return false
258 }
259
260 return res.StatusCode == http.StatusRequestTimeout ||
261 res.StatusCode == http.StatusConflict ||
262 res.StatusCode == http.StatusTooManyRequests ||
263 res.StatusCode >= http.StatusInternalServerError
264}
265
266func parseRetryAfterHeader(resp *http.Response) (time.Duration, bool) {
267 if resp == nil {
268 return 0, false
269 }
270
271 type retryData struct {
272 header string
273 units time.Duration
274
275 // custom is used when the regular algorithm failed and is optional.
276 // the returned duration is used verbatim (units is not applied).
277 custom func(string) (time.Duration, bool)
278 }
279
280 nop := func(string) (time.Duration, bool) { return 0, false }
281
282 // the headers are listed in order of preference
283 retries := []retryData{
284 {
285 header: "Retry-After-Ms",
286 units: time.Millisecond,
287 custom: nop,
288 },
289 {
290 header: "Retry-After",
291 units: time.Second,
292
293 // retry-after values are expressed in either number of
294 // seconds or an HTTP-date indicating when to try again
295 custom: func(ra string) (time.Duration, bool) {
296 t, err := time.Parse(time.RFC1123, ra)
297 if err != nil {
298 return 0, false
299 }
300 return time.Until(t), true
301 },
302 },
303 }
304
305 for _, retry := range retries {
306 v := resp.Header.Get(retry.header)
307 if v == "" {
308 continue
309 }
310 if retryAfter, err := strconv.ParseFloat(v, 64); err == nil {
311 return time.Duration(retryAfter * float64(retry.units)), true
312 }
313 if d, ok := retry.custom(v); ok {
314 return d, true
315 }
316 }
317
318 return 0, false
319}
320
321// isBeforeContextDeadline reports whether the non-zero Time t is
322// before ctx's deadline. If ctx does not have a deadline, it
323// always reports true (the deadline is considered infinite).
324func isBeforeContextDeadline(t time.Time, ctx context.Context) bool {
325 d, ok := ctx.Deadline()
326 if !ok {
327 return true
328 }
329 return t.Before(d)
330}
331
332// bodyWithTimeout is an io.ReadCloser which can observe a context's cancel func
333// to handle timeouts etc. It wraps an existing io.ReadCloser.
334type bodyWithTimeout struct {
335 stop func() // stops the time.Timer waiting to cancel the request
336 rc io.ReadCloser
337}
338
339func (b *bodyWithTimeout) Read(p []byte) (n int, err error) {
340 n, err = b.rc.Read(p)
341 if err == nil {
342 return n, nil
343 }
344 if err == io.EOF {
345 return n, err
346 }
347 return n, err
348}
349
350func (b *bodyWithTimeout) Close() error {
351 err := b.rc.Close()
352 b.stop()
353 return err
354}
355
356func retryDelay(res *http.Response, retryCount int) time.Duration {
357 // If the API asks us to wait a certain amount of time (and it's a reasonable amount),
358 // just do what it says.
359
360 if retryAfterDelay, ok := parseRetryAfterHeader(res); ok && 0 <= retryAfterDelay && retryAfterDelay < time.Minute {
361 return retryAfterDelay
362 }
363
364 maxDelay := 8 * time.Second
365 delay := time.Duration(0.5 * float64(time.Second) * math.Pow(2, float64(retryCount)))
366 if delay > maxDelay {
367 delay = maxDelay
368 }
369
370 jitter := rand.Int63n(int64(delay / 4))
371 delay -= time.Duration(jitter)
372 return delay
373}
374
375func (cfg *RequestConfig) Execute() (err error) {
376 if cfg.BaseURL == nil {
377 if cfg.DefaultBaseURL != nil {
378 cfg.BaseURL = cfg.DefaultBaseURL
379 } else {
380 return fmt.Errorf("requestconfig: base url is not set")
381 }
382 }
383
384 cfg.Request.URL, err = cfg.BaseURL.Parse(strings.TrimLeft(cfg.Request.URL.String(), "/"))
385 if err != nil {
386 return err
387 }
388
389 if cfg.Body != nil && cfg.Request.Body == nil {
390 switch body := cfg.Body.(type) {
391 case *bytes.Buffer:
392 b := body.Bytes()
393 cfg.Request.ContentLength = int64(body.Len())
394 cfg.Request.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }
395 cfg.Request.Body, _ = cfg.Request.GetBody()
396 case *bytes.Reader:
397 cfg.Request.ContentLength = int64(body.Len())
398 cfg.Request.GetBody = func() (io.ReadCloser, error) {
399 _, err := body.Seek(0, 0)
400 return io.NopCloser(body), err
401 }
402 cfg.Request.Body, _ = cfg.Request.GetBody()
403 default:
404 if rc, ok := body.(io.ReadCloser); ok {
405 cfg.Request.Body = rc
406 } else {
407 cfg.Request.Body = io.NopCloser(body)
408 }
409 }
410 }
411
412 handler := cfg.HTTPClient.Do
413 if cfg.CustomHTTPDoer != nil {
414 handler = cfg.CustomHTTPDoer.Do
415 }
416 for i := len(cfg.Middlewares) - 1; i >= 0; i -= 1 {
417 handler = applyMiddleware(cfg.Middlewares[i], handler)
418 }
419
420 // Don't send the current retry count in the headers if the caller modified the header defaults.
421 shouldSendRetryCount := cfg.Request.Header.Get("X-Stainless-Retry-Count") == "0"
422
423 var res *http.Response
424 var cancel context.CancelFunc
425 for retryCount := 0; retryCount <= cfg.MaxRetries; retryCount += 1 {
426 ctx := cfg.Request.Context()
427 if cfg.RequestTimeout != time.Duration(0) && isBeforeContextDeadline(time.Now().Add(cfg.RequestTimeout), ctx) {
428 ctx, cancel = context.WithTimeout(ctx, cfg.RequestTimeout)
429 defer func() {
430 // The cancel function is nil if it was handed off to be handled in a different scope.
431 if cancel != nil {
432 cancel()
433 }
434 }()
435 }
436
437 req := cfg.Request.Clone(ctx)
438 if shouldSendRetryCount {
439 req.Header.Set("X-Stainless-Retry-Count", strconv.Itoa(retryCount))
440 }
441
442 res, err = handler(req)
443 if ctx != nil && ctx.Err() != nil {
444 return ctx.Err()
445 }
446 if !shouldRetry(cfg.Request, res) || retryCount >= cfg.MaxRetries {
447 break
448 }
449
450 // Prepare next request and wait for the retry delay
451 if cfg.Request.GetBody != nil {
452 cfg.Request.Body, err = cfg.Request.GetBody()
453 if err != nil {
454 return err
455 }
456 }
457
458 // Can't actually refresh the body, so we don't attempt to retry here
459 if cfg.Request.GetBody == nil && cfg.Request.Body != nil {
460 break
461 }
462
463 time.Sleep(retryDelay(res, retryCount))
464 }
465
466 // Save *http.Response if it is requested to, even if there was an error making the request. This is
467 // useful in cases where you might want to debug by inspecting the response. Note that if err != nil,
468 // the response should be generally be empty, but there are edge cases.
469 if cfg.ResponseInto != nil {
470 *cfg.ResponseInto = res
471 }
472 if responseBodyInto, ok := cfg.ResponseBodyInto.(**http.Response); ok {
473 *responseBodyInto = res
474 }
475
476 // If there was a connection error in the final request or any other transport error,
477 // return that early without trying to coerce into an APIError.
478 if err != nil {
479 return err
480 }
481
482 if res.StatusCode >= 400 {
483 contents, err := io.ReadAll(res.Body)
484 res.Body.Close()
485 if err != nil {
486 return err
487 }
488
489 // If there is an APIError, re-populate the response body so that debugging
490 // utilities can conveniently dump the response without issue.
491 res.Body = io.NopCloser(bytes.NewBuffer(contents))
492
493 // Load the contents into the error format if it is provided.
494 aerr := apierror.Error{Request: cfg.Request, Response: res, StatusCode: res.StatusCode}
495 err = aerr.UnmarshalJSON(contents)
496 if err != nil {
497 return err
498 }
499 return &aerr
500 }
501
502 _, intoCustomResponseBody := cfg.ResponseBodyInto.(**http.Response)
503 if cfg.ResponseBodyInto == nil || intoCustomResponseBody {
504 // We aren't reading the response body in this scope, but whoever is will need the
505 // cancel func from the context to observe request timeouts.
506 // Put the cancel function in the response body so it can be handled elsewhere.
507 if cancel != nil {
508 res.Body = &bodyWithTimeout{rc: res.Body, stop: cancel}
509 cancel = nil
510 }
511 return nil
512 }
513
514 contents, err := io.ReadAll(res.Body)
515 res.Body.Close()
516 if err != nil {
517 return fmt.Errorf("error reading response body: %w", err)
518 }
519
520 // If we are not json, return plaintext
521 contentType := res.Header.Get("content-type")
522 mediaType, _, _ := mime.ParseMediaType(contentType)
523 isJSON := strings.Contains(mediaType, "application/json") || strings.HasSuffix(mediaType, "+json")
524 if !isJSON {
525 switch dst := cfg.ResponseBodyInto.(type) {
526 case *string:
527 *dst = string(contents)
528 case **string:
529 tmp := string(contents)
530 *dst = &tmp
531 case *[]byte:
532 *dst = contents
533 default:
534 return fmt.Errorf("expected destination type of 'string' or '[]byte' for responses with content-type '%s' that is not 'application/json'", contentType)
535 }
536 return nil
537 }
538
539 // If the response happens to be a byte array, deserialize the body as-is.
540 switch dst := cfg.ResponseBodyInto.(type) {
541 case *[]byte:
542 *dst = contents
543 }
544
545 err = json.NewDecoder(bytes.NewReader(contents)).Decode(cfg.ResponseBodyInto)
546 if err != nil {
547 return fmt.Errorf("error parsing response json: %w", err)
548 }
549
550 return nil
551}
552
553func ExecuteNewRequest(ctx context.Context, method string, u string, body any, dst any, opts ...RequestOption) error {
554 cfg, err := NewRequestConfig(ctx, method, u, body, dst, opts...)
555 if err != nil {
556 return err
557 }
558 return cfg.Execute()
559}
560
561func (cfg *RequestConfig) Clone(ctx context.Context) *RequestConfig {
562 if cfg == nil {
563 return nil
564 }
565 req := cfg.Request.Clone(ctx)
566 var err error
567 if req.Body != nil {
568 req.Body, err = req.GetBody()
569 }
570 if err != nil {
571 return nil
572 }
573 new := &RequestConfig{
574 MaxRetries: cfg.MaxRetries,
575 RequestTimeout: cfg.RequestTimeout,
576 Context: ctx,
577 Request: req,
578 BaseURL: cfg.BaseURL,
579 HTTPClient: cfg.HTTPClient,
580 Middlewares: cfg.Middlewares,
581 APIKey: cfg.APIKey,
582 AuthToken: cfg.AuthToken,
583 }
584
585 return new
586}
587
588func (cfg *RequestConfig) Apply(opts ...RequestOption) error {
589 for _, opt := range opts {
590 err := opt.Apply(cfg)
591 if err != nil {
592 return err
593 }
594 }
595 return nil
596}
597
598// PreRequestOptions is used to collect all the options which need to be known before
599// a call to [RequestConfig.ExecuteNewRequest], such as path parameters
600// or global defaults.
601// PreRequestOptions will return a [RequestConfig] with the options applied.
602//
603// Only request option functions of type [PreRequestOptionFunc] are applied.
604func PreRequestOptions(opts ...RequestOption) (RequestConfig, error) {
605 cfg := RequestConfig{}
606 for _, opt := range opts {
607 if opt, ok := opt.(PreRequestOptionFunc); ok {
608 err := opt.Apply(&cfg)
609 if err != nil {
610 return cfg, err
611 }
612 }
613 }
614 return cfg, nil
615}
616
617// WithDefaultBaseURL returns a RequestOption that sets the client's default Base URL.
618// This is always overridden by setting a base URL with WithBaseURL.
619// WithBaseURL should be used instead of WithDefaultBaseURL except in internal code.
620func WithDefaultBaseURL(baseURL string) RequestOption {
621 u, err := url.Parse(baseURL)
622 return RequestOptionFunc(func(r *RequestConfig) error {
623 if err != nil {
624 return err
625 }
626 r.DefaultBaseURL = u
627 return nil
628 })
629}