feat: simplify HTTP client proxy pools and authenticated JSON calls
This commit is contained in:
99
httpc/request.go
Normal file
99
httpc/request.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user