httpx

package module
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 14 Imported by: 0

README

httpx

CI Coverage Status Go Reference Go Report Card Go version

Composable middleware and utilities for net/http — servers and clients alike. Structured logging, OpenTelemetry tracing and metrics, retries, rate limiting, timeouts — all as plain func(http.Handler) http.Handler and func(http.RoundTripper) http.RoundTripper decorators.

Philosophy

  • Plain net/http. Every server middleware is a standard func(http.Handler) http.Handler, so it works with http.ServeMux and any compatible router (chi, gorilla/mux, ...). No framework, no new handler type, no router dependency.
  • The client is not an afterthought. The same concerns — logging, tracing, limits, retries — are available symmetrically for http.Client as http.RoundTripper middleware.
  • Observability first. Logging goes through log/slog, enriched from context via slogx so cross-cutting fields attach to every line; tracing and metrics follow the OpenTelemetry semantic conventions, and log↔trace correlation plugs in as a slogx.WithExtractor (see Logging).
  • Lean dependencies. Runtime dependencies are OpenTelemetry, slogx, and golang.org/x — no compression codecs, no metrics backends, nothing you didn't ask for.
  • Composable, removable. Wrap applies a chain where the first middleware listed is the outermost. Each concern is one decorator — add or remove them one at a time.
  • Upgrade-safe. Logging, metrics, and body-wrapping middleware detect WebSocket upgrade requests and handle them correctly, so one chain serves REST and WebSocket routes alike.

Install

go get github.com/go-artel/httpx

Requires Go 1.25 or newer.

Quick start

Server
package main

import (
	"log/slog"
	"net/http"
	"os"

	"github.com/go-artel/httpx/middleware"
	"github.com/go-artel/httpx/rest"
	"github.com/go-artel/slogx"
)

type EchoReq struct {
	Message string `json:"message"`
}

func main() {
	log := slog.New(slogx.NewHandler(slog.NewJSONHandler(os.Stdout, nil)))

	mux := http.NewServeMux()
	mux.HandleFunc("POST /echo", func(w http.ResponseWriter, r *http.Request) {
		var req EchoReq
		if err := rest.BindJSON(r, &req); err != nil {
			rest.RenderJSONWithStatus(w, http.StatusBadRequest, rest.JSON{"error": err.Error()})
			return
		}

		rest.RenderJSON(w, req)
	})

	handler := middleware.Wrap(mux,
		middleware.RealIP,                      // RemoteAddr from X-Forwarded-For & co.
		middleware.Heartbeat("/healthz"),       // GET /healthz -> 200 OK
		middleware.Tracer(),                    // OpenTelemetry server span per request
		middleware.MeterDefault("echo"),        // OpenTelemetry metrics: in-flight, duration
		middleware.AccessLogger(log),           // one structured log line per request
		middleware.Recoverer(log),              // recover panics -> 500, stack in the log
		middleware.BodySizeLimiter(1<<20, log), // reject request bodies over 1 MiB
	)

	if err := http.ListenAndServe(":8080", handler); err != nil {
		log.Error("server failed", slogx.Error(err))
		os.Exit(1)
	}
}
Client
client := &http.Client{
	Timeout: 30 * time.Second,
	Transport: roundtrip.Wrap(http.DefaultTransport,
		roundtrip.Retry(roundtrip.Retries(3)), // retry with jitter backoff
		roundtrip.RateLimiter(rate.NewLimiter(rate.Limit(50), 50)),
		roundtrip.Tracer(),          // OpenTelemetry client span, propagates trace headers
		roundtrip.Logger(1024, log), // log request/response, bodies up to 1 KiB
	),
}

Packages

Package What it holds
middleware func(http.Handler) http.Handler decorators
roundtrip func(http.RoundTripper) http.RoundTripper decorators
rest Request binding and response rendering (JSON, XML, HTML)
httpx Request/response introspection, dumping, response writer wrapper

middleware — server side

Middleware Purpose
Wrap Applies a middleware chain to a handler; the first listed is the outermost.
AccessLogger One structured log line per request: method, URL, status, duration, user agent.
Logger / RequestLogger / ResponseLogger Logs the full request and/or response — headers and body up to a size limit (-1 = whole body, 0 = none).
Dumper / RequestDumper / ResponseDumper Dumps the raw wire-format request/response into the log.
Recoverer Recovers panics, logs the stack, responds 500.
Tracer OpenTelemetry server span per request (otelhttp-style); propagators, provider, and span options configurable.
SpanName Sets the current span's name and http.route attribute — use per route.
Meter / MeterDefault OpenTelemetry metrics per route: in-flight requests and request duration, for plain and upgraded connections.
BodySizeLimiter Rejects request bodies over a limit with 413.
Throttler Caps concurrent in-flight requests; 503 over the cap.
Timeout Adds a timeout to the request context.
Heartbeat Liveness endpoint for load balancers and uptime checks.
Profiler Mounts net/http/pprof under a prefix.
RealIP Rewrites RemoteAddr from CF-Connecting-IP / True-Client-IP / X-Real-IP / X-Forwarded-For. Only safe behind a trusted proxy; prefer RealIPFromHeaders limited to the header your proxy sets.
NoCache Sets response headers that prevent client and proxy caching.
SetHeader Sets a static response header.
GracefulConnectionUpgrader Graceful shutdown for upgraded (WebSocket) connections: on context cancel, waits for them to finish.
WriteErrorLogger Logs errors that occur while writing the response.

