140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
package httpc
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type requestOptions struct {
|
|
headers http.Header
|
|
maxResponseBytes int64
|
|
successStatuses [2]uint64
|
|
restrictStatus bool
|
|
}
|
|
|
|
// RequestOption applies only to one JSON call. Returned options can be reused
|
|
// concurrently; request headers are independent, with no client-wide credentials.
|
|
type RequestOption func(*requestOptions) error
|
|
|
|
// WithHeader sets a header for this call; later options replace earlier values.
|
|
// Framing and Host must be controlled through a standard Request passed to Do.
|
|
func WithHeader(name, value string) RequestOption {
|
|
name = http.CanonicalHeaderKey(name)
|
|
err := checkHeader(name, []string{value})
|
|
return func(o *requestOptions) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if o.headers == nil {
|
|
o.headers = make(http.Header)
|
|
}
|
|
o.headers[name] = []string{value}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithHeaders snapshots headers immediately. Editing the original map after
|
|
// construction does not change the option; each invocation gets separate slices.
|
|
func WithHeaders(headers http.Header) RequestOption {
|
|
snapshot := make(http.Header, len(headers))
|
|
var err error
|
|
for key, values := range headers {
|
|
name := http.CanonicalHeaderKey(key)
|
|
if err = checkHeader(name, values); err != nil {
|
|
break
|
|
}
|
|
if _, exists := snapshot[name]; exists {
|
|
err = errors.New("httpc: duplicate canonical header name")
|
|
break
|
|
}
|
|
snapshot[name] = append([]string(nil), values...)
|
|
}
|
|
return func(o *requestOptions) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if o.headers == nil {
|
|
o.headers = make(http.Header, len(snapshot))
|
|
}
|
|
for name, values := range snapshot {
|
|
o.headers[name] = append([]string(nil), values...)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// WithMaxResponseBytes overrides JSON's bounded response read for one call.
|
|
// The size is positive and at most 1 GiB. Do remains a caller-owned stream.
|
|
func WithMaxResponseBytes(limit int64) RequestOption {
|
|
return func(o *requestOptions) error {
|
|
if limit <= 0 || limit > maxResponseBytes {
|
|
return errors.New("httpc: response limit must be positive and at most 1 GiB")
|
|
}
|
|
o.maxResponseBytes = limit
|
|
return nil
|
|
}
|
|
}
|
|
func checkHeader(name string, values []string) error {
|
|
if name == "" {
|
|
return errors.New("httpc: header name must not be empty")
|
|
}
|
|
for _, c := range []byte(name) {
|
|
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("!#$%&'*+-.^_`|~", rune(c)) {
|
|
continue
|
|
}
|
|
return errors.New("httpc: invalid header name")
|
|
}
|
|
switch name {
|
|
case "Host", "Content-Length", "Transfer-Encoding", "Trailer":
|
|
return errors.New("httpc: use Do for Host or request framing controls")
|
|
}
|
|
for _, value := range values {
|
|
for i := 0; i < len(value); i++ {
|
|
c := value[i]
|
|
if c == '\x7f' || c < ' ' && c != '\t' {
|
|
return errors.New("httpc: invalid header value")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// WithSuccessStatuses restricts this JSON call to the listed 2xx statuses.
|
|
// Omission keeps all 2xx successful. Empty/out-of-range lists are rejected before
|
|
// sending a request. Options snapshot their input and can be reused concurrently.
|
|
// The policy does not add retries or validate application-specific JSON fields.
|
|
func WithSuccessStatuses(statuses ...int) RequestOption {
|
|
var accepted [2]uint64
|
|
var err error
|
|
if len(statuses) == 0 {
|
|
err = errors.New("httpc: at least one success status is required")
|
|
}
|
|
for _, status := range statuses {
|
|
if status < 200 || status >= 300 {
|
|
err = errors.New("httpc: success statuses must be between 200 and 299")
|
|
break
|
|
}
|
|
bit := status - 200
|
|
accepted[bit/64] |= uint64(1) << uint(bit%64)
|
|
}
|
|
return func(o *requestOptions) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.successStatuses = accepted
|
|
o.restrictStatus = true
|
|
return nil
|
|
}
|
|
}
|
|
func (o requestOptions) accepts(status int) bool {
|
|
if status < 200 || status >= 300 {
|
|
return false
|
|
}
|
|
if !o.restrictStatus {
|
|
return true
|
|
}
|
|
bit := status - 200
|
|
return o.successStatuses[bit/64]&(uint64(1)<<uint(bit%64)) != 0
|
|
}
|