httperr

Overview

RFC 9457 problem details for HTTP handlers.

httperr is a standalone Go module (github.com/gp-system/httperr): the wire-facing side of the gpsystem error model, turning a Go error into an RFC 9457 problem+json HTTP response. It depends on gp-system/errs (for reading a built error's code, public message and status) and go-playground/validator (via the httperr/validate subpackage, for turning struct-tag validation failures into the same shape). It works with plain net/http: no framework required. Building the errors it renders is errs's job, covered on the Error model page; this page and the two after it cover rendering.

Install

go get github.com/gp-system/httperr@v0.1.0 # the kit itself uses this tag
go get github.com/gp-system/httperr@latest

Go 1.25+. Dependencies: github.com/gp-system/errs and github.com/go-playground/validator/v10 (only pulled in transitively if you import httperr/validate; the base httperr package needs just errs).

The Problem type

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

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"`
}

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

Problem implements error, so you can build one and return it directly from a handler like any other error; the next page covers how it (and every other error) actually reaches the HTTP response. A worked example, in full:

if productID == "" {
    return httperr.BadRequest("productId is required")
}
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "productId is required"
}

Constructors

httperr.New(status, title, detail) // title defaults to the HTTP status text
httperr.BadRequest(detail)         // 400
httperr.Unauthorized(detail)       // 401
httperr.Forbidden(detail)          // 403
httperr.NotFound(detail)           // 404
httperr.Conflict(detail)           // 409
httperr.Internal(detail)           // 500
httperr.Validation(fieldErrs...)   // 422, with errors[]

These cover quick, anonymous 4xx cases well: return httperr.NotFound("no such product") in a handler is perfectly fine as-is. As soon as a failure mode has a name, a stable code, or more than one call site, errs.Define is the better home for it: recognition (errors.Is) and rendering (status, code, message) then live in one declaration instead of scattered httperr.___(...) calls, and the mapping picks it up the same way.

  • Error responses: WriteError/NewWriteError, the full mapping order, and how HTTP_EXPOSE_INTERNAL_ERRORS fits in.
  • Validation: the httperr/validate package, Translate, and the 422 response shape.
  • errs: Overview: the module that builds the errors httperr renders.
  • Error model: the whole request-to-response pipeline.
Copyright © 2026