120 lines
3.2 KiB
Go
120 lines
3.2 KiB
Go
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
|
|
}
|