Skip to content
Open
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ if err != nil {
</tr>
<tr><td>15+ lines</td><td>4+ lines</td></tr></tbody></table>

### POST a raw body with an explicit Content-Type

`Body` sends a raw/streamed body and sets no Content-Type; declare one with
`HeaderPairs` when the server expects a specific type:

```go
// application/xml
r, err := requests.Post("http://example.com",
requests.Body(strings.NewReader(xmlStr)),
requests.HeaderPairs("Content-Type", "application/xml"))

// application/octet-stream
r, err = requests.Post("http://example.com",
requests.Data([]byte(binaryData)),
requests.HeaderPairs("Content-Type", "application/octet-stream"))
```

`Data` also deduces a Content-Type from the data type when none is set, but
the deduction is a heuristic (e.g. XML is detected as `text/xml`, not
`application/xml`), so set it explicitly when the exact type matters.

### GET a JSON object with context

<table>
Expand Down
21 changes: 10 additions & 11 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ func WithTimeout(timeout time.Duration) ClientOption {
}
}

// WithTransport specifies a transport for client.
// WithTransport specifies the transport for the client.
func WithTransport(transport http.RoundTripper) ClientOption {
return func(c *Client) {
c.client.Transport = transport
}
}

// WithInterceptor specifies an interceptor for client.
// WithInterceptor specifies the interceptor for the client.
// You can use [ChainInterceptors] to chain multiple interceptors into one.
func WithInterceptor(interceptor InterceptorFunc) ClientOption {
return func(c *Client) {
Expand All @@ -40,7 +40,7 @@ type Client struct {
interceptor InterceptorFunc
}

// NewClient creates a new client to serve HTTP requests.
// NewClient creates a new Client for sending HTTP requests.
func NewClient(setters ...ClientOption) *Client {
client := newDefaultClient()
for _, setter := range setters {
Expand All @@ -49,10 +49,10 @@ func NewClient(setters ...ClientOption) *Client {
return client
}

// request is the common func to send an HTTP request.
// request sends an HTTP request via the client.
func (c *Client) request(method, url string, opts *Options, body []byte) (*Response, error) {
ctx := opts.ctx
if opts.Timeout > 0 { // ctx with timeout if specified
if opts.Timeout > 0 { // apply per-request timeout
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
defer cancel()
Expand Down Expand Up @@ -85,8 +85,7 @@ func (c *Client) request(method, url string, opts *Options, body []byte) (*Respo
// do sends an HTTP request and returns an HTTP response, following policy
// (such as redirects, cookies, auth) as configured on the client.
func (c *Client) do(_ context.Context, r *Request) (*Response, error) {
// If the returned error is nil, the Response will contain
// a non-nil Body which the user is expected to close.
// The response body is read and closed by newResponse.
resp, err := c.client.Do(r.Request)
if err != nil {
return nil, err
Expand All @@ -102,7 +101,7 @@ func (c *Client) do(_ context.Context, r *Request) (*Response, error) {
return newResponse(resp, r.opts)
}

// Get sends an HTTP request with GET method.
// Get sends an HTTP GET request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
Expand All @@ -115,23 +114,23 @@ func (c *Client) Post(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodPost, url, options...)
}

// Put sends an HTTP request with PUT method.
// Put sends an HTTP PUT request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func (c *Client) Put(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodPut, url, options...)
}

// Patch sends an HTTP request with PATCH method.
// Patch sends an HTTP PATCH request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func (c *Client) Patch(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodPatch, url, options...)
}

// Delete sends an HTTP request with DELETE method.
// Delete sends an HTTP DELETE request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
Expand Down
2 changes: 1 addition & 1 deletion default.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func getDefaultClient() *Client {
return defaultClient
}

// InitDefaultClient initializes the default client with given options.
// InitDefaultClient initializes the default client with the given options.
func InitDefaultClient(setters ...ClientOption) {
client := getDefaultClient()
for _, setter := range setters {
Expand Down
8 changes: 3 additions & 5 deletions interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import (
"context"
)

// Do is called by Interceptor to complete HTTP requests.
// Do is called by an interceptor to complete the HTTP request.
type Do func(ctx context.Context, r *Request) (*Response, error)

// InterceptorFunc provides a hook to intercept the execution of an HTTP request
// invocation. When an interceptor(s) is set, requests delegates all HTTP
// client invocations to the interceptor, and it is the responsibility of the
// interceptor to call do to complete the processing of the HTTP request.
// InterceptorFunc intercepts an HTTP request. When set, the client delegates
// the request to the interceptor, which must call do to complete it.
type InterceptorFunc func(ctx context.Context, r *Request, do Do) (*Response, error)

// ChainInterceptors chains multiple interceptors into one.
Expand Down
8 changes: 4 additions & 4 deletions method.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package requests

// Get sends an HTTP request with GET method.
// Get sends an HTTP GET request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
Expand All @@ -13,23 +13,23 @@ func Post(url string, options ...Option) (*Response, error) {
return getDefaultClient().Post(url, options...)
}

// Put sends an HTTP request with PUT method.
// Put sends an HTTP PUT request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func Put(url string, options ...Option) (*Response, error) {
return getDefaultClient().Put(url, options...)
}

// Patch sends an HTTP request with PATCH method.
// Patch sends an HTTP PATCH request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func Patch(url string, options ...Option) (*Response, error) {
return getDefaultClient().Patch(url, options...)
}

// Delete sends an HTTP request with DELETE method.
// Delete sends an HTTP DELETE request.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
Expand Down
72 changes: 44 additions & 28 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/Wenchy/requests/internal/auth"
)

// Options defines all optional parameters for HTTP request.
// Options defines the optional parameters for an HTTP request.
type Options struct {
ctx context.Context

Expand Down Expand Up @@ -48,7 +48,7 @@ type Options struct {
// Option is the functional option type.
type Option func(*Options)

// newDefaultOptions creates a new default HTTP options.
// newDefaultOptions creates default HTTP options.
func newDefaultOptions() *Options {
return &Options{
ctx: context.Background(),
Expand All @@ -67,7 +67,7 @@ func parseOptions(options ...Option) *Options {

// Context sets the HTTP request context.
//
// For outgoing client request, the context controls the entire lifetime of
// For an outgoing client request, the context controls the entire lifetime of
// a request and its response: obtaining a connection, sending the request,
// and reading the response headers and body.
func Context(ctx context.Context) Option {
Expand Down Expand Up @@ -111,7 +111,7 @@ func Headers[T map[string]string | http.Header](headers T) Option {
}
}

// HeaderPairs sets HTTP headers formed by the mapping of key-value pairs.
// HeaderPairs sets HTTP headers from key-value pairs.
// The keys should be in canonical form, as returned by
// [http.CanonicalHeaderKey]. It panics if len(kv) is odd.
//
Expand Down Expand Up @@ -140,8 +140,7 @@ func HeaderPairs(kv ...string) Option {
return Headers(headers)
}

// Params sets the given query parameters into the URL query string.
// Two types are supported:
// Params sets the URL query string parameters. Two types are supported:
//
// # Type 1: map[string]string
//
Expand Down Expand Up @@ -176,7 +175,7 @@ func Params[T map[string]string | url.Values](params T) Option {
}
}

// ParamPairs sets the query parameters formed by the mapping of key-value pairs.
// ParamPairs sets query parameters from key-value pairs.
// It panics if len(kv) is odd.
//
// Values with the same key will be merged into a list:
Expand All @@ -202,30 +201,44 @@ func ParamPairs(kv ...string) Option {
return Params(params)
}

// Body sets io.Reader to hold request body.
// Body sets the request body from an [io.Reader]. It does not set
// Content-Type and streams the reader as-is; declare a Content-Type via
// [Headers] or [HeaderPairs] when the server expects one (e.g.
// "application/xml", "text/html", "application/octet-stream").
func Body(body io.Reader) Option {
return func(opts *Options) {
opts.Body = body
opts.bodyType = bodyTypeDefault
}
}

// Data sets data of request body. It also deduces Content-Type based on
// input data types:
// Data sets the request body from data. Content-Type is resolved in two
// cases: if the caller sets it via [Headers] or [HeaderPairs], that value
// is used as-is; otherwise it is deduced from data's type:
//
// 1. auto deduce by [http.DetectContentType]: io.Reader, []byte
// 2. "application/json": struct, slice(except []byte), and map
// 3. "text/plain": others
// - io.Reader (e.g. *bytes.Buffer): read and detected via [http.DetectContentType]
// - []byte or *[]byte: detected via [http.DetectContentType]
// - struct, map, slice (except []byte): "application/json"
// - otherwise: "text/plain", formatted with %v
//
// The deduction is a heuristic and may be imprecise — for example XML is
// detected as "text/xml" (or "text/plain" without an XML declaration), not
// "application/xml" — so set Content-Type explicitly when the exact type
// matters.
//
// Non-reader pointers are dereferenced before the kind is determined, so
// *struct and *[]byte behave like their pointed-to values. A typed nil
// pointer such as (*T)(nil) falls into the last case and is sent as "<nil>".
func Data(data any) Option {
return func(opts *Options) {
opts.Data = data
opts.bodyType = bodyTypeData
}
}

// Form sets the given form values into the request body.
// It also sets the Content-Type as "application/x-www-form-urlencoded".
// Two types are supported:
// Form sets the request body from form values, and forces Content-Type to
// "application/x-www-form-urlencoded" (overriding any caller value). Two
// types are supported:
//
// # Type 1: map[string]string
//
Expand Down Expand Up @@ -261,7 +274,7 @@ func Form[T map[string]string | url.Values](params T) Option {
}
}

// FormPairs sets form values by the mapping of key-value pairs.
// FormPairs sets form values from key-value pairs.
// It panics if len(kv) is odd.
//
// Values with the same key will be merged into a list:
Expand All @@ -287,17 +300,20 @@ func FormPairs(kv ...string) Option {
return Form(form)
}

// JSON marshals the given struct as JSON into the request body.
// It also sets the Content-Type as "application/json".
// JSON marshals v as JSON into the request body, and forces Content-Type to
// "application/json" (overriding any caller value). For a custom JSON-based
// media type (e.g. "application/ld+json"), use [Data] with an explicit
// Content-Type instead.
func JSON(v any) Option {
return func(opts *Options) {
opts.JSON = v
opts.bodyType = bodyTypeJSON
}
}

// Files sets files to a map of (field, fileHandler).
// It also sets the Content-Type as "multipart/form-data".
// Files sets files as a map of field to file handler, and forces
// Content-Type to "multipart/form-data" with the multipart boundary
// (overriding any caller value).
func Files(files map[string]*os.File) Option {
return func(opts *Options) {
if opts.Files != nil {
Expand All @@ -311,21 +327,21 @@ func Files(files map[string]*os.File) Option {
}
}

// ToText unmarshals HTTP response body to string.
// ToText captures the response body as text into the string pointed to by v.
func ToText(v *string) Option {
return func(opts *Options) {
opts.ToText = v
}
}

// ToJSON unmarshals HTTP response body to given struct as JSON.
// ToJSON decodes the response body into v as JSON.
func ToJSON(v any) Option {
return func(opts *Options) {
opts.ToJSON = v
}
}

// BasicAuth is the option to implement HTTP Basic Auth.
// BasicAuth enables HTTP Basic Auth with the given username and password.
func BasicAuth(username, password string) Option {
return func(opts *Options) {
opts.AuthInfo = &auth.AuthInfo{
Expand All @@ -343,8 +359,8 @@ func Timeout(timeout time.Duration) Option {
}
}

// Dump dumps outgoing client request and response to the corresponding
// input param (req or resp) if not nil.
// Dump dumps the outgoing request into req and the response into resp,
// when the corresponding pointer is non-nil.
//
// Refer:
// - https://pkg.go.dev/net/http/httputil#DumpRequestOut
Expand All @@ -356,8 +372,8 @@ func Dump(req, resp *string) Option {
}
}

// Interceptor prepends an interceptor to environment interceptors for current
// request only.
// Interceptor prepends an interceptor to the client interceptors for the
// current request only.
func Interceptor(interceptor InterceptorFunc) Option {
return func(opts *Options) {
opts.Interceptor = interceptor
Expand Down
Loading
Loading