errors

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Dec 18, 2025 License: Apache-2.0 Imports: 9 Imported by: 0

README

errors

Framework-agnostic error formatting for HTTP responses.

This package provides a clean, extensible way to format errors for HTTP APIs, supporting multiple response formats including RFC 9457 Problem Details, JSON:API, and simple JSON.

Features

  • Multiple formats: RFC 9457 Problem Details, JSON:API, Simple JSON
  • Content negotiation: Choose format based on Accept header
  • Extensible: Add custom formatters by implementing the Formatter interface
  • Framework-agnostic: Works with any HTTP handler (net/http, Gin, Echo, etc.)
  • Type-safe: Domain errors can implement optional interfaces to control formatting

Quick Start

import "rivaas.dev/errors"

// Create a formatter
formatter := errors.NewRFC9457("https://api.example.com/problems")

// Format an error
response := formatter.Format(req, err)

// Write response
w.WriteHeader(response.Status)
w.Header().Set("Content-Type", response.ContentType)
json.NewEncoder(w).Encode(response.Body)

Formatters

RFC 9457 Problem Details

RFC 9457 (formerly RFC 7807) provides a standardized way to represent errors in HTTP APIs.

formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)

Response format:

{
  "type": "https://api.example.com/problems/validation_error",
  "title": "Bad Request",
  "status": 400,
  "detail": "Validation failed",
  "instance": "/api/users",
  "error_id": "err-abc123",
  "code": "validation_error",
  "errors": [...]
}

Customization:

formatter := &errors.RFC9457{
    BaseURL: "https://api.example.com/problems",
    TypeResolver: func(err error) string {
        // Custom type resolution logic
        return "https://api.example.com/problems/custom-type"
    },
    StatusResolver: func(err error) int {
        // Custom status resolution logic
        return http.StatusBadRequest
    },
    ErrorIDGenerator: func() string {
        // Custom error ID generation
        return "custom-id-" + uuid.New().String()
    },
    DisableErrorID: false, // Set to true to disable error IDs
}
JSON:API

JSON:API compliant error responses.

formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)

Response format:

{
  "errors": [
    {
      "id": "err-abc123",
      "status": "400",
      "code": "validation_error",
      "title": "Bad Request",
      "detail": "Validation failed",
      "source": {
        "pointer": "/data/attributes/email"
      }
    }
  ]
}

Customization:

formatter := &errors.JSONAPI{
    StatusResolver: func(err error) int {
        // Custom status resolution
        return http.StatusBadRequest
    },
}
Simple JSON

Simple, straightforward JSON error responses.

formatter := errors.NewSimple()
response := formatter.Format(req, err)

Response format:

{
  "error": "Something went wrong",
  "code": "internal_error",
  "details": {...}
}

Customization:

formatter := &errors.Simple{
    StatusResolver: func(err error) int {
        // Custom status resolution
        return http.StatusBadRequest
    },
}

Domain Error Interfaces

Your domain errors can implement optional interfaces to control how they're formatted:

ErrorType

Control the HTTP status code:

type NotFoundError struct {
    Resource string
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Resource)
}

func (e NotFoundError) HTTPStatus() int {
    return http.StatusNotFound
}
ErrorCode

Provide a machine-readable error code:

type ValidationError struct {
    Fields []FieldError
}

func (e ValidationError) Code() string {
    return "validation_error"
}
ErrorDetails

Provide structured details (e.g., field-level validation errors):

type ValidationError struct {
    Fields []FieldError
}

func (e ValidationError) Details() any {
    return e.Fields
}

Content Negotiation

Use multiple formatters with content negotiation:

formatters := map[string]errors.Formatter{
    "application/problem+json": errors.NewRFC9457("https://api.example.com/problems"),
    "application/vnd.api+json": errors.NewJSONAPI(),
    "application/json":         errors.NewSimple(),
}

// Select formatter based on Accept header
accept := req.Header.Get("Accept")
formatter := formatters[accept] // Add fallback logic as needed
response := formatter.Format(req, err)

Integration Examples

With net/http
func errorHandler(w http.ResponseWriter, req *http.Request, err error) {
    formatter := errors.NewRFC9457("https://api.example.com/problems")
    response := formatter.Format(req, err)
    
    w.WriteHeader(response.Status)
    w.Header().Set("Content-Type", response.ContentType)
    json.NewEncoder(w).Encode(response.Body)
}
With Custom Framework
type MyContext struct {
    Request  *http.Request
    Response http.ResponseWriter
}

func (c *MyContext) Error(err error) {
    formatter := errors.NewRFC9457("https://api.example.com/problems")
    response := formatter.Format(c.Request, err)
    
    c.Response.WriteHeader(response.Status)
    c.Response.Header().Set("Content-Type", response.ContentType)
    json.NewEncoder(c.Response).Encode(response.Body)
}

