httperr

package module
v0.0.0-...-3b20622 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 7 Imported by: 0

README

httperr

RFC 9457 ("Problem Details for HTTP APIs") error types and net/http-based writers for chi/std-http servers, plus a validate subpackage that turns go-playground/validator failures into the same error shape.

httperr is developed as part of the gp-system tooling but has no dependency on it beyond github.com/gp-system/errs: it works in any net/http-based Go server, oapi-codegen generated or not.

The problem it solves

Every handler in a modular backend needs to turn an error into an HTTP response, and every response should look the same: same JSON shape, same content type, same rules for when to leak a message to the client versus log it and return a generic one. httperr centralizes that mapping once, so handlers return a *Problem or a plain error and the writer takes care of status codes, application/problem+json, and 5xx logging with request context. validate extends the same mapping to request-body validation, so a failed validator tag becomes a 422 Problem with per-field errors instead of a hand-rolled response.

Install

go get github.com/gp-system/httperr@latest

Tagged releases start at v0.1.0.

Usage

Problem

Problem is an RFC 9457 object. It implements error, so handlers and services can return it directly:

func getUser(id string) (*User, error) {
	u, ok := users[id]
	if !ok {
		return nil, httperr.NotFound("user not found")
	}
	return u, nil
}

Constructors exist for the common statuses: BadRequest, Unauthorized, Forbidden, NotFound, Conflict, Internal, Validation, or New for anything else.

WriteError / WriteBadRequest

WriteError renders any error as a Problem response. *Problem is rendered as-is; a github.com/gp-system/errs error maps to its declared status (falling back to 500), carrying its public message and code; anything else becomes a 500 whose detail is suppressed unless WithExposeInternal(true) is set. Only 5xx causes are logged, with the errs stack and wrap chain attached when present:

mux.Handle("/users/", errors.Handler(getUser, httperr.WriteError))

