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