Custom Formatters

Create your own formatter by implementing the Formatter interface:

type CustomFormatter struct {
    // Your configuration
}

func (f *CustomFormatter) Format(req *http.Request, err error) errors.Response {
    // Your formatting logic
    return errors.Response{
        Status:      http.StatusBadRequest,
        ContentType: "application/json",
        Body:        map[string]string{"error": err.Error()},
    }
}

Testing

The package includes comprehensive tests. Run them with:

go test ./errors/...

License

See the main project license.

Documentation

Overview

Package errors provides framework-agnostic error formatting for HTTP responses.

The package defines a Formatter interface and provides concrete implementations for different error response formats:

  • RFC9457: RFC 9457 Problem Details (application/problem+json)
  • JSONAPI: JSON:API error responses (application/vnd.api+json)
  • Simple: Simple JSON error responses (application/json)

The package is independent of any HTTP framework and can be used with any HTTP handler. Domain errors can implement optional interfaces (ErrorType, ErrorDetails, ErrorCode) to control status codes and provide structured details.

Quick Start

Basic usage with RFC 9457 format:

package main

import (
	"encoding/json"
	"net/http"
	"rivaas.dev/errors"
)

func handler(w http.ResponseWriter, r *http.Request) {
	err := someOperation()
	if err != nil {
		formatter := errors.NewRFC9457("https://api.example.com/problems")
		response := formatter.Format(r, err)
		w.Header().Set("Content-Type", response.ContentType)
		w.WriteHeader(response.Status)
		json.NewEncoder(w).Encode(response.Body)
		return
	}
}

JSON:API format:

