package httpkit import ( "context" "errors" "fmt" "io" "net/http" ) // Download fetches a URL and returns the response body as bytes. // maxBytes limits the response size; 0 means no limit. // // Unlike Do, Download does not apply default headers (download URLs are // typically pre-signed or public), does not marshal JSON, and does not // retry. It does respect the client's timeout. func (c *Client) Download(ctx context.Context, url string, maxBytes int64) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("create download request: %w", err) } resp, err := c.http.Do(req) if err != nil { if errors.Is(err, context.DeadlineExceeded) || isTimeoutError(err) { return nil, fmt.Errorf("download: %w", ErrTimeout) } return nil, fmt.Errorf("download: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) return nil, &APIError{ StatusCode: resp.StatusCode, Body: string(body), sentinel: ClassifyStatus(resp.StatusCode), } } var reader io.Reader = resp.Body if maxBytes > 0 { reader = io.LimitReader(resp.Body, maxBytes+1) } data, err := io.ReadAll(reader) if err != nil { return nil, fmt.Errorf("read download body: %w", err) } if maxBytes > 0 && int64(len(data)) > maxBytes { return nil, fmt.Errorf("download exceeds %d byte limit", maxBytes) } return data, nil }