roundtrip — client side

Middleware Purpose
Wrap Applies a middleware chain to a transport; the first listed is the outermost.
Retry Retries with jitter backoff. Defaults: 3 attempts, 100ms wait, 2s max wait, retry on any error — all configurable via options.
Timeout Adds a timeout to the round trip; cancels the request and closes the body on expiry.
RateLimiter Blocks until a golang.org/x/time/rate limiter allows the request.
Throttler Caps concurrent outgoing requests.
Tracer OpenTelemetry client span per request; injects trace context into outgoing headers.
Logger / RequestLogger / ResponseLogger Structured logs of the outgoing request / incoming response with bodies up to a size limit.
Dumper / RequestDumper / ResponseDumper Raw wire-format dumps into the log.
ResponseBodySizeLimiter Fails with ErrResponseBodyTooLarge when a response body exceeds the limit.
RequestGetBodySetter Ensures req.GetBody is set (buffering when needed) so HTTP/2 retries after GOAWAY work.
Hook / HookMerger Before/after callbacks around a round trip without writing a full wrapper.
Func Adapter to use an ordinary function as an http.RoundTripper.

rest — binding and rendering

Binding decodes a request body into a struct and runs optional validation functions. Bind dispatches on the parsed Content-Type media type (application/json, application/xml, text/xml); BindJSON and BindXML decode directly:

validate := validator.New() // e.g. github.com/go-playground/validator

func handle(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderReq
	if err := rest.BindJSON(r, &req, validate.StructCtx); err != nil {
		rest.RenderJSONWithStatus(w, http.StatusBadRequest, rest.JSON{"error": err.Error()})
		return
	}
	// ...
}

