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 ¶
- 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
- func WithCodeAndMessage(err error, code int, msg string) error
- func WithStatusCode(err error, code int) error
- func WithUserMessage(err error, msg string) error
- func WithUserMessagef(err error, format string, v ...any) error
- type StatusCoder
- type UserMessenger
- type ValidationError
- type Validator
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶ added in v0.20.4
New is a convenience function for calling fmt.Errorf and WithStatusCode.
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 ¶ added in v0.22.0
ValidationErrors returns any ValidationError found in err's error chain or an empty map.
func WithCodeAndMessage ¶
WithCodeAndMessage is a convenience function for calling both WithStatusCode and WithUserMessage.
func WithStatusCode ¶
WithStatusCode adds a StatusCoder to err's error chain. Unlike pkg/errors, WithStatusCode will wrap nil error.
func WithUserMessage ¶
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.
Types ¶
type StatusCoder ¶
StatusCoder is an error with an associated HTTP status code
type UserMessenger ¶
UserMessenger is an error with an associated user-facing message
type ValidationError ¶ added in v0.22.0
ValidationError is an error with an associated set of validation messages for request fields
type Validator ¶ added in v0.22.0
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
Add the provided message to field values. Add works with the zero value of Validator.
func (*Validator) AddIf ¶ added in v0.22.0
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
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.