resperr

package module
v2.0.0-...-7bdbf69 Latest Latest
Warning

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

Go to latest
Published: May 3, 2025 License: MIT Imports: 8 Imported by: 0

README

resperr GoDoc Go Report Card Calver v2.YY.Minor

Resperr is a Go package to associate status codes and messages with errors.

Example usage

See blog post for a full description or read the test code for more context:

// write a simple handler that just checks for errors
// and replies with an error object if it gets one

func myHandler(w http.ResponseWriter, r *http.Request) {
	// ... check user permissions...
	if err := hasPermissions(r); err != nil {
		replyError(w, r, err)
		return
	}
	// ...validate request...
	n, err := getItemNoFromRequest(r)
	if err != nil {
		replyError(w, r, err)
		return
	}
	// ...get the data ...
	item, err := getItemByNumber(n)
	if err != nil {
		replyError(w, r, err)
		return
	}
	replyJSON(w, r, http.StatusOK, item)
}

// in the functions that your handler calls
// use resp err to associate different error conditions
// with appropriate HTTP status codes

func getItemByNumber(n int) (item *Item, err error) {
	item, err = dbCall("...", n)
	if err == sql.ErrNoRows {
		// this is an anticipated 404
		return nil, resperr.New(
			http.StatusNotFound,
			"%d not found", n)
	}
	if err != nil {
		// this is an unexpected 500!
		return nil, err
	}
	// ...
	return
}

// you can also return specific messages for users as needed

func getItemNoFromRequest(r *http.Request) (int, error) {
	var v resperr.Validator
	ns := r.URL.Query().Get("n")
	v.AddIf("n", ns == "", "Please enter a number.")
	n, err := strconv.Atoi(ns)
	v.AddIfUnset("n", err != nil, "Input is not a number.")
	return n, v.Err()
}


func hasPermissions(r *http.Request) error {
	// lol, don't do this!
	user := r.URL.Query().Get("user")
	if user == "admin" {
		return nil
	}
	return resperr.New(http.StatusForbidden,
		"bad user %q", user)
}

Documentation

Overview

Package resperr contains helpers for associating http status codes and user messages with errors

Example
package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"

	"github.com/sphericalres/resperr/v2"
)

func main() {
	ts := httptest.NewServer(http.HandlerFunc(myHandler))
	defer ts.Close()

	printResponse(ts.URL, "?")
	// logs: [403] bad user ""
	// response: {"status":403,"error":"Forbidden"}
	printResponse(ts.URL, "?user=admin")
	// logs: validation error: n=Please enter a number.
	// response: {"status":400,"error":"Bad Request","details":{"n":["Please enter a number."]}}
	printResponse(ts.URL, "?user=admin&n=x")
	// logs: validation error: n=Input is not a number.
	// response: {"status":400,"error":"Bad Request","details":{"n":["Input is not a number."]}}
	printResponse(ts.URL, "?user=admin&n=1")
	// logs: [404] 1 not found
	// response: {"status":404,"error":"Not Found"}
	printResponse(ts.URL, "?user=admin&n=2")
	// logs: could not connect to database (X_X)
	// response: {"status":500,"error":"Internal Server Error"}
	printResponse(ts.URL, "?user=admin&n=3")
	// response: {"data":"data 3"}

}

func replyError(w http.ResponseWriter, r *http.Request, err error) {
	logError(w, r, err)
	code := resperr.StatusCode(err)
	msg := resperr.UserMessage(err)
	details := resperr.ValidationErrors(err)
	replyJSON(w, r, code, struct {
		Status  int        `json:"status"`
		Error   string     `json:"error,omitzero"`
		Details url.Values `json:"details,omitzero"`
	}{
		code,
		msg,
		details,
	})
}

func myHandler(w http.ResponseWriter, r *http.Request) {
	// ... check user permissions...
	if err := hasPermissions(r); err != nil {
		replyError(w, r, err)
		return
	}
	// ...validate request...
	n, err := getItemNoFromRequest(r)
	if err != nil {
		replyError(w, r, err)
		return
	}
	// ...get the data ...
	item, err := getItemByNumber(n)
	if err != nil {
		replyError(w, r, err)
		return
	}
	replyJSON(w, r, http.StatusOK, item)
}

func getItemByNumber(n int) (item *Item, err error) {
	item, err = dbCall("...", n)
	if err == sql.ErrNoRows {
		// this is an anticipated 404
		return nil, resperr.New(
			http.StatusNotFound,
			"%d not found", n)
	}
	if err != nil {
		// this is an unexpected 500!
		return nil, err
	}
	// ...
	return
}

func getItemNoFromRequest(r *http.Request) (int, error) {
	var v resperr.Validator
	ns := r.URL.Query().Get("n")
	v.AddIf("n", ns == "", "Please enter a number.")
	n, err := strconv.Atoi(ns)
	v.AddIfUnset("n", err != nil, "Input is not a number.")
	return n, v.Err()
}

func hasPermissions(r *http.Request) error {
	// lol, don't do this!
	user := r.URL.Query().Get("user")
	if user == "admin" {
		return nil
	}
	return resperr.New(http.StatusForbidden,
		"bad user %q", user)
}

// boilerplate below:

type Item struct {
	Data string `json:"data"`
}

func dbCall(s string, i int) (*Item, error) {
	if i == 1 {
		return nil, sql.ErrNoRows
	}
	if i == 2 {
		return nil, fmt.Errorf("could not connect to database (X_X)")
	}
	return &Item{fmt.Sprintf("data %d", i)}, nil
}

