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 ¶
- func M(format string, v ...any) error
- func New(code int, format string, v ...any) error
- func NotFound(r *http.Request) error
- func StatusCode(err error) (code int)
- func UserMessage(err error) string
- func ValidationErrors(err error) url.Values
- type E
- type StatusCoder
- type UserMessenger
- type ValidationError
- type Validator
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NotFound ¶
NotFound creates an error with a 404 status code and a user message showing the request path that was not found.
func StatusCode ¶
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 ¶
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 ¶
ValidationErrors returns any ValidationError found in err's error chain or an empty map.
Types ¶
type E ¶
E is a simple struct for building response errors.
func (E) StatusCode ¶
func (E) UserMessage ¶
type StatusCoder ¶
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 ¶
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 ¶
ValidationError is an error with an associated set of validation messages for request fields
type Validator ¶
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 ¶
Add the provided message to field values. Add works with the zero value of Validator.
func (*Validator) AddIf ¶
AddIf adds the provided message to field if cond is true. AddIf works with the zero value of Validator.
func (*Validator) AddIfUnset ¶
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.