100 lines
2.8 KiB
Go
100 lines
2.8 KiB
Go
package httpc
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type requestOptions struct {
|
|
headers http.Header
|
|
maxResponseBytes int64
|
|
}
|
|
|
|
// 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
|
|
}
|