requestconfig.go

  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}