Patch is an HTTP client built on top of net/http that that helps you make API requests without the boilerplate.
- Automatic encoding & decoding of bodies
- Option to bring your own
http.Client{}
- Easy asynchronous requests
- Response status code validation
go get github.com/jakewright/patch
The New
function will return a client with sensible defaults
c := patch.New()
The defaults can be overridden with Options.
c := patch.New(
// The default timeout is 30 seconds. This can be
// changed. Setting a timeout of 0 means no timeout.
patch.WithTimeout(10 * time.Second),
// The default status validator returns true for
// any 2xx status code. To remove the status
// validator, pass nil instead of a func.
patch.WithStatusValidator(func(status int) bool {
return status == 200
}),
// By default, request bodies are encoded as JSON.
// This can be changed by providing a different
// Encoder. If a request has its own Encoder set,
// it will override the client's Encoder.
patch.WithEncoder(&patch.EncoderFormURL{}),
)
Custom base client
Patch creates an http.Client{}
which it uses to make requests. If you'd like to provide your own instance, use the NewFromBaseClient
function.
bc := http.Client{}
c := NewFromBaseClient(&bc)
For flexibility, a custom base client doesn't have to be of type http.Client{}
. It just has to implement the following interface. Note that the WithTimeout
option won't work with non-standard base client types.
An http.Client
can be wrapped in a custom Doer
implementation to build middleware.
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
user := struct{
Name string `json:"name"`
Age int `json:"age"
}{}
// The response is returned and also decoded into the last argument
rsp, err := client.Get(ctx, http://example.com/user/204, &user)
if err != nil {
panic(err)
}
The response type embeds the original http.Response{}
but provides some convenience functions.
// Read the body as a []byte or string
b, err := rsp.BodyBytes()
s, err := rsp.BodyString()
The body can be read an unlimited number of times. The underlying rsp.Body
is also available as normal.
The Post()
function takes an extra argument: the body. By default, it will be encoded as JSON and an application/json; charset=utf-8
Content-Type header will be set.
Helper functions also exist for PUT
, PATCH
and DELETE
.
body := struct{
Name string `json:"name"`
Age int `json:"age"`
}{
Name: "Homer Simpson",
Age: 39,
}
// If desired, the response body can be decoded into the last argument.
rsp, err := client.Post(ctx, "http://example.com/users", &body, nil)
Note that the response is not decoded if the request fails, including if status code validation fails. See the section on error handling for more information.
The helper functions Get
, Post
, Put
, Patch
and Delete
are built on top the of Send
function. You can use this directly for more control over the request, including making asynchronous requests.
req := &patch.Request{
Method: "GET"
URL: "http://example.com"
}
// Send is non-blocking and returns a Future
ftr := client.Send(&req)
// Do other work
// Response blocks until the response is available
rsp, err := ftr.Response()
By default, requests are encoded as JSON. The default encoding can be changed by using the WithEncoder()
option when creating the client.
Encoding can also be set per-request by setting the Encoder
field on the request struct. If this is not nil
, it will override the client's default Encoder.
req := &patch.Request{
Encoder: &patch.EncoderFormURL{},
}
JSON encoder
The JSON encoder uses encoding/json
to marshal the body into JSON. The Content-Type header is set to application/json; charset=utf-8
but this can be changed by setting the CustomContentType
field on the EncoderJSON{}
struct.
Form URL encoder
The Form encoder will marshal types as follows:
- If the body is of type
url.Values{}
ormap[string][]string
, it is encoded using Values.Encode. - If the body is of type
map[string]string
, it is converted to aurl.Values{}
and encoded as above. - If the body is of any other type, it is converted to a
url.Values{}
bygorilla/schema
and then encoded as above.
The tag alias used by gorilla/schema
is configurable on the EncoderFormURL{}
struct.
The Content-Type header is set to application/x-www-form-urlencoded
but this can be changed by setting the CustomContentType
field on the EncoderFormURL{}
struct.
enc := &patch.EncoderFormURL{
TagAlias: "url",
}
client, err := patch.New(patch.WithEncoder(enc))
if err != nil {
panic(err)
}
// The body will be encoded as "name=Homer&age=39"
body := struct{
Name string `url:"name"`
Age int `url:"age"`
}{
Name: "Homer Simpson",
Age: 39,
}
rsp, err := client.Post(ctx, "http://example.com", &body, nil)
Custom encoder
A custom encoder can be provided. It must implement the following interface.
type Encoder interface {
ContentType() string
Encode(interface{}) (io.Reader, error)
}
If the final argument v
to Get
, Post
, Put
, Patch
or Delete
is not nil
, then the body will be decoded into the value pointed to by v
. The decoder to use will be inferred from the response's Content-Type header. To explicitly specify a Decoder, use the convenience functions on the Response
struct.
rsp, err := client.Get(ctx, "http://example.com", nil, nil)
if err != nil {
panic(err)
}
v := struct{...}{}
// Decode will infer the decoder from the Content-Type header.
err := rsp.Decode(&v)
// DecodeJSON will decode the body as JSON, regardless of the Content-Type header.
err := rsp.DecodeJSON(&v)
// DecodeUsing will decode the body using a custom Decoder.
err := rsp.DecodeUsing(dec, &v)
Decode hooks
Sometimes, you want to decode into different targets depending on the response status code. Arguments to the decode functions can be wrapped in a DecodeHook
to specify for which status codes the target should be used.
err := rsp.Decode(patch.On2xx(&result), patch.On4xx(&clientErr), patch.On5xx(&serverErr))
Decode hooks work as the final argument to the method helper functions too.
Specific status codes can be targeted using the patch.OnStatus(404, &target)
hook. Of course, you can write your own hooks too.
The method helper functions Get
, Post
, Put
, Patch
and Delete
will not try to decode the body if the baseClient
returned an error, of if the status validator returns false.
If the request succeeds but decoding the body fails, the decoding error will be returned.
Some errors are identifiable using errors.As()
. See errors.go
for a list of typed errors that can be returned.
Here is an example of integrating with the GitHub API to list repositories by user, inspired by dghubble/sling.
type Repository struct {
ID int `json:"id"`
Name string `json:"name"`
}
type GithubError struct {
Message string `json:"message"`
}
func (e *GithubError) Error() string {
return e.Message
}
type RepoService struct {
client *patch.Client
}
func NewRepoService() *RepoService {
sv := func(status int) bool {
if status >= 200 && status < 300 {
return true
}
// Allow 4xx status codes because we
// expect to be able to decode them
if status >= 400 && status < 500 {
return true
}
return false
}
return &RepoService{
client: patch.New(
patch.WithBaseURL("https://api.github.com"),
patch.WithStatusValidator(sv),
),
}
}
func (s *RepoService) List(ctx context.Context, username string) ([]*Repository, error) {
path := fmt.Sprintf("/users/%s/repos", username)
rsp, err := s.client.Get(ctx, path, nil)
if err != nil {
panic(err)
}
var repos []*Repository
var apiErr *GithubError
if err := rsp.DecodeJSON(On2xx(repos), On4xx(apiErr)); err != nil {
return nil, err
}
return repos, apiErr
}
Inspired by a multitude of great HTTP clients, including but not limited to: