Files
go-omni/httpc/request.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
}