func logError(w http.ResponseWriter, r *http.Request, err error) {
	fmt.Printf("logged   ?%s: %v\n", r.URL.RawQuery, err)
}

func replyJSON(w http.ResponseWriter, r *http.Request, statusCode int, data any) {
	b, err := json.Marshal(data)
	if err != nil {
		logError(w, r, err)
		w.WriteHeader(http.StatusInternalServerError)
		// Don't use replyJSON to write the error, due to possible loop
		w.Write([]byte(`{"status": 500, "error": "Internal server error"}`))
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	_, err = w.Write(b)
	if err != nil {
		logError(w, r, err)
	}
}

func printResponse(base, u string) {
	resp, err := http.Get(base + u)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	fmt.Printf("response %s: %s\n", u, b)
}
Output:

logged   ?: [403] bad user ""
response ?: {"status":403,"error":"Forbidden"}
logged   ?user=admin: validation error: n=Please enter a number.
response ?user=admin: {"status":400,"error":"Bad Request","details":{"n":["Please enter a number."]}}
logged   ?user=admin&n=x: validation error: n=Input is not a number.
response ?user=admin&n=x: {"status":400,"error":"Bad Request","details":{"n":["Input is not a number."]}}
logged   ?user=admin&n=1: [404] 1 not found
response ?user=admin&n=1: {"status":404,"error":"Not Found"}
logged   ?user=admin&n=2: could not connect to database (X_X)
response ?user=admin&n=2: {"status":500,"error":"Internal Server Error"}
response ?user=admin&n=3: {"data":"data 3"}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func M

func M(format string, v ...any) error

M is a convenience function for calling fmt.Sprintf for a UserMessage.

func New

func New(code int, format string, v ...any) error

New is a convenience function for setting a status code and calling fmt.Errorf.

func NotFound

func NotFound(r *http.Request) error

NotFound creates an error with a 404 status code and a user message showing the request path that was not found.

func StatusCode

func StatusCode(err error) (code int)

StatusCode returns the status code associated with an error. If no status code is found, it returns 500 http.StatusInternalServerError. As a special case, it checks for Timeout() and Temporary() errors and returns 504 http.StatusGatewayTimeout and 503 http.StatusServiceUnavailable respectively. If err is nil, it returns 200 http.StatusOK.

func UserMessage

func UserMessage(err error) string

UserMessage returns the user message associated with an error. If no message is found, it checks StatusCode and returns that message. Because the default status is 500, the default message is "Internal Server Error". If err is nil, it returns "".

func ValidationErrors

func ValidationErrors(err error) url.Values

ValidationErrors returns any ValidationError found in err's error chain or an empty map.

Types

type E

type E struct {
	S int    // StatusCode
	M string // UserMessage
	E error  // Cause
}

E is a simple struct for building response errors.

func (E) Error

func (e E) Error() string

func (E) StatusCode

func (e E) StatusCode() int

func (E) Unwrap

func (e E) Unwrap() error

func (E) UserMessage

func (e E) UserMessage() string

type StatusCoder

type StatusCoder interface {
	error
	StatusCode() int
}

StatusCoder is an error with an associated HTTP status code. StatusCode may return 0 to indicate that the status code should be taken from another error in the chain.

type UserMessenger

type UserMessenger interface {
	error
	UserMessage() string
}

UserMessenger is an error with an associated user-facing message. UserMessage may return "" to indicate that the user message should be taken from another error in the chain.

type ValidationError

type ValidationError interface {
	error
	ValidationErrors() url.Values
}

ValidationError is an error with an associated set of validation messages for request fields

type Validator

type Validator url.Values

Validator creates a map of fields to error messages.

Example
var v resperr.Validator
v.AddIf("heads", 2 > 1, "Two are better than one.")
v.AddIf("heads", true, "I win, tails you lose.")
err := v.Err()

fmt.Println(resperr.StatusCode(err))
for field, msgs := range resperr.ValidationErrors(err) {
	for _, msg := range msgs {
		fmt.Println(field, "=", msg)
	}
}
Output:

400
heads = Two are better than one.
heads = I win, tails you lose.

func (*Validator) Add

func (v *Validator) Add(field string, message string, a ...any)

Add the provided message to field values. Add works with the zero value of Validator.

func (*Validator) AddIf

func (v *Validator) AddIf(field string, cond bool, message string, a ...any)

AddIf adds the provided message to field if cond is true. AddIf works with the zero value of Validator.

func (*Validator) AddIfUnset

func (v *Validator) AddIfUnset(field string, cond bool, message string, a ...any)

AddIfUnset adds the provided message to field if cond is true and the field does not already have a validation message. AddIfUnset works with the zero value of Validator.

Example
var v resperr.Validator
x, err := strconv.Atoi("hello")
v.AddIf("x", err != nil, "Could not parse x.")
v.AddIf("x", x < 1, "X must be positive.")

y, err := strconv.Atoi("hello")
v.AddIf("y", err != nil, "Could not parse y.")
v.AddIfUnset("y", y < 1, "Y must be positive.")
fmt.Println(v.Err())
Output:

validation error: x=Could not parse x. x=X must be positive. y=Could not parse y.

func (*Validator) Err

func (v *Validator) Err() error

Err transforms v to a ValidatorError if v is not empty. The error created shares the same underlying map reference as v.

func (*Validator) Valid

func (v *Validator) Valid() bool

Valid reports whether v had any validation failures.

Jump to

Keyboard shortcuts

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