formatter := errors.NewJSONAPI()
response := formatter.Format(r, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Simple JSON format:

formatter := errors.NewSimple()
response := formatter.Format(r, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Error Interfaces

Domain errors can implement optional interfaces to provide additional information:

  • ErrorType: Declare HTTP status code
  • ErrorDetails: Provide structured details (e.g., field-level validation errors)
  • ErrorCode: Provide machine-readable error codes

Example error with all interfaces:

type ValidationError struct {
	Message string
	Fields  []FieldError
	Code    string
}

func (e ValidationError) Error() string {
	return e.Message
}

func (e ValidationError) HTTPStatus() int {
	return http.StatusBadRequest
}

func (e ValidationError) Details() any {
	return e.Fields
}

func (e ValidationError) Code() string {
	return e.Code
}

Examples

See the example_test.go file for complete working examples.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ErrorCode

type ErrorCode interface {
	error
	// Code returns a machine-readable error code.
	Code() string
}

ErrorCode allows errors to provide a machine-readable code. Domain errors can implement this interface to expose application-specific error codes.

Example:

type NotFoundError struct {
	Resource string
}

func (e NotFoundError) Error() string {
	return fmt.Sprintf("%s not found", e.Resource)
}

func (e NotFoundError) Code() string {
	return "RESOURCE_NOT_FOUND"
}

type ErrorDetails

type ErrorDetails interface {
	error
	// Details returns structured information about the error.
	Details() any
}

ErrorDetails allows errors to provide additional structured information. Domain errors can implement this interface to expose field-level details.

Example:

type ValidationError struct {
	Message string
	Fields  []FieldError
}

func (e ValidationError) Error() string {
	return e.Message
}

func (e ValidationError) Details() any {
	return e.Fields
}

type ErrorType

type ErrorType interface {
	error
	// HTTPStatus returns the HTTP status code for this error.
	HTTPStatus() int
}

ErrorType allows errors to declare their own HTTP status code. Domain errors can optionally implement this interface to control their status code.

Example:

type ValidationError struct {
	Message string
}

func (e ValidationError) Error() string {
	return e.Message
}

func (e ValidationError) HTTPStatus() int {
	return http.StatusBadRequest
}

type Formatter

type Formatter interface {
	// Format converts an error into HTTP response components.
	// It returns status code, content-type, and response body.
	//
	// Example:
	//
	//	response := formatter.Format(req, err)
	//	w.Header().Set("Content-Type", response.ContentType)
	//	w.WriteHeader(response.Status)
	//	json.NewEncoder(w).Encode(response.Body)
	//
	// Parameters:
	//   - req: HTTP request context (used for instance URI in RFC9457)
	//   - err: Error to format
	//
	// Returns a Response containing status code, content type, and body.
	Format(req *http.Request, err error) Response
}

Formatter defines how errors are formatted in HTTP responses. Implementations are framework-agnostic and work with any HTTP handler.

Example:

formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

type JSONAPI

type JSONAPI struct {
	// StatusResolver determines HTTP status from error.
	// If nil, uses ErrorType interface or defaults to 500.
	StatusResolver func(err error) int
}

JSONAPI formats errors per JSON:API specification. It produces responses with Content-Type "application/vnd.api+json". See: https://jsonapi.org/format/#errors

Example:

formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Example

ExampleJSONAPI demonstrates how to use the JSONAPI formatter.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"rivaas.dev/errors"

	stderrors "errors"
)

func main() {
	// Create a formatter
	formatter := errors.NewJSONAPI()

	// Create a test error
	err := stderrors.New("resource not found")

	// Create a request
	req := httptest.NewRequest(http.MethodGet, "/api/users/123", nil)

	// Format the error
	response := formatter.Format(req, err)

	// Write the response
	w := httptest.NewRecorder()
	w.WriteHeader(response.Status)
	w.Header().Set("Content-Type", response.ContentType)
	_ = json.NewEncoder(w).Encode(response.Body)

	_, _ = fmt.Printf("Status: %d\n", response.Status)
	_, _ = fmt.Printf("Content-Type: %s\n", response.ContentType)
}
Output:

Status: 500
Content-Type: application/vnd.api+json; charset=utf-8

func NewJSONAPI

func NewJSONAPI() *JSONAPI

NewJSONAPI creates a new JSONAPI formatter.

Example:

formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Returns a new JSONAPI formatter instance.

func (*JSONAPI) Format

func (f *JSONAPI) Format(req *http.Request, err error) Response

Format converts an error into a JSON:API error response. If the error implements ErrorDetails, it converts field-level errors into multiple JSON:API error objects. If the error implements ErrorCode, it includes the code in the error object.

Example:

formatter := errors.NewJSONAPI()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Parameters:

  • req: HTTP request (currently unused, reserved for future use)
  • err: Error to format

Returns a Response with JSON:API formatted error.

type ProblemDetail

type ProblemDetail struct {
	Type       string         `json:"type"`
	Title      string         `json:"title"`
	Status     int            `json:"status"`
	Detail     string         `json:"detail,omitempty"`
	Instance   string         `json:"instance,omitempty"`
	Extensions map[string]any `json:"-"` // Marshaled inline
}

ProblemDetail represents an RFC 9457 problem detail. It contains the standard problem detail fields plus extensions.

Example:

p := ProblemDetail{
	Type:     "https://api.example.com/problems/validation-error",
	Title:    "Validation Error",
	Status:   400,
	Detail:   "The request contains invalid data",
	Instance: "/api/users",
	Extensions: map[string]any{
		"errors": []FieldError{...},
	},
}

func (ProblemDetail) MarshalJSON

func (p ProblemDetail) MarshalJSON() ([]byte, error)

MarshalJSON implements custom JSON marshaling to include extensions inline. It merges extension fields into the main JSON object while protecting reserved field names.

Returns the JSON-encoded problem detail with extensions merged inline.

type RFC9457

type RFC9457 struct {
	// BaseURL is prepended to problem type slugs to create full URIs.
	// Example: "https://api.example.com/problems" + "/validation-error"
	BaseURL string

	// TypeResolver maps error types/codes to problem type URIs.
	// If nil, uses default mapping based on ErrorCode interface.
	TypeResolver func(err error) string

	// StatusResolver determines HTTP status from error.
	// If nil, uses default logic (ErrorType interface, then 500).
	StatusResolver func(err error) int

	// ErrorIDGenerator generates unique IDs for error tracking.
	// If nil, uses default UUID-based generation.
	ErrorIDGenerator func() string

	// DisableErrorID disables automatic error ID generation.
	DisableErrorID bool
}

RFC9457 formats errors as RFC 9457 Problem Details. It produces responses with Content-Type "application/problem+json".

Example:

formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Example

ExampleRFC9457 demonstrates how to use the RFC9457 formatter.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"rivaas.dev/errors"

	stderrors "errors"
)

func main() {
	// Create a formatter with a base URL for problem types
	formatter := errors.NewRFC9457("https://api.example.com/problems")

	// Create a test error
	err := stderrors.New("validation failed")

	// Create a request
	req := httptest.NewRequest(http.MethodPost, "/api/users", nil)

	// Format the error
	response := formatter.Format(req, err)

	// Write the response
	w := httptest.NewRecorder()
	w.WriteHeader(response.Status)
	w.Header().Set("Content-Type", response.ContentType)
	_ = json.NewEncoder(w).Encode(response.Body)

	_, _ = fmt.Printf("Status: %d\n", response.Status)
	_, _ = fmt.Printf("Content-Type: %s\n", response.ContentType)
}
Output:

