1package typesafe23import (4 "bytes"5 "context"6 "errors"7 "fmt"8 "io"9 "net"10 "net/http"11 "runtime"12 "strconv"13)1415const maxResponseBytes = 16 << 201617type wireResponse struct {18 status int19 header http.Header20 body []byte21}2223func (r *wireResponse) requestID() string {24 for _, name := range []string{"x-request-id", "request-id", "x-typesafe-request-id"} {25 if value := r.header.Get(name); value != "" {26 return value27 }28 }29 return ""30}3132type transportRequest struct {33 method string34 url string35 body []byte36}3738type transportError struct {39 err error40 timeout bool41}4243func (e *transportError) Error() string { return "typesafe: HTTP transport failed: " + e.err.Error() }44func (e *transportError) Unwrap() error { return e.err }4546func (c *Client) send(ctx context.Context, method, path string, body []byte) (*wireResponse, error) {47 request := transportRequest{method: method, url: c.config.baseURL + path, body: body}48 for attempt := 0; ; attempt++ {49 if err := ctx.Err(); err != nil {50 return nil, err51 }52 response, err := c.attempt(ctx, request, attempt)53 if ctx.Err() != nil {54 return nil, ctx.Err()55 }56 var headers http.Header57 if err == nil {58 if response.status >= 200 && response.status < 300 {59 return response, nil60 }61 err = response.apiError(request)62 if !retryableStatus(response.status) {63 return nil, err64 }65 headers = response.header66 } else if !c.config.retry.retriesError(err) {67 return nil, err68 }69 if attempt >= c.config.retry.MaxRetries {70 return nil, err71 }72 if err := waitForRetry(ctx, retryDelay(c.config.retry, attempt, headers)); err != nil {73 return nil, err74 }75 }76}7778func (r *wireResponse) apiError(request transportRequest) *APIError {79 return &APIError{80 StatusCode: r.status, RequestID: r.requestID(), Header: r.header,81 Body: r.body, Method: request.method, URL: request.url,82 Message: http.StatusText(r.status),83 }84}8586func (c *Client) attempt(ctx context.Context, spec transportRequest, attempt int) (*wireResponse, error) {87 attemptCtx, cancel := context.WithTimeout(ctx, c.config.timeout)88 defer cancel()89 request, err := http.NewRequestWithContext(attemptCtx, spec.method, spec.url, bytes.NewReader(spec.body))90 if err != nil {91 return nil, fmt.Errorf("typesafe: construct HTTP request: %w", err)92 }93 request.Header = c.requestHeaders(spec.body != nil, attempt)94 response, err := c.config.httpClient.Do(request)95 if err != nil {96 return nil, wrapTransportError(attemptCtx, err)97 }98 wire, err := readResponse(response)99 if err != nil {100 if _, ok := errors.AsType[*ResponseError](err); ok {101 return nil, err102 }103 return nil, wrapTransportError(attemptCtx, err)104 }105 return wire, nil106}107108func (c *Client) requestHeaders(hasBody bool, attempt int) http.Header {109 headers := c.config.headers.Clone()110 headers.Set("Authorization", "Bearer "+c.config.apiKey)111 headers.Set("Accept", "application/json")112 headers.Set("User-Agent", "typesafe-go/"+Version)113 headers.Set("X-TypeSafe-SDK", "typesafe-go/"+Version)114 headers.Set("X-TypeSafe-Runtime", runtime.Version()+" "+runtime.GOOS+"/"+runtime.GOARCH)115 headers.Del("Content-Type")116 if hasBody {117 headers.Set("Content-Type", "application/json")118 }119 headers.Del("X-TypeSafe-Retry-Count")120 if attempt > 0 {121 headers.Set("X-TypeSafe-Retry-Count", strconv.Itoa(attempt))122 }123 return headers124}125126func readResponse(response *http.Response) (*wireResponse, error) {127 wire := &wireResponse{status: response.StatusCode, header: response.Header.Clone()}128 body, readErr := io.ReadAll(io.LimitReader(response.Body, maxResponseBytes+1))129 closeErr := response.Body.Close()130 if err := errors.Join(readErr, closeErr); err != nil {131 return nil, err132 }133 if len(body) > maxResponseBytes {134 return nil, &ResponseError{RequestID: wire.requestID(), Err: fmt.Errorf("response exceeds 16 MiB limit")}135 }136 wire.body = body137 return wire, nil138}139140func wrapTransportError(ctx context.Context, err error) error {141 if ctx.Err() != nil {142 err = ctx.Err()143 }144 timeout := errors.Is(err, context.DeadlineExceeded)145 if network, ok := errors.AsType[net.Error](err); ok {146 timeout = timeout || network.Timeout()147 }148 return &transportError{err: err, timeout: timeout}149}