httpclient

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2025 License: MIT Imports: 12 Imported by: 1

README

License go.mod Go version GoDoc Latest tag Go Report

httpclient

A minimalist, fluent HTTP client Go library that simplifies request creation and response handling through builder patterns.

Overview

This package provides a chainable API that reduces boilerplate code while maintaining type safety. It makes HTTP client code more readable and testable by offering:

  • Fluent Interface: Chain method calls for readable request building.
  • Type Safety: Compile-time checks for request/response handling.
  • Testing Support: Built-in utilities for mocking and testing HTTP interactions.
  • Flexibility: Works with any HTTP client implementing the Doer interface.

Installation

go get github.com/krostar/httpclient

Quick Start

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/krostar/httpclient"
)

func main() {
    var user User
    err := httpclient.NewRequest("GET", "https://api.example.com/users/123").
        Do(context.Background()).
        ReceiveJSON(200, &user).
        Error()

    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("User: %+v\n", user)
}

Core Concepts

  • RequestBuilder: provides a fluent interface for constructing HTTP requests.
  • ResponseBuilder: handles HTTP responses with status-specific logic.
  • API: provides request defaults and reusable configuration.

Examples

Simple GET Request

Fetch a user by ID:

var user User
err := httpclient.NewRequest("GET", "https://api.example.com/users/123").
    Do(ctx).
    ReceiveJSON(200, &user).
    Error()
POST with JSON

Create a new user:

newUser := CreateUserRequest{Name: "John", Email: "[email protected]"}
var response CreateUserResponse

err := httpclient.NewRequest("POST", "https://api.example.com/users").
    SendJSON(&newUser).
    Do(ctx).
    ReceiveJSON(201, &response).
    Error()
Complex Request with Error Handling

A more comprehensive example showing headers, query parameters, and error handling:

var users []User
err := httpclient.NewRequest("GET", "https://api.example.com/users").
    SetHeader("Authorization", "Bearer "+token).
    SetHeader("User-Agent", "MyApp/1.0").
    SetQueryParam("page", "1").
    SetQueryParam("limit", "10").
    Do(ctx).
    ReceiveJSON(200, &users).
    SuccessOnStatus(304). // 304 Not Modified is also success
    ErrorOnStatus(401, ErrUnauthorized).
    ErrorOnStatus(403, ErrForbidden).
    ErrorOnStatus(429, ErrRateLimited).
    Error()
Using the API Type

For applications making multiple requests to the same service, use the API type to reduce duplication:

// Create reusable API client
api := httpclient.NewAPI(http.DefaultClient, url.URL{
    Scheme: "https",
    Host:   "api.example.com",
}).
    WithRequestHeaders(http.Header{
        "Authorization": []string{"Bearer " + token},
        "User-Agent":    []string{"MyApp/1.0"},
    }).
    WithResponseHandler(401, func(resp *http.Response) error {
        return ErrUnauthorized
    }).
    WithResponseBodySizeReadLimit(1024 * 1024) // 1MB limit

// Use the API for multiple requests
var user User
err := api.Do(ctx, api.Get("/users/123")).
    ReceiveJSON(200, &user).
    Error()

var users []User
err = api.Do(ctx, api.Get("/users").
    SetQueryParam("page", "1")).
    ReceiveJSON(200, &users).
    Error()

Testing

The httpclienttest package provides utilities for testing HTTP client code (DoerStub, DoerSpy, request matching, ...).

Comparison

The classic way using standard library

A typical go code would look like this:

func performUserCreationRequest(ctx context.Context, userEmail string) (uint64, error) {
  // serialization of the request content
  body, err := json.Marshal(&UserCreationRequest{Email: userEmail})
  if err != nil {
    return 0, fmt.Errorf("unable to serialize in json: %v", err)
  }

  // create the request using the provided context to respect cancellation or deadlines
  req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://example.com/users", bytes.NewReader(body))
  if err != nil {
      return 0, fmt.Errorf("unable to create the request: %v", err)
  }
  req.Header.Set("Content-Type", "application/json")

  // the client used is hardcoded to be http.Default client but in real-life scenario it is probably injected somehow to ease tests
  client := http.DefaultClient
  // perform the request
  resp, err := client.Do(req)
  if err != nil {
    return 0, fmt.Errorf("unable to perform request: %v", err)
  }

  switch resp.StatusCode {
  case http.StatusCreated:
    // handled below
  case http.StatusUnauthorized:
    return 0, ErrUnauthorizedRequest
  default:
    return 0, fmt.Errorf("unhandled http status: %d", resp.StatusCode)
  }

  // deserialize the response
  var userCreationResponse UserCreationResponse
  if err := json.NewDecoder(resp.Body).Decode(&userCreationResponse); err != nil {
    return 0, fmt.Errorf("unable to deserialize json: %v", err)
  }

  // return the newly generated user id
  return userCreationResponse.UserID, nil
}

This approach is straightforward but verbose:

  • create the JSON body
  • create the request with proper headers and context propagation
  • perform the request using the HTTP client
  • jandle errors with specific logic for authentication failures
  • parse the JSON response body on success
  • return the parsed user ID

