httputil

httputil provides request parsing, response writing with RFC 9457 problem documents, a status-capturing ResponseWriter wrapper, and URL link composition for net/http.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/httputil"

Request and response primitives for net/http handlers: header decorators, query and path extraction with defaults, buffered JSON/XML/text responses, RFC 9457 problem documents, and a ResponseWriter wrapper that captures status and size.

No framework, no router of its own, no interface your handlers must implement.

Reading a request

Header decorators for outbound requests:

httputil.AddJSONHeaders(r)
httputil.AddBearerToken(token, r)
httputil.AddBasicAuth(apiKey, apiSecret, r)
httputil.AddAuthorizationHeader(auth, r)

Extraction with defaults for inbound ones:

page := httputil.QueryIntOrDefault(q, "page", 1)
sort := httputil.QueryStringOrDefault(q, "sort", "created_at")
ct   := httputil.HeaderOrDefault(r, "Content-Type", "application/json")
id   := httputil.PathParam(r, "id")

The OrDefault shape collapses the three-line parse-check-fallback that otherwise appears around every optional query parameter. QueryIntOrDefault returns the default for a value that is absent and for one that fails to parse, so a ?page=abc yields the default rather than an error, which is the behaviour you want for pagination and the behaviour you do not want for anything security-relevant.

Writing a response

Text, JSON and XML responses, each setting no-cache and nosniff headers. One encoding detail matters: the body is fully buffered before anything is written, and a marshal failure produces a 500 instead of a truncated body.

That matters because the naive form writes the status code, then streams the encoder straight to the ResponseWriter. If marshalling fails halfway through a nested structure, the status line already said 200 and the client receives valid-looking JSON that stops mid-object. Buffering first means the failure is still recoverable when it happens.

RFC 9457 problem documents

Problem, NewProblem and SendProblem produce application/problem+json error documents:

p := httputil.NewProblem(http.StatusNotFound, "https://example.com/errors/not-found",
    "Resource not found", "No account with that identifier")

RFC 9457 gives API errors a standard shape (type, title, status, detail, instance) so a client can handle them generically instead of learning each service’s bespoke error envelope.

The router fallbacks come in the same format, which is the part that usually gets missed: ProblemNotFoundHandlerFunc, ProblemMethodNotAllowedHandlerFunc and ProblemPanicHandlerFunc. Without them a service returns tidy problem documents from its handlers and the router’s plain-text default for a 404, so the contract breaks exactly where a client is least able to cope.

For the JSend envelope instead, see jsendx. SendJSONType covers any other JSON-based media type.

Observing the response

ResponseWriterWrapper captures the status code and the number of bytes written, and can optionally tee the body.

This is what middleware needs and what http.ResponseWriter does not provide: once a handler has written, the status code is gone. Access logging, latency metrics by status class, and response-size histograms all depend on recovering it.

HTTPResp logs each response with code, duration, timestamp, trace ID and payload metadata, at a level chosen by status class: 2xx at debug, 4xx at warn, 5xx at error. Routing by class keeps a normal traffic load out of the log at info level while a genuine fault still appears at error.

Status projection

StatusSuccess, StatusFail and StatusError map HTTP status codes onto the JSend status vocabulary, with Status.UnmarshalJSON for the round trip and ErrInvalidStatus for a value outside the set.

StatusText extends the standard library’s http.StatusText. Where that returns an empty string for a code it does not know, this falls back to the class name: Informational, Successful, Redirection, Client Error, Server Error, and Unknown Status for anything outside 100 to 599.

So a 499 from an nginx front end, or a vendor-specific 4xx, still yields a usable reason phrase instead of an empty one. A log line or an error body built from http.StatusText alone silently loses its reason for exactly those codes.

Link composes a URL from a base and a template with substituted segments, for building Location headers and pagination links without fmt.Sprintf and manual escaping at each call site.

Example

page := httputil.QueryIntOrDefault(r.URL.Query(), "page", 1)

fmt.Println(page)

Full source is in example_httputil_test.go. More runnable examples are on pkg.go.dev.

Dependencies

Reaches github.com/julienschmidt/httprouter for path parameters, plus traceid and encode.