Simple HTTP Client for Go with interceptor support.
go get github.com/morkid/hc- Interceptor pattern for request/response customization
- Request mocking via
TakeOver - Automatic
BaseURLresolution (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
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)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)The interceptor has two modes:
- Return an error: the request is rejected with that error.
- Return an
*hc.Interceptorwith emptyErrorMessageand aTakeOverfunction: the request is intercepted and theTakeOverfunction 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 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).
Use hc.JSONResponse to unmarshal the response body directly into a struct.
var result map[string]any
err := hc.JSONResponse(res, &result)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.
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
})| 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) |
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")