Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HC (Http Client)

test Go Reference License: MIT

Simple HTTP Client for Go with interceptor support.

Table of Contents

Installation

go get github.com/morkid/hc

Features

  • Interceptor pattern for request/response customization
  • Request mocking via TakeOver
  • Automatic BaseURL resolution (supports relative paths)
  • Configurable logging (request, response body, headers)
  • Single-line JSON log mode
  • Custom logger support
  • Configurable timeout
  • Configurable TLS certificate verification
  • Configurable HTTP transport parameters
  • Automatic retry with configurable delay and condition
  • Request body log suppression
  • JSON response helper
  • Resty integration

Usage

Basic with Interceptor

import "github.com/morkid/hc"

client := hc.New(hc.Config{
    BaseURL: "http://example.com",
    LogEnabled: true,
    Interceptor: func(req *http.Request) error {
        req.Header.Add("Accept", "application/json")

        // forward to the original request
        return nil
    },
})

req, _ := http.NewRequest("GET", "/hello-world.json", nil)
res, err := client.Do(req)
log.Println(err)
log.Println(res.StatusCode)

BaseURL

Set a BaseURL and use relative paths in your requests. HC will resolve the full URL automatically.

client := hc.New(hc.Config{
    BaseURL: "https://dummyjson.com:443/",
})

req, _ := http.NewRequest("GET", "/products", nil)
res, _ := client.Do(req)

Mocking Response (TakeOver)

The interceptor has two modes:

  1. Return an error: the request is rejected with that error.
  2. Return an *hc.Interceptor with empty ErrorMessage and a TakeOver function: the request is intercepted and the TakeOver function provides a mock response.
helloWorld := hc.Interceptor{
    TakeOver: func(req *http.Request) (res *http.Response, err error) {
        return &http.Response{
            Body:       io.NopCloser(strings.NewReader(`{"message":"hello world"}`)),
            Status:     "200 OK",
            StatusCode: 200,
            Proto:      "HTTP/1.1",
        }, nil
    },
}

client := hc.New(hc.Config{
    Interceptor: func(req *http.Request) error {
        if req.URL.Path == "/hello-world.json" {
            return &helloWorld
        }
        return nil
    },
})

req, _ := http.NewRequest("GET", "/hello-world.json", nil)
res, _ := client.Do(req)

Retry

Retry on failure (error or status >= 500) with a configurable delay.

client := hc.New(hc.Config{
    MaxRetries: 3,
    RetryDelay: time.Second,
})

You can also provide a custom retry condition to control when retries happen:

client := hc.New(hc.Config{
    MaxRetries:     3,
    RetryDelay:     500 * time.Millisecond,
    RetryCondition: func(res *http.Response, err error) bool {
        return err != nil || res.StatusCode >= 500
    },
})

Default is 0 (no retry).

JSON Response Helper

Use hc.JSONResponse to unmarshal the response body directly into a struct.

var result map[string]any
err := hc.JSONResponse(res, &result)

JSON Log Mode

When LogSingleJSONEnabled is true, HC outputs a single JSON line per request instead of individual log lines. All other log lines are suppressed.

client := hc.New(hc.Config{
    LogEnabled:             true,
    LogSingleJSONEnabled:   true,
    LogResponseBodyEnabled: true,
    LogHeaderEnabled:       true,
})

Output:

{"method":"POST","url":"https://example.com/api/login","status_code":200,"duration_ms":320,"attempts":1,"request_body":"...","response_body":"...","request_headers":{...},"response_headers":{...},"error_message":"","raw_error":null}

The JSONLog struct is exported and implements the error interface. You can type-assert errors from Interceptor or RetryCondition to *JSONLog for programmatic access to request metadata.

Request Body Log Suppression

Set LogRequestBodyDisabled to true to exclude the request body from log output while keeping response body logging enabled.

client := hc.New(hc.Config{
    LogEnabled:             true,
    LogResponseBodyEnabled: true,
    LogRequestBodyDisabled: true,  // request body won't appear in logs
})

Configuration

Field Type Description
LogEnabled bool Enable request/response logging
LogResponseBodyEnabled bool Enable response body logging
LogHeaderEnabled bool Enable request header logging
LogHTTPPrefixDisabled bool Disable[HTTP] << / [HTTP] >> prefix
LogSingleJSONEnabled bool Enable single-line JSON log output
LogRequestBodyDisabled bool Disable request body from log output
LogPrefix string Prefix for log messages
Logger *log.Logger Custom logger instance
Interceptor func(*http.Request) error Intercept and customize requests
Timeout int Timeout in seconds (default: 30)
BaseURL string Base URL for relative path resolution
InsecureSkipVerify bool Skip TLS certificate verification
ForceAttemptHTTP2Disabled bool Disable HTTP/2 (default: false)
MaxIdleConns int Max idle connections (default: 100)
IdleConnTimeoutSecond int Idle connection timeout seconds (default: 90)
TLSHandshakeTimeoutSecond int TLS handshake timeout seconds (default: 10)
ExpectContinueTimeoutSecond int Expect continue timeout seconds (default: 1)
MaxRetries int Maximum retry attempts (default: 0 = no retry)
RetryDelay time.Duration Delay between retries
RetryCondition func(*http.Response, error) bool Custom retry condition (default: retry on error or status >= 500)

Resty Integration

HC's transport can be dropped into Resty, so you get HC's interceptor, logging, and base URL features through Resty's fluent API.

client := hc.New()

restyClient := resty.New()
restyClient.SetTransport(client.Transport)

res, err := restyClient.R().
    SetHeader("Accept", "application/json").
    Get("http://example.com/hello-world.json")

About

Golang HTTP Client with Interceptor

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages