resperr

package module
v0.22.0 Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2022 License: MIT Imports: 5 Imported by: 4

README

resperr GoDoc Go Report Card Calver v0.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) {
	ns := r.URL.Query().Get("n")
	if ns == "" {
		return 0, resperr.WithUserMessage(
			resperr.New(
				http.StatusBadRequest,
				"missing ?n= in query"),
			"Please enter a number.")
	}
	n, err := strconv.Atoi(ns)
	if err != nil {
		return 0, resperr.WithCodeAndMessage(
			err, http.StatusBadRequest,
			"Input is not a number.")
	}
	return n, nil
}


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"
	"strconv"

	"github.com/carlmjohnson/resperr"
)

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

	printResponse(ts.URL, "?")
	// logs: [403] bad user ""
	// response: {"status":403,"message":"Forbidden"}
	printResponse(ts.URL, "?user=admin")
	// logs: [400] missing ?n= in query
	// response: {"status":400,"message":"Please enter a number."}
	printResponse(ts.URL, "?user=admin&n=x")
	// logs: [400] strconv.Atoi: parsing "x": invalid syntax
	// response: {"status":400,"message":"Input is not a number."}
	printResponse(ts.URL, "?user=admin&n=1")
	// logs: [404] 1 not found
	// response: {"status":404,"message":"Not Found"}
	printResponse(ts.URL, "?user=admin&n=2")
	// logs: could not connect to database (X_X)
	// response: {"status":500,"message":"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)
	replyJSON(w, r, code, struct {
		Status  int    `json:"status"`
		Message string `json:"message"`
	}{
		code,
		msg,
	})
}

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) {
	ns := r.URL.Query().Get("n")
	if ns == "" {
		return 0, resperr.WithUserMessage(
			resperr.New(
				http.StatusBadRequest,
				"missing ?n= in query"),
			"Please enter a number.")
	}
	n, err := strconv.Atoi(ns)
	if err != nil {
		return 0, resperr.WithCodeAndMessage(
			err, http.StatusBadRequest,
			"Input is not a number.")
	}
	return n, nil
}

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, "message": "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,"message":"Forbidden"}
logged   ?user=admin: [400] missing ?n= in query
response ?user=admin: {"status":400,"message":"Please enter a number."}
logged   ?user=admin&n=x: [400] strconv.Atoi: parsing "x": invalid syntax
response ?user=admin&n=x: {"status":400,"message":"Input is not a number."}
logged   ?user=admin&n=1: [404] 1 not found
response ?user=admin&n=1: {"status":404,"message":"Not Found"}
logged   ?user=admin&n=2: could not connect to database (X_X)
response ?user=admin&n=2: {"status":500,"message":"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 New added in v0.20.4

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

New is a convenience function for calling fmt.Errorf and WithStatusCode.

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 added in v0.22.0

func ValidationErrors(err error) url.Values

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

func WithCodeAndMessage

func WithCodeAndMessage(err error, code int, msg string) error

WithCodeAndMessage is a convenience function for calling both WithStatusCode and WithUserMessage.

func WithStatusCode

func WithStatusCode(err error, code int) error

WithStatusCode adds a StatusCoder to err's error chain. Unlike pkg/errors, WithStatusCode will wrap nil error.

func WithUserMessage

func WithUserMessage(err error, msg string) error

WithUserMessage adds a UserMessenger to err's error chain. If a status code has not previously been set, a default status of Bad Request (400) is added. Unlike pkg/errors, WithUserMessage will wrap nil error.

func WithUserMessagef added in v0.20.2

func WithUserMessagef(err error, format string, v ...any) error

WithUserMessagef calls fmt.Sprintf before calling WithUserMessage.

Types

type StatusCoder

type StatusCoder interface {
	error
	StatusCode() int
}

StatusCoder is an error with an associated HTTP status code

type UserMessenger

type UserMessenger interface {
	error
	UserMessage() string
}

UserMessenger is an error with an associated user-facing message

type ValidationError added in v0.22.0

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

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

type Validator added in v0.22.0

type Validator url.Values

Validator creates a map of fields to error messages.

Example
package main

import (
	"fmt"

	"github.com/carlmjohnson/resperr"
)

func main() {
	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 added in v0.22.0

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 added in v0.22.0

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 added in v0.22.0

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
package main

import (
	"fmt"
	"strconv"

	"github.com/carlmjohnson/resperr"
)

func main() {
	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 added in v0.22.0

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 added in v0.22.0

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