Back to godoc.org
github.com/carlmjohnson/resperr

Package resperr

v0.20.5
Latest Go to latest

The latest major version is .

Published: Aug 24, 2020 | License: MIT | Module: github.com/carlmjohnson/resperr

Overview

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

Example

Code:

package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"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 interface{}) {
	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, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("response %s: %s\n", u, b)
}
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

func New

func New(code int, format string, v ...interface{}) 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 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. Unlike pkg/errors, WithUserMessage will wrap nil error.

func WithUserMessagef

func WithUserMessagef(err error, format string, v ...interface{}) error

WithUserMessagef calls fmt.Sprintf before calling WithUserMessage.

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

Package Files

Documentation was rendered with GOOS=linux and GOARCH=amd64.

Jump to identifier

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to identifier