WriteBadRequest is the same mapping but treats every non-*Problem error as a 400, for request-decoding paths (e.g. oapi-codegen's RequestErrorHandlerFunc) where nothing else makes sense.

Both functions match oapi-codegen's ResponseErrorHandlerFunc / ErrorHandlerFunc hook signatures, so they plug straight into generated chi/std-http servers.

validate
import "github.com/gp-system/httperr/validate"

v := validate.New()

type createUser struct {
	Email string `json:"email" validate:"required,email"`
}

if err := v.Validate(&in); err != nil {
	// err is a *httperr.Problem (422) with per-field errors
}

Design rules

  • One response shape. Every error, wherever it originates, renders as the same RFC 9457 JSON: type, title, status, detail, instance, plus the code/errors extension members where relevant.
  • 5xx is the only thing that gets logged as an incident. A status-mapped 4xx errs error is expected client behavior; only 5xx responses are logged and reported.
  • Stacks and chains are dev-only. WithExposeInternal(true) is the only way to leak an error's message, stack, or wrap chain into a response; it is meant for local development, never production.

Documentation

Overview

Package httperr provides RFC 9457 Problem Details error types and net/http-based writers (WriteError, WriteBadRequest) that render every error as application/problem+json for chi/std-http servers.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewWriteError

func NewWriteError(opts ...HandlerOption) func(http.ResponseWriter, *http.Request, error)

NewWriteError returns a function that renders err as an RFC 9457 application/problem+json response.

Mapping order: *Problem is rendered as-is; an errs error maps to its errs.Status (via errs.StatusOf), falling back to 500 when none is set, carrying its Public message as detail and its Code as the "code" extension member; a *http.MaxBytesError becomes a 413; anything else becomes a 500 whose detail is suppressed unless WithExposeInternal(true) is set. Only 5xx causes are logged with request context so trace correlation (via the otelslog bridge) is preserved — a status-mapped 4xx errs error (e.g. an errs.Definition declared with errs.Status(http.StatusNotFound)) is expected client behavior, not an incident, and produces no ERROR log or Sentry event. Logged errs errors carry their structured chain and stack. Stacks and chains appear in responses only under WithExposeInternal (dev mode).

The signature matches oapi-codegen's ResponseErrorHandlerFunc hook, so it plugs straight into generated chi/std-http servers.

func WriteBadRequest

func WriteBadRequest(w http.ResponseWriter, r *http.Request, err error)

WriteBadRequest renders err as a 400 Problem unless err already carries a *Problem (rendered as-is) or is a body-limit violation (413). It is meant for oapi-codegen's parameter-binding and request-decoding hooks (ErrorHandlerFunc, RequestErrorHandlerFunc), where every error is a client error — WriteError would map unknown errors to 500 there.

func WriteError

func WriteError(w http.ResponseWriter, r *http.Request, err error)

WriteError is NewWriteError with default options, for direct use in generated-server options when no customization is needed.

Types

type FieldError

type FieldError struct {
	Field   string `json:"field"`
	Rule    string `json:"rule,omitempty"`
	Message string `json:"message"`
}

FieldError describes a single invalid input field. It is carried by validation Problems under the "errors" extension member.

type HandlerOption

type HandlerOption func(*handlerOptions)

HandlerOption configures NewWriteError.

func WithExposeInternal

func WithExposeInternal(expose bool) HandlerOption

WithExposeInternal includes the underlying error message in the detail of 5xx responses, and — when the error carries an errs error — its stack and wrap chain in the "stack" and "chain" extension members. Enable only in development; in production internal errors are logged but the client receives a generic Problem with no stack or chain.

func WithLogger

func WithLogger(l *slog.Logger) HandlerOption

WithLogger sets the logger used for 5xx errors. Defaults to slog.Default().

type Problem

type Problem struct {
	Type     string       `json:"type"`
	Title    string       `json:"title"`
	Status   int          `json:"status"`
	Detail   string       `json:"detail,omitempty"`
	Instance string       `json:"instance,omitempty"`
	Code     string       `json:"code,omitempty"`
	Errors   []FieldError `json:"errors,omitempty"`

	// Stack and Chain are dev-only debugging extension members. The error
	// handlers populate them only under WithExposeInternal(true); in production
	// they are always empty. They are deliberately not mirrored in the
	// generated errors.tsp contract — clients must not depend on them.
	Stack []errs.Frame `json:"stack,omitempty"`
	Chain []errs.Step  `json:"chain,omitempty"`
}

Problem is an RFC 9457 Problem Details object. It implements error, so handlers and services can return it directly; WriteError renders it.

func BadRequest

func BadRequest(detail string) *Problem

BadRequest builds a 400 Problem.

func Conflict

func Conflict(detail string) *Problem

Conflict builds a 409 Problem.

func Forbidden

func Forbidden(detail string) *Problem

Forbidden builds a 403 Problem.

func Internal

func Internal(detail string) *Problem

Internal builds a 500 Problem.

func New

func New(status int, title, detail string) *Problem

New builds a Problem with the given status. Title defaults to the standard HTTP status text when empty.

func NotFound

func NotFound(detail string) *Problem

NotFound builds a 404 Problem.

func Unauthorized

func Unauthorized(detail string) *Problem

Unauthorized builds a 401 Problem.

func Validation

func Validation(fields ...FieldError) *Problem

Validation builds a 422 Problem carrying per-field errors.

func (*Problem) Error

func (p *Problem) Error() string

Directories

Path Synopsis
Package validate wraps go-playground/validator so validation failures come back as an *httperr.Problem (422, per-field errors) instead of a raw validator.ValidationErrors, ready to write from any net/http handler.
Package validate wraps go-playground/validator so validation failures come back as an *httperr.Problem (422, per-field errors) instead of a raw validator.ValidationErrors, ready to write from any net/http handler.

Jump to

Keyboard shortcuts

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