resperr

package module
v0.20.5 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2020 License: MIT Imports: 3 Imported by: 4

README

resperr GoDoc Go Report Card Calver v0.YY.Minor

Go package to associate status codes and messages with errors

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/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)
}
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 ...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 added in v0.20.2

func WithUserMessagef(err error, format string, v ...interface{}) 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

Jump to

Keyboard shortcuts

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