Categorygithub.com/krostar/httpclient
repositorypackage
0.2.0
Repository: https://github.com/krostar/httpclient.git
Documentation: pkg.go.dev

# Packages

No description provided by the author

# 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.