feat: simplify HTTP client proxy pools and authenticated JSON calls
This commit is contained in:
119
httpc/json.go
Normal file
119
httpc/json.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package httpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSON bounds response memory, closes the body, and decodes only 2xx responses.
|
||||
// Optional headers/limits belong to this call and never alter the client.
|
||||
// output=nil discards a bounded stream; it does not buffer or validate JSON.
|
||||
// Empty successful bodies leave output unchanged. StatusError contains no body.
|
||||
func (c *Client) JSON(ctx context.Context, method, url string, input, output any, options ...RequestOption) error {
|
||||
if ctx == nil {
|
||||
return errors.New("httpc: nil context")
|
||||
}
|
||||
if c.closed.Load() {
|
||||
return ErrClosed
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
settings := requestOptions{maxResponseBytes: c.config.MaxResponseBytes}
|
||||
if len(options) > 0 {
|
||||
var err error
|
||||
settings, err = jsonOptions(c.config.MaxResponseBytes, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var body io.Reader
|
||||
if input != nil {
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = bytes.NewReader(data)
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if settings.headers != nil {
|
||||
request.Header = settings.headers
|
||||
}
|
||||
if input != nil {
|
||||
if _, exists := request.Header["Content-Type"]; !exists {
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
}
|
||||
response, err := c.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
drainStatusBody(response, settings.maxResponseBytes)
|
||||
return &StatusError{StatusCode: response.StatusCode}
|
||||
}
|
||||
if response.ContentLength > settings.maxResponseBytes {
|
||||
return ErrResponseTooLarge
|
||||
}
|
||||
limited := io.LimitReader(response.Body, settings.maxResponseBytes+1)
|
||||
if output == nil {
|
||||
count, err := io.Copy(io.Discard, limited)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > settings.maxResponseBytes {
|
||||
return ErrResponseTooLarge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
data, err := io.ReadAll(limited)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int64(len(data)) > settings.maxResponseBytes {
|
||||
return ErrResponseTooLarge
|
||||
}
|
||||
if len(bytes.TrimSpace(data)) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, output)
|
||||
}
|
||||
|
||||
// A known small status body may still be slow. Spend at most a short grace
|
||||
// period trying to retain the connection, then cancel the HTTP read. The status
|
||||
// remains authoritative and the deferred Close still owns body cleanup.
|
||||
func drainStatusBody(response *http.Response, limit int64) {
|
||||
if response.ContentLength < 0 || response.ContentLength > min(limit, 32<<10) {
|
||||
return
|
||||
}
|
||||
body, ok := response.Body.(*responseBody)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
timer := time.AfterFunc(25*time.Millisecond, body.cancel)
|
||||
defer timer.Stop()
|
||||
drainSmallBody(response, limit)
|
||||
}
|
||||
|
||||
// Keep the option builder out of the allocation path of existing plain calls.
|
||||
func jsonOptions(limit int64, options []RequestOption) (requestOptions, error) {
|
||||
settings := requestOptions{maxResponseBytes: limit}
|
||||
for _, option := range options {
|
||||
if option == nil {
|
||||
return requestOptions{}, errors.New("httpc: nil request option")
|
||||
}
|
||||
if err := option(&settings); err != nil {
|
||||
return requestOptions{}, err
|
||||
}
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
Reference in New Issue
Block a user