However, this approach has several drawbacks: extensive boilerplate code, complex testing requirements, manual verification of context usage and headers, and potential security issues like unrestricted response body reading.

Developer-friendly alternative

The same functionality using httpclient:

func performUserCreationRequest(ctx context.Context, userEmail string) (uint64, error) {
  var resp CreateUserResponse

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

  return resp.UserID, nil
}

This approach is significantly more concise, readable, and maintainable.

For applications making multiple requests to the same API, create a reusable API object:

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

This object supports extensive configuration options to define default behavior for all requests:

Usage example:

func (api myAPIMethods) performUserCreationRequest(ctx context.Context, 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
}

This reduces duplication by centralizing error handling and request attributes, eliminates repeated API addresses, and simplifies testing.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package httpclient provides a fluent, builder-pattern HTTP client.

Simplifies HTTP request creation and response handling with a chainable API. Reduces boilerplate code while maintaining flexibility and type safety. Makes HTTP client code more readable, testable, and maintainable.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParsePostForm

func ParsePostForm(req *http.Request) error

ParsePostForm parses request body as form data and populates req.PostForm. Extends standard library to handle non-standard HTTP methods.

Standard library only parses body for POST, PUT, PATCH. Other methods (like DELETE) ignore body even with form data. This function fills that gap.

Behavior:

  • POST, PUT, PATCH: Delegates to standard req.ParseForm()
  • Other methods: Manually parses request body as form data
  • Idempotent (safe to call multiple times)
  • Does nothing if req.PostForm already populated

Populates req.PostForm with parsed values.

Types

type API

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

API stores configuration and defaults for HTTP requests to a specific service. It provides common attributes (headers, response handlers) applied to all requests.

API instances are safe for concurrent use as they create new builders for each request.

func NewAPI

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

NewAPI creates an API instance with the provided client and server address.

The client must implement the Doer interface (http.Client does). Endpoint paths are appended to the server address.

func (*API) Clone added in v0.2.0

func (api *API) Clone() *API

Clone creates a deep copy with independent configuration.

Headers, response handlers, and settings are copied. The HTTP client is shared.

func (*API) Delete

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

Delete creates a DELETE request builder with default headers and settings.

func (*API) Do

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

Do executes the request and returns a response builder with API defaults.

Applies default response handlers and body size limits. Use for custom response handling beyond Execute().

func (*API) Execute

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

Execute performs the request using API defaults and returns any error.

Convenience method for simple success/failure handling. Use Do() for complex response processing.

func (*API) Get

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

Get creates a GET request builder with default headers and settings.

func (*API) Head

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

Head creates a HEAD request builder with default headers and settings.

func (*API) Patch

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

Patch creates a PATCH request builder with default headers and settings.

func (*API) Post

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

Post creates a POST request builder with default headers and settings.

func (*API) Put

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

Put creates a PUT request builder with default headers and settings.

func (*API) URL

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

URL constructs absolute URL by combining endpoint with server address.

func (*API) WithRequestHeaders

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

WithRequestHeaders adds headers to all requests. Request-specific headers take precedence over API-level headers.

Provided headers are merged into existing headers, not replaced. Common uses: authentication, user agents, ...

func (*API) WithRequestOverrideFunc added in v0.2.0

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

WithRequestOverrideFunc sets a function called for every request to modify the final http.Request before execution.

Useful for authentication, signing, or consistent request modifications.

func (*API) WithResponseBodySizeReadLimit

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

WithResponseBodySizeReadLimit sets maximum bytes to read from response bodies. Security feature preventing memory exhaustion attacks.

Behavior:

  • Larger Content-Length causes failure
  • Unknown Content-Length stops at limit
  • 0 uses Content-Length as limit
  • Negative disables limit (use with caution)

func (*API) WithResponseHandler

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

WithResponseHandler sets a default handler for the specified status code. Called automatically unless overridden by request-specific handlers.

Useful for consistent error handling (e.g., 401 as authentication error).

type Doer

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

Doer defines interface for executing HTTP requests. Allows httpclient to work with any HTTP client implementation. Standard http.Client implements this interface directly.

func DoerWrapDumpB64

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

DoerWrapDumpB64 wraps Doer with request/response dumping capability. Captures complete HTTP traffic (headers and bodies) as base64 strings.

Useful for debugging, logging, or testing HTTP traffic inspection. Dumps include all headers and body content.

If dumpFunc is nil, no dumping occurs but wrapper still applied. Uses httputil.DumpRequestOut and httputil.DumpResponse.

type RequestBuilder

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

RequestBuilder provides a fluent interface for building HTTP requests. Configure method, URL, headers, body through method chaining.

Not thread-safe. Each instance builds and executes a single request.

func NewRequest

func NewRequest(method, endpoint string) *RequestBuilder

NewRequest creates a RequestBuilder for the HTTP method and endpoint URL.

