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)<