Rendering encodes the payload and sets the Content-Type header; the *WithStatus variants also set a status code (the rest rely on net/http's implicit 200): RenderJSON, RenderJSONWithStatus, RenderJSONFromBytes, RenderJSONWithHTML (keeps HTML characters unescaped), and the RenderXML* / RenderHTML* counterparts.

httpx — utilities

The root package holds the building blocks the middleware are made of, usable on their own:

  • ExtractRequestInfo / ExtractResponseInfo — a structured snapshot of a request/response (method, URL, headers, body up to a limit) that implements slog.LogValuer and JSON marshaling; this is what the Logger middleware emits.
  • DumpRequest / DumpResponse — wire-format dumps with a body size cap.
  • NewWrapResponseWriter — an http.ResponseWriter proxy that records status code, bytes written, and the first write error, and can tee the body.
  • ExtractRealIP — the client's real IP from proxy headers.
  • IsUpgradeRequest — detects protocol upgrade requests (RFC 7230).
  • NewMIMEMatcher — matches MIME types against patterns with wildcards (text/*, */json), case-insensitively, ignoring parameters like charset=utf-8.

Logging

Every middleware logs through the plain *slog.Logger you pass in — one mechanism, log.LogAttrs(ctx, ...), nothing else. Each line always carries its own attributes (the request line its request info, the response line its response info), with any logger.

Cross-cutting enrichment is the application's opt-in: install slogx.NewHandler on your logger and the context does the rest — httpx stamps request/request_dump/retry_attempt into the request context via slogx.ContextWithAttrs, so they attach to every line logged under that context: httpx's own lines and your handler's. It works no matter how many decorators wrap your handler — enrichment rides the standard Handle(ctx, record) contract, not type detection. Without a context-aware handler those context attributes simply stay off the lines.

Tracer follows the same rule: it puts the active span in the request context and stamps nothing into the logs — correlation is a slogx.WithExtractor you install once on your logger, which reads the live span at log time (so nested client spans correlate too):

log := slog.New(slogx.NewHandler(jsonHandler, slogx.WithExtractor(traceFields)))

// traceFields lives in your module.
func traceFields(ctx context.Context) []slog.Attr {
	sc := trace.SpanContextFromContext(ctx)
	if !sc.IsValid() {
		return nil
	}

	return []slog.Attr{
		slog.String("trace_id", sc.TraceID().String()),
		slog.String("span_id", sc.SpanID().String()),
	}
}

A POST /echo handled by the quick-start server then logs roughly:

{"level":"DEBUG","msg":"Received request","trace_id":"357570a3...","span_id":"b7ad6b71...","request":{"method":"POST","url":"/echo","header":{"Content-Type":"application/json"},"body":{"message":"hi"}}}
{"level":"DEBUG","msg":"Generated response","trace_id":"357570a3...","span_id":"b7ad6b71...","duration":"23µs","response":{"status_code":200,"body":{"message":"hi"}}}
{"level":"INFO","msg":"Request processed","trace_id":"357570a3...","span_id":"b7ad6b71...","method":"POST","url":"/echo","status_code":200,"duration":"82µs"}

Logger escalates the "Generated response" level with the status code — 4xx logs as WARN, 5xx as ERROR — so failed exchanges surface without extra configuration.

License

MIT

Documentation

Overview

Package httpx provides building blocks for net/http servers and clients: request/response dumping and structured request/response info for logging, client IP extraction, protocol upgrade detection, MIME type matching, and a response writer wrapper that records status code and bytes written.

Subpackages build on it:

  • middleware: func(http.Handler) http.Handler decorators for servers;
  • roundtrip: func(http.RoundTripper) http.RoundTripper decorators for clients;
  • rest: request binding and response rendering helpers (JSON, XML, HTML).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DumpRequest

func DumpRequest(req *http.Request, maxBodySize int) ([]byte, error)

DumpRequest dumps req in its HTTP/1.x wire representation. Headers are always dumped; maxBodySize controls only the body: 0 omits it, -1 dumps it in full, a positive value truncates the dumped body to maxBodySize bytes, and any other negative value behaves like 0. At most maxBodySize+1 bytes of the body are read into memory (all of it for -1) and req.Body is replaced with a reader that still yields the original data.

func DumpResponse

func DumpResponse(resp *http.Response, maxBodySize int) ([]byte, error)

DumpResponse dumps resp in its HTTP/1.x wire representation. Headers are always dumped; maxBodySize controls only the body: 0 omits it, -1 dumps it in full, a positive value truncates the dumped body to maxBodySize bytes, and any other negative value behaves like 0. At most maxBodySize+1 bytes of the body are read into memory (all of it for -1) and resp.Body is replaced with a reader that still yields the original data.

func ExtractRealIP

func ExtractRealIP(req *http.Request, headers ...string) string

ExtractRealIP returns the client IP address extracted from the request headers, in priority order. With no explicit headers the default order is CF-Connecting-IP, True-Client-IP, X-Real-IP, X-Forwarded-For (first entry). A header whose value is not a valid IP is skipped in favor of the next one; an empty string is returned when no header carries a valid IP.

These headers are set by the client unless a trusted proxy overwrites them, so the result is only trustworthy behind such a proxy — and only for the headers that proxy actually manages: with the default list a spoofed higher-priority header (e.g. CF-Connecting-IP when you are not behind Cloudflare) wins over the genuine one. Pass exactly the headers your infrastructure sets.

func IsUpgradeRequest

func IsUpgradeRequest(req *http.Request) bool

IsUpgradeRequest determines if an HTTP request is requesting a protocol upgrade. It checks for presence of both "Connection: Upgrade" and a valid "Upgrade" header according to the HTTP/1.1 specification (RFC 7230, Section 6.7).

Types

type MIMEMatcher

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

MIMEMatcher contains pre-parsed patterns

func NewMIMEMatcher

func NewMIMEMatcher(patterns []string) *MIMEMatcher

NewMIMEMatcher creates a new matcher from an array of patterns

func (*MIMEMatcher) Matches

func (m *MIMEMatcher) Matches(mimeType string) bool

Matches checks if the MIME type matches at least one pattern

type RequestInfo

type RequestInfo struct {
	RemoteAddr string            `json:"remote_addr"`
	Host       string            `json:"host"`
	Proto      string            `json:"proto"`
	Method     string            `json:"method"`
	URL        string            `json:"url"`
	Path       string            `json:"path"`
	Query      map[string]string `json:"query"`
	Header     map[string]string `json:"header"`

	// Body is a human-readable capture of the request body, serialized as a
	// JSON string. Invalid UTF-8 (binary bodies) is replaced with U+FFFD
	// during marshalling, so the round-trip is lossy for binary payloads —
	// readability of the logs is chosen over fidelity here.
	Body []byte `json:"body"`
}

RequestInfo contains metadata about a http.Request including query parameters, headers, and part of the body.

func ExtractRequestInfo

func ExtractRequestInfo(req *http.Request, maxBodySize int) (RequestInfo, error)

ExtractRequestInfo extracts RequestInfo from req. Headers are always included; maxBodySize controls only the body: 0 omits it, -1 captures it in full, a positive value truncates the captured body to maxBodySize bytes, and any other negative value behaves like 0. The body is taken from req.GetBody when available, leaving req.Body untouched; otherwise at most maxBodySize+1 bytes are read from req.Body (all of it for -1) and req.Body is replaced with a reader that still yields the original data.

func (RequestInfo) LogValue

func (ri RequestInfo) LogValue() slog.Value

LogValue implements slog.LogValuer.

func (RequestInfo) MarshalJSON

func (ri RequestInfo) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler; the body is encoded as a string.

func (*RequestInfo) UnmarshalJSON

func (ri *RequestInfo) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler; the body is decoded from a string.

type ResponseInfo

type ResponseInfo struct {
	Proto      string            `json:"proto"`
	StatusCode int               `json:"status_code"`
	Header     map[string]string `json:"header"`

	// Body is a human-readable capture of the response body, serialized as a
	// JSON string. Invalid UTF-8 (binary bodies) is replaced with U+FFFD
	// during marshalling, so the round-trip is lossy for binary payloads —
	// readability of the logs is chosen over fidelity here.
	Body []byte `json:"body"`
}

ResponseInfo contains metadata about a http.Response including headers and part of the body.

func ExtractResponseInfo

func ExtractResponseInfo(resp *http.Response, maxBodySize int) (ResponseInfo, error)

ExtractResponseInfo extracts ResponseInfo from resp. Headers are always included; maxBodySize controls only the body: 0 omits it, -1 captures it in full, a positive value truncates the captured body to maxBodySize bytes, and any other negative value behaves like 0. At most maxBodySize+1 bytes are read from resp.Body (all of it for -1) and resp.Body is replaced with a reader that still yields the original data.

func (ResponseInfo) LogValue

func (ri ResponseInfo) LogValue() slog.Value

LogValue implements slog.LogValuer.

func (ResponseInfo) MarshalJSON

func (ri ResponseInfo) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler; the body is encoded as a string.

func (*ResponseInfo) UnmarshalJSON

func (ri *ResponseInfo) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler; the body is decoded from a string.

type WrapResponseWriter

type WrapResponseWriter interface {
	http.ResponseWriter
	// Status returns the HTTP status of the response. It defaults to
	// http.StatusOK when the handler has not set one explicitly; use Written
	// to check whether the response header has actually been sent.
	Status() int
	// Written reports whether the response header has been written.
	Written() bool
	// BytesWritten returns the total number of bytes sent to the client.
	BytesWritten() int
	// Tee causes the response body to be written to the given io.Writer in
	// addition to proxying the writes through. Only one io.Writer can be
	// tee'd to at once: setting a second one will overwrite the first.
	// Writes will be sent to the proxy before being written to this
	// io.Writer. It is illegal for the tee'd writer to be modified
	// concurrently with writes.
	Tee(io.Writer)
	// Unwrap returns the original proxied target.
	Unwrap() http.ResponseWriter
	// Error returns the first error recorded while writing the body
	// (via Write or ReadFrom).
	Error() error
}

WrapResponseWriter is a proxy around a http.ResponseWriter that allows you to hook into various parts of the response process. It can't catch a hijack writer, because it is not possible to wrap it.

func NewWrapResponseWriter

func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter

NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to hook into various parts of the response process.

Directories

Path Synopsis
internal
semconv
Package semconv maps HTTP requests and responses onto OpenTelemetry attributes and span statuses, following the stable HTTP semantic conventions (semconv v1.41.0).
Package semconv maps HTTP requests and responses onto OpenTelemetry attributes and span statuses, following the stable HTTP semantic conventions (semconv v1.41.0).
Package middleware provides composable net/http middleware for common server concerns: structured request/response logging, OpenTelemetry tracing and metrics, panic recovery, body size limits, throttling, timeouts, and more.
Package middleware provides composable net/http middleware for common server concerns: structured request/response logging, OpenTelemetry tracing and metrics, panic recovery, body size limits, throttling, timeouts, and more.
Package rest provides helpers for HTTP request binding and response rendering: decoding JSON or XML request bodies with optional validation, and rendering JSON, XML, or HTML responses with the correct headers and status codes.
Package rest provides helpers for HTTP request binding and response rendering: decoding JSON or XML request bodies with optional validation, and rendering JSON, XML, or HTML responses with the correct headers and status codes.
Package roundtrip provides composable http.RoundTripper middleware for common HTTP client concerns: retries with backoff, structured request/response logging, OpenTelemetry tracing, rate limiting, throttling, timeouts, and response body size limits.
Package roundtrip provides composable http.RoundTripper middleware for common HTTP client concerns: retries with backoff, structured request/response logging, OpenTelemetry tracing, rate limiting, throttling, timeouts, and response body size limits.

Jump to

Keyboard shortcuts

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