Method should be valid (GET, POST, PUT, DELETE, etc.). Endpoint should be complete URL (containing scheme, host, endpoint, ...).

Parse errors are captured and returned when Request() or Do() is called.

func (*RequestBuilder) AddHeader

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

AddHeader appends values to header, preserving existing ones. Header names are canonicalized. Creates header if missing, appends if exists.

func (*RequestBuilder) AddHeaders

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

AddHeaders appends all provided header values to existing ones. Use SetHeaders() to replace entirely.

func (*RequestBuilder) AddQueryParam

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

AddQueryParam appends values to query parameter, preserving existing ones. Creates parameter if missing, appends if exists.

func (*RequestBuilder) AddQueryParams

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

AddQueryParams appends all provided parameter values to existing ones. Use SetQueryParams() to replace entirely.

func (*RequestBuilder) Client

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

Client sets the HTTP client for request execution. Must implement Doer interface (http.Client does). Defaults to http.DefaultClient. Allows custom clients with timeouts, transports.

func (*RequestBuilder) Do

Do builds, executes request and returns ResponseBuilder.

func (*RequestBuilder) PathReplacer

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

PathReplacer replaces pattern occurrences in URL path with replacement. Keeps URLs 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 and returns the http.Request.

func (*RequestBuilder) Send

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

Send sets io.Reader as request body with Content-Type: application/octet-stream.

Useful for binary data, file uploads, or custom content.

func (*RequestBuilder) SendForm

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

SendForm sets request body to form values as application/x-www-form-urlencoded. Sets appropriate Content-Type header.

Used for HTML forms and form-encoded API endpoints.

func (*RequestBuilder) SendJSON

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

SendJSON sets object as JSON request body and Content-Type header.

Marshaling is lazy - happens during execution, not when called. Object must be JSON-serializable. Marshal errors are returned during execution.

func (*RequestBuilder) SetHeader

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

SetHeader sets HTTP header values, replacing existing ones. Header names are canonicalized.

func (*RequestBuilder) SetHeaders

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

SetHeaders merges multiple headers, replacing existing values for same keys. Other headers remain unchanged.

Equivalent to calling SetHeader for each provided header.

func (*RequestBuilder) SetOverrideFunc added in v0.2.0

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

SetOverrideFunc sets function called before request execution. Receives built request, returns modified request and error. Useful for authentication, signing, dynamic modifications.

func (*RequestBuilder) SetQueryParam

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

SetQueryParam sets query parameter values, replacing existing ones. Multiple values can be provided for the same parameter.

func (*RequestBuilder) SetQueryParams

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

SetQueryParams merges query parameters, replacing existing ones. Does not append; replaces entirely.

type RequestOverrideFunc added in v0.2.0

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

RequestOverrideFunc modifies an http.Request just before execution. Useful for authentication, signatures, or dynamic headers.

Returns modified request and error. Errors cause request failure.

type ResponseBuilder

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

ResponseBuilder provides fluent interface for handling HTTP responses. Define status code processing, parse bodies, apply security constraints.

Not thread-safe. Each instance handles a single response.

func (*ResponseBuilder) BodySizeReadLimit

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

BodySizeReadLimit sets maximum bytes to read from response body. Security feature preventing memory exhaustion attacks.

Behavior:

  • Positive: Max bytes. Larger Content-Length fails immediately.
  • Zero: Uses Content-Length as limit.
  • Negative: Disables limit (use with caution).

Without Content-Length header, reading stops at limit.

func (*ResponseBuilder) Error

func (b *ResponseBuilder) Error() error

Error processes response with configured handlers and returns any error.

Call last in chain to finalize processing:

  1. Apply body size limits
  2. Call status handler
  3. Return error if no handler configured

Unhandled status codes return error with request details and base64 body.

func (*ResponseBuilder) ErrorOnStatus

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

ErrorOnStatus sets error for specific status code. Useful for mapping status codes to domain-specific sentinel errors.

func (*ResponseBuilder) OnStatus

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

OnStatus sets custom handler for specific HTTP status code. Handler called when response matches status.

func (*ResponseBuilder) OnStatuses

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

OnStatuses sets single handler for multiple status codes. Convenience method for shared handling logic.

func (*ResponseBuilder) ReceiveJSON

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

ReceiveJSON parses response body as JSON for specified status code. Stores result in provided destination.

Does not validate Content-Type header. Destination must be pointer.

func (*ResponseBuilder) SuccessOnStatus

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

SuccessOnStatus marks status codes as successful (no error). Convenience method for success codes without special processing.

Equivalent to OnStatus with handler returning nil.

type ResponseHandler

type ResponseHandler func(*http.Response) error

ResponseHandler handles HTTP responses for specific status codes.

type ResponseStatusHandlers

type ResponseStatusHandlers map[int]ResponseHandler

ResponseStatusHandlers maps status codes to response handlers.

Directories

Path Synopsis
internal
Package httpclienttest provides testing utilities for HTTP client code.
Package httpclienttest provides testing utilities for HTTP client code.

Jump to

Keyboard shortcuts

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