Status: 500
Content-Type: application/problem+json; charset=utf-8
Example (CustomErrorID)

ExampleRFC9457_customErrorID demonstrates custom error ID generation.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"rivaas.dev/errors"

	stderrors "errors"
)

func main() {
	// Create a formatter with custom error ID generator
	formatter := &errors.RFC9457{
		BaseURL: "https://api.example.com/problems",
		ErrorIDGenerator: func() string {
			return "custom-id-12345"
		},
	}

	err := stderrors.New("test error")
	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	response := formatter.Format(req, err)

	body := response.Body.(errors.ProblemDetail)
	_, _ = fmt.Printf("Error ID: %v\n", body.Extensions["error_id"])
}
Output:

Error ID: custom-id-12345
Example (DisableErrorID)

ExampleRFC9457_disableErrorID demonstrates disabling error ID generation.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"rivaas.dev/errors"

	stderrors "errors"
)

func main() {
	// Create a formatter with error ID disabled
	formatter := &errors.RFC9457{
		BaseURL:        "https://api.example.com/problems",
		DisableErrorID: true,
	}

	err := stderrors.New("test error")
	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	response := formatter.Format(req, err)

	body := response.Body.(errors.ProblemDetail)
	if _, ok := body.Extensions["error_id"]; !ok {
		_, _ = fmt.Println("Error ID is disabled")
	}
}
Output:

Error ID is disabled

func NewRFC9457

func NewRFC9457(baseURL string) *RFC9457

NewRFC9457 creates a new RFC9457 formatter. The baseURL parameter is prepended to problem type slugs to create full URIs.

Example:

formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)

Parameters:

Returns a new RFC9457 formatter instance.

func (*RFC9457) Format

func (f *RFC9457) Format(req *http.Request, err error) Response

Format converts an error into an RFC 9457 Problem Details response. It determines the status code, problem type, and builds the problem detail structure. If the error implements ErrorDetails or ErrorCode interfaces, those are included as extensions.

Example:

formatter := errors.NewRFC9457("https://api.example.com/problems")
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Parameters:

  • req: HTTP request (used for instance URI)
  • err: Error to format

Returns a Response with RFC 9457 formatted error.

type Response

type Response struct {
	// Status is the HTTP status code.
	Status int

	// ContentType is the Content-Type header value.
	ContentType string

	// Body is the response body (will be marshaled to JSON/XML/etc).
	Body any

	// Headers contains additional headers to set (optional).
	Headers http.Header
}

Response represents a formatted error response. It contains all components needed to write an HTTP error response.

Example:

response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
if response.Headers != nil {
	for k, v := range response.Headers {
		for _, val := range v {
			w.Header().Add(k, val)
		}
	}
}
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

type Simple

type Simple struct {
	// StatusResolver determines HTTP status from error.
	// If nil, uses ErrorType interface or defaults to 500.
	StatusResolver func(err error) int
}

Simple formats errors as simple JSON objects. It produces responses with Content-Type "application/json". Format: {"error": "message", "details": {...}, "code": "..."}

Example:

formatter := errors.NewSimple()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)
Example

ExampleSimple demonstrates how to use the Simple formatter.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"

	"rivaas.dev/errors"

	stderrors "errors"
)

func main() {
	// Create a formatter
	formatter := errors.NewSimple()

	// Create a test error
	err := stderrors.New("internal server error")

	// Create a request
	req := httptest.NewRequest(http.MethodGet, "/api/health", nil)

	// Format the error
	response := formatter.Format(req, err)

	// Write the response
	w := httptest.NewRecorder()
	w.WriteHeader(response.Status)
	w.Header().Set("Content-Type", response.ContentType)
	_ = json.NewEncoder(w).Encode(response.Body)

	_, _ = fmt.Printf("Status: %d\n", response.Status)
	_, _ = fmt.Printf("Content-Type: %s\n", response.ContentType)
}
Output:

Status: 500
Content-Type: application/json; charset=utf-8

func NewSimple

func NewSimple() *Simple

NewSimple creates a new Simple formatter.

Example:

formatter := errors.NewSimple()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Returns a new Simple formatter instance.

func (*Simple) Format

func (f *Simple) Format(req *http.Request, err error) Response

Format converts an error into a simple JSON response. If the error implements ErrorDetails or ErrorCode interfaces, those are included in the response.

Example:

formatter := errors.NewSimple()
response := formatter.Format(req, err)
w.Header().Set("Content-Type", response.ContentType)
w.WriteHeader(response.Status)
json.NewEncoder(w).Encode(response.Body)

Parameters:

  • req: HTTP request (currently unused, reserved for future use)
  • err: Error to format

Returns a Response with simple JSON formatted error.

Jump to

Keyboard shortcuts

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