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 }