openai/openai-go

Public

mirrored from https://github.com/openai/openai-goAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat-client-unwrap-error-message

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/requestconfig/requestconfig.go

601lines · modecode

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