Documentation
¶
Overview ¶
Package httputil provides reusable HTTP request/response primitives for Go services built on top of net/http.
Problem ¶
HTTP handlers frequently repeat the same infrastructure code: setting JSON/auth headers, parsing query defaults, extracting router path params, tracking request timing, writing consistent response payloads, and instrumenting response writers. Duplicating this logic across handlers increases inconsistency and maintenance overhead.
Solution ¶
This package centralizes that boilerplate into focused helpers:
- request helpers: header decorators, path/query/header default extraction, request-time context utilities
- response helpers: text/JSON/XML responses with no-cache and nosniff headers, full-buffer encoding with a 500 fallback on marshal failure, and structured response logging via HTTPResp
- response-writer wrapper: status/size capture and optional tee support for middleware instrumentation (ResponseWriterWrapper)
- URL composition helper for link building (Link)
Highlights ¶
- Header helpers for JSON, Basic Auth, and Bearer tokens.
- Safe query/header parsing with typed defaults for string/int/uint.
- Context-based request start-time propagation and retrieval.
- Uniform response writing with MIME constants and JSend-style status projection and round-trip parsing (StatusSuccess, StatusFail, StatusError, Status.UnmarshalJSON, ErrInvalidStatus).
- Structured response logs containing code, duration, timestamp, trace ID, and payload metadata, logged by class (2xx debug, 4xx warn, 5xx error).
- Middleware-friendly response writer proxy exposing written status and byte size.
Benefits ¶
httputil reduces repetitive handler scaffolding, improves request/response consistency, and makes HTTP middleware stacks easier to observe and maintain.
Index ¶
- Constants
- Variables
- func AddAuthorizationHeader(auth string, r *http.Request)
- func AddBasicAuth(apiKey, apiSecret string, r *http.Request)
- func AddBearerToken(token string, r *http.Request)
- func AddJSONHeaders(r *http.Request)
- func GetRequestTime(r *http.Request) (time.Time, bool)
- func GetRequestTimeFromContext(ctx context.Context) (time.Time, bool)
- func HeaderOrDefault(r *http.Request, key string, defaultValue string) string
- func Link(url, template string, segments ...any) string
- func PathParam(r *http.Request, name string) string
- func QueryIntOrDefault(q url.Values, key string, defaultValue int) int
- func QueryStringOrDefault(q url.Values, key string, defaultValue string) string
- func QueryUintOrDefault(q url.Values, key string, defaultValue uint) uint
- func StringValueOrDefault(v, def string) string
- func WithRequestTime(ctx context.Context, t time.Time) context.Context
- type HTTPResp
- func (hr *HTTPResp) SendJSON(ctx context.Context, w http.ResponseWriter, statusCode int, data any)
- func (hr *HTTPResp) SendStatus(ctx context.Context, w http.ResponseWriter, statusCode int)
- func (hr *HTTPResp) SendText(ctx context.Context, w http.ResponseWriter, statusCode int, data string)
- func (hr *HTTPResp) SendXML(ctx context.Context, w http.ResponseWriter, statusCode int, xmlHeader string, ...)
- type ResponseWriterWrapper
- type Status
Examples ¶
Constants ¶
const ( HeaderAuthorization = "Authorization" HeaderAuthBasic = "Basic " HeaderAuthBearer = "Bearer " HeaderContentType = "Content-Type" HeaderAccept = "Accept" MimeTypeJSON = "application/json" )
Common HTTP headers and MIME types.
const ( // MimeApplicationJSON contains the mime type string for JSON content. MimeApplicationJSON = "application/json; charset=utf-8" // MimeApplicationXML contains the mime type string for XML content. MimeApplicationXML = "application/xml; charset=utf-8" // MimeTextPlain contains the mime type string for text content. MimeTextPlain = "text/plain; charset=utf-8" )
const ( StatusSuccess = "success" StatusFail = "fail" StatusError = "error" )
JSend status codes.
const ReqTimeCtxKey = timeCtxKey("request_time")
ReqTimeCtxKey is the Context key to retrieve the request time.
const XMLHeader = xml.Header
XMLHeader is a default XML Declaration header suitable for use with the SendXML function.
Variables ¶
var ErrInvalidStatus = errors.New("invalid JSend status")
ErrInvalidStatus is returned when unmarshaling a JSend status value that is not one of "success", "fail", or "error".
Functions ¶
func AddAuthorizationHeader ¶
AddAuthorizationHeader sets the Authorization header to the specified value, replacing any previously set value. The Authorization header is a singleton per RFC 9110 §11.6.2, so repeated calls (e.g. refreshing a token before a retry) must not accumulate multiple values.
func AddBasicAuth ¶
AddBasicAuth adds Basic Authorization header with base64-encoded "apiKey:apiSecret" credentials.
Per RFC 7617 the user-id (apiKey) must not contain a colon, as the first colon separates the user-id from the password when the credentials are decoded.
func AddBearerToken ¶
AddBearerToken adds Bearer Authorization header with the provided token.
func AddJSONHeaders ¶
AddJSONHeaders sets application/json Accept and Content-Type headers on the request.
func GetRequestTime ¶
GetRequestTime returns the request time from the http request.
func GetRequestTimeFromContext ¶
GetRequestTimeFromContext retrieves request timestamp from context, returning zero time if not present.
func HeaderOrDefault ¶
HeaderOrDefault returns the HTTP header value, or defaultValue if the header is missing or set to the empty string.
func Link ¶
Link composes URL by substituting template segments using fmt.Sprintf and appending to service URL.
A trailing slash on url and a leading slash on template are trimmed so exactly one separator joins them.
When segments are provided, template is used as a fmt.Sprintf format string. For this reason template MUST be a trusted, compile-time constant format string controlled by the caller. Never pass an externally-derived or user-supplied value as template, as it would be interpreted as a format string (a format-string injection footgun).
Segments are not URL-escaped: callers passing untrusted string segments must escape them (e.g. with url.PathEscape) to avoid injecting path or query delimiters. Note also that a literal "%" in template is emitted verbatim when no segments are given but interpreted as a fmt verb once any segment is passed.
Example ¶
package main
import (
"fmt"
"net/url"
"github.com/tecnickcom/nurago/pkg/httputil"
)
func main() {
// A trusted, compile-time template is used as a fmt.Sprintf format string.
fmt.Println(httputil.Link("https://api.example.invalid", "users/%s", "42"))
// Untrusted string segments must be escaped by the caller to avoid injecting
// path or query delimiters.
fmt.Println(httputil.Link("https://api.example.invalid", "users/%s", url.PathEscape("a/b?c")))
}
Output: https://api.example.invalid/users/42 https://api.example.invalid/users/a%2Fb%3Fc
func PathParam ¶
PathParam returns the value of a named path segment from httprouter params in request context.
httprouter prefixes catch-all parameters with a single "/"; only that router-added slash is stripped, so slashes that are part of the client-supplied value are preserved.
func QueryIntOrDefault ¶
QueryIntOrDefault parses query parameter as signed integer or returns defaultValue if missing or invalid.
Example ¶
package main
import (
"fmt"
"net/url"
"github.com/tecnickcom/nurago/pkg/httputil"
)
func main() {
q := url.Values{"limit": []string{"25"}}
fmt.Println(httputil.QueryIntOrDefault(q, "limit", 10))
fmt.Println(httputil.QueryIntOrDefault(q, "missing", 10))
}
Output: 25 10
func QueryStringOrDefault ¶
QueryStringOrDefault returns query parameter value or defaultValue if missing or empty.
func QueryUintOrDefault ¶
QueryUintOrDefault parses query parameter as unsigned integer or returns defaultValue if missing or invalid.
func StringValueOrDefault ¶
StringValueOrDefault returns the string value or a default value.
Types ¶
type HTTPResp ¶
type HTTPResp struct {
// contains filtered or unexported fields
}
HTTPResp holds the configuration for the HTTP response methods.
func NewHTTPResp ¶
NewHTTPResp constructs HTTP response helper with structured logging to provided logger (or slog.Default() if nil).
Example ¶
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"github.com/tecnickcom/nurago/pkg/httputil"
)
func main() {
// The logger is where structured response entries go; the body is written to w.
res := httputil.NewHTTPResp(slog.New(slog.DiscardHandler))
rr := httptest.NewRecorder()
res.SendJSON(context.Background(), rr, http.StatusOK, struct {
Message string `json:"message"`
}{Message: "hello"})
fmt.Print(rr.Body.String())
}
Output: {"message":"hello"}
func (*HTTPResp) SendJSON ¶
SendJSON encodes data as JSON, writes with cache-control headers, and logs response entry.
The payload is marshaled fully before any header or status code is written, so a marshaling failure produces a clean 500 Internal Server Error instead of a partial or empty body sent under the requested (success) status code.
func (*HTTPResp) SendStatus ¶
SendStatus writes HTTP status code with standard text and logs response entry.
func (*HTTPResp) SendText ¶
func (hr *HTTPResp) SendText(ctx context.Context, w http.ResponseWriter, statusCode int, data string)
SendText writes plain text response with cache-control headers and structured logging.
func (*HTTPResp) SendXML ¶
func (hr *HTTPResp) SendXML(ctx context.Context, w http.ResponseWriter, statusCode int, xmlHeader string, data any)
SendXML encodes data as XML with header prefix, cache-control headers, and structured logging.
The document (declaration header plus encoded payload) is buffered fully before any header or status code is written, so an encoding failure produces a clean 500 Internal Server Error instead of a truncated document under a success status code.
type ResponseWriterWrapper ¶
type ResponseWriterWrapper interface {
http.ResponseWriter
// Size returns the total number of bytes sent to the client.
Size() int
// Status returns the HTTP response status code written to the client.
// It returns 0 until the header is written; callers that need the effective
// value should treat 0 as the implicit 200 that net/http sends.
Status() int
// Tee sets a writer that receives a copy of the bytes written to the response
// writer. Only bytes written after this call are copied. A tee-write failure is
// surfaced as the error return of Write, after the client bytes have already
// been sent. Setting a tee also routes ReadFrom through a generic copy,
// disabling any io.ReaderFrom (sendfile) fast path on the underlying writer.
Tee(w io.Writer)
}
ResponseWriterWrapper augments http.ResponseWriter with status/size tracking and tee support.
func NewResponseWriterWrapper ¶
func NewResponseWriterWrapper(w http.ResponseWriter) ResponseWriterWrapper
NewResponseWriterWrapper wraps http.ResponseWriter with status/size capture and optional tee support.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/tecnickcom/nurago/pkg/httputil"
)
func main() {
// Middleware wraps the writer to observe the status and byte size a handler produced.
rr := httptest.NewRecorder()
ww := httputil.NewResponseWriterWrapper(rr)
ww.WriteHeader(http.StatusCreated)
_, _ = ww.Write([]byte("created"))
fmt.Printf("status=%d size=%d\n", ww.Status(), ww.Size())
}
Output: status=201 size=7
type Status ¶
type Status int
Status translates the HTTP status code to a JSend status string.
The value-receiver String/MarshalJSON and pointer-receiver UnmarshalJSON mix is required: UnmarshalJSON must mutate the receiver, while String/MarshalJSON must work on non-addressable values.
func (Status) MarshalJSON ¶
MarshalJSON implements the custom marshaling function for the json encoder, emitting the JSend status string (see Status.String). Because Status also implements fmt.Stringer, slog text handlers render the same string.
func (Status) String ¶
String projects the HTTP status code onto a JSend status string:
- codes below 400 (including 0 and negative values) map to "success"
- codes in the 400-499 range map to "fail"
- codes 500 and above map to "error"
func (*Status) UnmarshalJSON ¶
UnmarshalJSON implements the custom unmarshaling function for the json decoder.
A JSend status string is mapped back to a representative HTTP status code. The mapping is intentionally lossy — the original code is not recoverable from the status string alone and should be read from the accompanying code field (e.g. jsendx.Response.Code):
- "success" maps to 200 (http.StatusOK)
- "fail" maps to 400 (http.StatusBadRequest)
- "error" maps to 500 (http.StatusInternalServerError)
Any other value yields ErrInvalidStatus.