httputil

package
v1.149.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 19 Imported by: 0

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

Examples

Constants

View Source
const (
	HeaderAuthorization = "Authorization"
	HeaderAuthBasic     = "Basic "
	HeaderAuthBearer    = "Bearer "
	HeaderContentType   = "Content-Type"
	HeaderAccept        = "Accept"
	MimeTypeJSON        = "application/json"
)

Common HTTP headers and MIME types.

View Source
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"
)
View Source
const (
	StatusSuccess = "success"
	StatusFail    = "fail"
	StatusError   = "error"
)

JSend status codes.

View Source
const ReqTimeCtxKey = timeCtxKey("request_time")

ReqTimeCtxKey is the Context key to retrieve the request time.

View Source
const XMLHeader = xml.Header

XMLHeader is a default XML Declaration header suitable for use with the SendXML function.

Variables

View Source
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

func AddAuthorizationHeader(auth string, r *http.Request)

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

func AddBasicAuth(apiKey, apiSecret string, r *http.Request)

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

func AddBearerToken(token string, r *http.Request)

AddBearerToken adds Bearer Authorization header with the provided token.

func AddJSONHeaders

func AddJSONHeaders(r *http.Request)

AddJSONHeaders sets application/json Accept and Content-Type headers on the request.

func GetRequestTime

func GetRequestTime(r *http.Request) (time.Time, bool)

GetRequestTime returns the request time from the http request.

func GetRequestTimeFromContext

func GetRequestTimeFromContext(ctx context.Context) (time.Time, bool)

GetRequestTimeFromContext retrieves request timestamp from context, returning zero time if not present.

func HeaderOrDefault

func HeaderOrDefault(r *http.Request, key string, defaultValue string) string

HeaderOrDefault returns the HTTP header value, or defaultValue if the header is missing or set to the empty string.

func Link(url, template string, segments ...any) string

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.

func PathParam

func PathParam(r *http.Request, name string) string

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

func QueryIntOrDefault(q url.Values, key string, defaultValue int) int

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

func QueryStringOrDefault(q url.Values, key string, defaultValue string) string

QueryStringOrDefault returns query parameter value or defaultValue if missing or empty.

func QueryUintOrDefault

func QueryUintOrDefault(q url.Values, key string, defaultValue uint) uint

QueryUintOrDefault parses query parameter as unsigned integer or returns defaultValue if missing or invalid.

func StringValueOrDefault

func StringValueOrDefault(v, def string) string

StringValueOrDefault returns the string value or a default value.

func WithRequestTime

func WithRequestTime(ctx context.Context, t time.Time) context.Context

WithRequestTime returns new context with request timestamp attached via ReqTimeCtxKey.

Types

type HTTPResp

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

HTTPResp holds the configuration for the HTTP response methods.

func NewHTTPResp

func NewHTTPResp(l *slog.Logger) *HTTPResp

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

func (hr *HTTPResp) SendJSON(ctx context.Context, w http.ResponseWriter, statusCode int, data any)

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

func (hr *HTTPResp) SendStatus(ctx context.Context, w http.ResponseWriter, statusCode int)

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

func (sc Status) MarshalJSON() ([]byte, error)

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

func (sc Status) String() 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

func (sc *Status) UnmarshalJSON(data []byte) error

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.

Directories

Path Synopsis
Package jsendx implements an extended JSend response envelope for HTTP APIs.
Package jsendx implements an extended JSend response envelope for HTTP APIs.

Jump to

Keyboard shortcuts

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