httpclient

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 31, 2023 License: MIT Imports: 12 Imported by: 1

README

License go.mod Go version GoDoc Latest tag Go Report

httpclient

This package offers an easy way of building http request and handling responses.

Its main goal is to reduce the code required to perform a classic http request, which accelerate development, ease maintenance and tests.

A classic HTTP request

Say we want to perform a POST request to example.com on /users/ to create a new user, with a json body containing the email.

This endpoint return a 201 Created if it succeeds, and a json body containing the user id.

A typical go code would look like this:

func performCreateUserRequest(ctx context.Context, client *http.Client, userEmail string) (uint64, error) {
	body, err := json.Marshal(&CreateUserRequest{Email: userEmail})
	if err != nil {
		return 0, fmt.Errorf("Unable to marshal json: %v", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com/users/", bytes.NewReader(body))
	if err != nil {
		return 0, fmt.Errorf("unable to marshal json: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := client.Do(req)
	if err != nil {
		return 0, fmt.Errorf("unable to perform request: %v", err)
	}

	if resp.StatusCode != http.StatusCreated {
		if resp.StatusCode == http.StatusUnauthorized {
			return 0, errors.New("unauthorized")
		}
		return 0, fmt.Errorf("unhandled http status: %d", resp.StatusCode)
	}

	var createUserResp CreateUserResponse
	if err := json.NewDecoder(resp.Body).Decode(&createUserResp); err != nil {
		return 0, fmt.Errorf("unable to decode json resp: %v", err)
	}
	return createUserResp.UserID, nil
}

This is straightforward:

  • create the json body
  • create the request (don't forget some useful headers, don't forget the context propagation using NewRequestWithContext)
  • perform the request on the provided http client
  • handle errors (and treat differently authentication related error to return a sentinel error)
  • handle success by parsing the json body
  • return the parsed user id

but as much as this is regular go code, this is a lot of code ; and it is not perfect (security issues related to not handling huge body by restricting readers for instance).

This code requires a lot of test cases to check each possible outcomes.

Here is the same code using the request builder and response handler:

func performCreateUserRequest(ctx context.Context, client *http.Client, userEmail string) (uint64, error) {
	var resp CreateUserResponse

	if err := httpclient.NewRequest(http.MethodPost, "http://example.com/users/").
		Client(client).
		SendJSON(&CreateUserRequest{Email: userEmail}).
		Do(ctx).
		ReceiveJSON(http.StatusCreated, &resp).
		ErrorOnStatus(http.StatusUnauthorized, errors.New("unauthorized")).
		Error(); err != nil {
		return 0, err
	}

	return resp.UserID, nil
}

To go even further, if we write multiple method for the same API, we can create an API object like:

api := httpclient.
	NewAPI(client, url.URL{
		Scheme: "http",
		Host:   "example.com",}).
	WithResponseHandler(http.StatusUnauthorized, func(rw *http.Response) error {
		return errors.New("unauthorized")
	})

to use it like this:

func performCreateUserRequest(ctx context.Context, api *httpclient.API, userEmail string) (uint64, error) {
	var resp CreateUserResponse

	if err := api.
		Do(ctx, api.Post("/users/").SendJSON(&CreateUserRequest{Email: userEmail})).
		ReceiveJSON(http.StatusCreated, &resp).
		Error(); err != nil {
		return 0, err
	}

	return resp.UserID, nil
}

which reduce duplication of error handling by setting common handler for common response handler, or by setting common request attributes.

More examples on the API object and on how to ease tests can be browsed in ./internal/example package.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParsePostForm

func ParsePostForm(req *http.Request) error

ParsePostForm sets req.PostForm by calling req.ParseForm forms, but also handles non-standard http methods. Like req.ParseForm, ParsePostForm is idempotent.

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API stores attributes common to multiple requests definition / responses handing. It helps to build requests and responses with less duplication.

func NewAPI

func NewAPI(client Doer, serverAddress url.URL) *API

NewAPI creates an API object that will use the provided client to perform all requests with. The provided address will be the base url used for each request.

func (*API) Clone added in v0.2.0

func (api *API) Clone() *API

Clone returns a deep clone of the original API.

func (*API) Delete

func (api *API) Delete(endpoint string) *RequestBuilder

Delete creates a DELETE request builder.

func (*API) Do

func (api *API) Do(ctx context.Context, req *RequestBuilder) *ResponseBuilder

Do performs the requests and returns a response builder. It differs from NewRequest().Do() by adding defaults to the request / response.

func (*API) Execute

func (api *API) Execute(ctx context.Context, req *RequestBuilder) error

Execute calls the server, handle the response with default response handlers, and returns the associated error. To customize the response handling, use api.Do().

func (*API) Get

func (api *API) Get(endpoint string) *RequestBuilder

Get creates a GET request builder.

func (*API) Head

func (api *API) Head(endpoint string) *RequestBuilder

Head creates a HEAD request builder.

func (*API) Patch

func (api *API) Patch(endpoint string) *RequestBuilder

Patch creates a PATCH request builder.

func (*API) Post

func (api *API) Post(endpoint string) *RequestBuilder

Post creates a POST request builder.

func (*API) Put

func (api *API) Put(endpoint string) *RequestBuilder

Put creates a PUT request builder.

func (*API) URL

func (api *API) URL(endpoint string) *url.URL

URL returns the absolute URL to query the server.

func (*API) WithRequestHeaders

func (api *API) WithRequestHeaders(headers http.Header) *API

WithRequestHeaders sets headers that will be sent to each request.

func (*API) WithRequestOverrideFunc added in v0.2.0

func (api *API) WithRequestOverrideFunc(overrideFunc RequestOverrideFunc) *API

WithRequestOverrideFunc sets a function that allow each requests to be overridden.

func (*API) WithResponseBodySizeReadLimit

func (api *API) WithResponseBodySizeReadLimit(bodySizeReadLimit int64) *API

WithResponseBodySizeReadLimit sets the maximum sized read for any API response. A value of 64ko is set by default. See ResponseBuilder.BodySizeReadLimit for more details on the provided value.

func (*API) WithResponseHandler

func (api *API) WithResponseHandler(status int, handler ResponseHandler) *API

WithResponseHandler sets a response handler that will be used by default (unless override) for the provided status.

type Doer

type Doer interface {
	Do(req *http.Request) (*http.Response, error)
}

Doer defines the Do method, implemented by default by http.DefaultClient. Its main usage is to avoid being tightly coupled with http.Client object to allow additional behavior on top of the underlying client.

func DoerWrapDumpB64

func DoerWrapDumpB64(doer Doer, dumpFunc func(requestB64, responseB64 string)) Doer

DoerWrapDumpB64 wraps the provided doer by calling a callback with a base64 encoded dump of the request and response.

type RequestBuilder

type RequestBuilder struct {
	// contains filtered or unexported fields
}

RequestBuilder stores the different attributes set by the builder methods.

func NewRequest

func NewRequest(method, endpoint string) *RequestBuilder

NewRequest returns a new request builder.

func (*RequestBuilder) AddHeader

func (b *RequestBuilder) AddHeader(key, value string, values ...string) *RequestBuilder

AddHeader appends the provided value to the provided header.

func (*RequestBuilder) AddHeaders

func (b *RequestBuilder) AddHeaders(header http.Header) *RequestBuilder

AddHeaders appends the provided value to the provided header.

func (*RequestBuilder) AddQueryParam

func (b *RequestBuilder) AddQueryParam(key, value string, values ...string) *RequestBuilder

AddQueryParam sets / appends the provided value to the provided query parameter.

func (*RequestBuilder) AddQueryParams

func (b *RequestBuilder) AddQueryParams(params url.Values) *RequestBuilder

AddQueryParams sets / appends the provided value to the provided query parameters.

func (*RequestBuilder) Client

func (b *RequestBuilder) Client(client Doer) *RequestBuilder

Client overrides the default http client with the provided one.

func (*RequestBuilder) Do

Do builds the request using Request(), executes it and returns a builder to handle the response.

func (*RequestBuilder) PathReplacer

func (b *RequestBuilder) PathReplacer(pattern, replaceWith string) *RequestBuilder

PathReplacer replaces any matching occurrences of the provided pattern inside the url path, with the provided replacement. It is useful to keep the url provided to NewRequest readable and searchable. Example: NewRequest("PUT", "/users/{userID}/email").PathReplacer({"{userID}", userID).

func (*RequestBuilder) Request

func (b *RequestBuilder) Request(ctx context.Context) (*http.Request, error)

Request builds the request.

func (*RequestBuilder) Send

func (b *RequestBuilder) Send(body io.Reader) *RequestBuilder

Send sets the provided body to be used as the request body, with Content-Type octet-stream.

func (*RequestBuilder) SendForm

func (b *RequestBuilder) SendForm(values url.Values) *RequestBuilder

SendForm sets the provided values as url-encoded form values to the request body, with Content-Type header.

func (*RequestBuilder) SendJSON

func (b *RequestBuilder) SendJSON(obj any) *RequestBuilder

SendJSON sets the provided object, marshaled in JSON, to the request body, with Content-Type header.

func (*RequestBuilder) SetHeader

func (b *RequestBuilder) SetHeader(key, value string, values ...string) *RequestBuilder

SetHeader replaces the value of the request header with the provided value.

func (*RequestBuilder) SetHeaders

func (b *RequestBuilder) SetHeaders(header http.Header) *RequestBuilder

SetHeaders replaces the value of the request with the provided value. It does not replace all the request header with provided headers (it is equivalent of calling SetHeader for each provided headers).

func (*RequestBuilder) SetOverrideFunc added in v0.2.0

func (b *RequestBuilder) SetOverrideFunc(overrideFunc RequestOverrideFunc) *RequestBuilder

SetOverrideFunc sets a function to be called that allow the request to be overridden.

func (*RequestBuilder) SetQueryParam

func (b *RequestBuilder) SetQueryParam(key, value string, values ...string) *RequestBuilder

SetQueryParam replaces the provided value to the provided query parameter.

func (*RequestBuilder) SetQueryParams

func (b *RequestBuilder) SetQueryParams(params url.Values) *RequestBuilder

SetQueryParams replaces the provided value to the provided query parameters. It does not replace all the request query parameters with provided query parameters (it is equivalent of calling SetQueryParam for each provided query parameter).

type RequestOverrideFunc added in v0.2.0

type RequestOverrideFunc func(req *http.Request) (*http.Request, error)

RequestOverrideFunc defines the signature to override a request.

type ResponseBuilder

type ResponseBuilder struct {
	// contains filtered or unexported fields
}

ResponseBuilder stores the different attributes set by the builder methods.

func (*ResponseBuilder) BodySizeReadLimit

func (b *ResponseBuilder) BodySizeReadLimit(bodySizeReadLimit int64) *ResponseBuilder

BodySizeReadLimit limits the maximum amount of octets to be read in the response. Server responses will be considered invalid if the read limit is less than the response content-length. Zero value is equivalent of setting bodySizeReadLimit = resp.ContentLength. Negative value disables checks and limitations.

func (*ResponseBuilder) Error

func (b *ResponseBuilder) Error() error

Error applies all the configured attributes on the request's response.

func (*ResponseBuilder) ErrorOnStatus

func (b *ResponseBuilder) ErrorOnStatus(status int, err error) *ResponseBuilder

ErrorOnStatus sets the provided err to be returned if the response http status is the provided status.

func (*ResponseBuilder) OnStatus

func (b *ResponseBuilder) OnStatus(status int, handler ResponseHandler) *ResponseBuilder

OnStatus sets the provided handler to be called if the response http status is the provided status.

func (*ResponseBuilder) OnStatuses

func (b *ResponseBuilder) OnStatuses(statuses []int, handler ResponseHandler) *ResponseBuilder

OnStatuses sets the provided handler to be called if the response http status is any of the provided statuses.

func (*ResponseBuilder) ReceiveJSON

func (b *ResponseBuilder) ReceiveJSON(status int, dest any) *ResponseBuilder

ReceiveJSON parses the response body as JSON (without caring about ContentType header), and sets the result in the provided destination.

func (*ResponseBuilder) SuccessOnStatus

func (b *ResponseBuilder) SuccessOnStatus(statuses ...int) *ResponseBuilder

SuccessOnStatus sets the provided statuses handler to return no errors if the response http status is the provided statuses.

type ResponseHandler

type ResponseHandler func(*http.Response) error

ResponseHandler defines the signature of the function called to handle a response.

type ResponseStatusHandlers

type ResponseStatusHandlers map[int]ResponseHandler

ResponseStatusHandlers maps response http status to response handler.

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL