Initial QSfera import
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
||||
# Configuration file for golangci-lint
|
||||
#
|
||||
# https://github.com/golangci/golangci-lint
|
||||
#
|
||||
# fighting with false positives?
|
||||
# https://github.com/golangci/golangci-lint#nolint
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
|
||||
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
|
||||
- gosec # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: true, auto-fix: false]
|
||||
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
|
||||
- deadcode # Finds unused code [fast: true, auto-fix: false]
|
||||
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false]
|
||||
|
||||
disable:
|
||||
# TODO(ross): fix errors reported by these checkers and enable them
|
||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false]
|
||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
||||
- dupl # Tool for code clone detection [fast: true, auto-fix: false]
|
||||
- errcheck # Inspects source code for security problems [fast: true, auto-fix: false]
|
||||
- gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false]
|
||||
- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false]
|
||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||
- gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false]
|
||||
- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
|
||||
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false]
|
||||
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
|
||||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||
- interfacer # Linter that suggests narrower interface types [fast: false, auto-fix: false]
|
||||
- lll # Reports long lines [fast: true, auto-fix: false]
|
||||
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false]
|
||||
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
|
||||
- prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false]
|
||||
- scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false]
|
||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false]
|
||||
- structcheck # Finds unused struct fields [fast: true, auto-fix: false]
|
||||
- stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false]
|
||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
||||
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
|
||||
- unparam # Reports unused function parameters [fast: false, auto-fix: false]
|
||||
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||
- varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/crewjam/saml
|
||||
govet:
|
||||
disable:
|
||||
- shadow
|
||||
enable:
|
||||
- asmdecl
|
||||
- assign
|
||||
- atomic
|
||||
- bools
|
||||
- buildtag
|
||||
- cgocall
|
||||
- composites
|
||||
- copylocks
|
||||
- errorsas
|
||||
- httpresponse
|
||||
- loopclosure
|
||||
- lostcancel
|
||||
- nilfunc
|
||||
- printf
|
||||
- shift
|
||||
- stdmethods
|
||||
- structtag
|
||||
- tests
|
||||
- unmarshal
|
||||
- unreachable
|
||||
- unsafeptr
|
||||
- unusedresult
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
- G104 # 'Errors unhandled. (gosec)
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
language: go
|
||||
|
||||
env: GO111MODULE=on
|
||||
|
||||
before_script:
|
||||
- curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.21.0
|
||||
|
||||
script:
|
||||
- golangci-lint run
|
||||
- go test -v ./...
|
||||
|
||||
go:
|
||||
- tip
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2016, Ross Kinder
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# httperr
|
||||
|
||||
[](https://godoc.org/github.com/crewjam/httperr)
|
||||
|
||||
[](https://travis-ci.org/crewjam/httperr)
|
||||
|
||||
Package httperr provides utilities for handling error conditions in http
|
||||
clients and servers.
|
||||
|
||||
## Client
|
||||
|
||||
This package provides an http.Client that returns errors for requests that return
|
||||
a status code >= 400. It lets you turn code like this:
|
||||
|
||||
```golang
|
||||
func GetFoo() {
|
||||
req, _ := http.NewRequest("GET", "https://api.example.com/foo", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("api call failed: %d", resp.StatusCode)
|
||||
}
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
Into code like this:
|
||||
|
||||
```golang
|
||||
func GetFoo() {
|
||||
req, _ := http.NewRequest("GET", "https://api.example.com/foo", nil)
|
||||
resp, err := httperr.Client().Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
Wow, three whole lines. Life changing, eh? But wait, there's more!
|
||||
|
||||
You can have the client parse structured errors returned from an API:
|
||||
|
||||
```golang
|
||||
|
||||
type APIError struct {
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func (a APIError) Error() string {
|
||||
// APIError must implement the Error interface
|
||||
return fmt.Sprintf("%s (code %d)", a.Message, a.Code)
|
||||
}
|
||||
|
||||
func GetFoo() {
|
||||
client := httperr.Client(http.DefaultClient, httperr.JSON(APIError{}))
|
||||
|
||||
req, _ := http.NewRequest("GET", "https://api.example.com/foo", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// If the server returned a status code >= 400, and the response was valid
|
||||
// JSON for APIError, then err is an *APIErr.
|
||||
return nil, err
|
||||
}
|
||||
// ....
|
||||
}
|
||||
```
|
||||
|
||||
## Server
|
||||
|
||||
Error handling in Go's http.Handler and http.HandlerFunc can be tricky. I often found myself wishing that we could just return an `err` and be done with things.
|
||||
|
||||
This package provides an adapter function which turns:
|
||||
|
||||
```golang
|
||||
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
|
||||
remoteUser, err := s.Auth.RequireUser(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.Storage.Get(remoteUser.Name)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: cannot fetch user: %s", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
```
|
||||
|
||||
Into this:
|
||||
|
||||
```golang
|
||||
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) error {
|
||||
remoteUser, err := s.Auth.RequireUser(w, r)
|
||||
if err != nil {
|
||||
return httperr.Unauthorized
|
||||
}
|
||||
|
||||
user, err := s.Storage.Get(remoteUser.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.NewEncoder(w).Encode(user)
|
||||
}
|
||||
```
|
||||
|
||||
Life changing? Probably not, but it seems to remove a lot of redundancy and make control flow in web servers simpler.
|
||||
|
||||
You can also wrap your calls with middleware that allow you to provide custom handling of errors that are returned from your handlers, but also >= 400 status codes issued by handlers that don't return errors.
|
||||
|
||||
```golang
|
||||
htmlErrorTmpl := template.Must(template.New("err").Parse(errorTemplate))
|
||||
handler := httperr.Middleware{
|
||||
OnError: func(w http.ResponseWriter, r *http.Request, err error) error {
|
||||
log.Printf("REQUEST ERROR: %s", err)
|
||||
if acceptHeaderContainsTextHTML(r) {
|
||||
htmlErrorTmpl.Execute(w, struct{ Error error }{Error: err})
|
||||
return nil // nil means we've handled the error
|
||||
}
|
||||
return err // fall back to the default
|
||||
},
|
||||
Handler: httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != "POST" {
|
||||
return httperr.MethodNotAllowed
|
||||
}
|
||||
var reqBody RequestBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||
return httperr.Public{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if reqBody.Count <= 0 {
|
||||
// The client won't see this, instead OnError will be called with a httperr.Response containing
|
||||
// the response. The OnError function can decide to write the error, or replace it with it's own.
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
fmt.Fprintln(w, "an obscure internal error happened, but the user doesn't want to see this.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ...
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
```
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package httperr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ClientArg is an argument to Client
|
||||
type ClientArg func(xport *Transport)
|
||||
|
||||
// Client returns an http.Client that wraps client with
|
||||
// an error handling transport.
|
||||
func Client(next *http.Client, args ...ClientArg) *http.Client {
|
||||
xport := Transport{Next: next.Transport}
|
||||
for _, arg := range args {
|
||||
arg(&xport)
|
||||
}
|
||||
|
||||
rv := *next
|
||||
rv.Transport = xport
|
||||
return &rv
|
||||
}
|
||||
|
||||
// DefaultClient returns an http.Client that wraps the default
|
||||
// http.Client with an error handling transport.
|
||||
func DefaultClient() *http.Client {
|
||||
return Client(http.DefaultClient)
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = Transport{}
|
||||
|
||||
// Transport is an http.RoundTripper that intercepts responses where
|
||||
// the StatusCode >= 400 and returns a Response{}.
|
||||
//
|
||||
// If ErrorFactory is specified it should return an error that can be used
|
||||
// to unmarshal a JSON error response. This is useful when a web service
|
||||
// offers structured error information. If the error structure cannot be
|
||||
// unmarshalled, then a regular Response error is returned.
|
||||
//
|
||||
// type APIError struct {
|
||||
// Code string `json:"code"`
|
||||
// Message string `json:"message"`
|
||||
// }
|
||||
//
|
||||
// func (a APIError) Error() string {
|
||||
// return fmt.Sprintf("%s (%d)", a.Message, a.Code)
|
||||
// }
|
||||
//
|
||||
// t := Transport{
|
||||
// ErrorFactory: func() error {
|
||||
// return &APIError{}
|
||||
// },
|
||||
// }
|
||||
//
|
||||
type Transport struct {
|
||||
Next http.RoundTripper
|
||||
OnError func(req *http.Request, resp *http.Response) error
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
next := t.Next
|
||||
if next == nil {
|
||||
next = http.DefaultTransport
|
||||
}
|
||||
|
||||
resp, err := next.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 400 {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if t.OnError != nil {
|
||||
if err := t.OnError(req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, Response(*resp)
|
||||
}
|
||||
|
||||
// JSON returns a ClientArg that specifies a function that
|
||||
// handles errors structured as a JSON object.
|
||||
func JSON(errStruct error) ClientArg {
|
||||
typ := reflect.TypeOf(errStruct)
|
||||
if typ.Kind() != reflect.Struct {
|
||||
panic("JSON() argument must be a structure")
|
||||
}
|
||||
|
||||
e := reflect.New(typ).Interface()
|
||||
_ = e.(error) // panic if errStruct
|
||||
|
||||
return func(xport *Transport) {
|
||||
xport.OnError = func(req *http.Request, resp *http.Response) error {
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body = ioutil.NopCloser(bytes.NewReader(body))
|
||||
|
||||
jsonErrValue := reflect.New(typ)
|
||||
|
||||
unmarshalErr := json.Unmarshal(body, jsonErrValue.Interface())
|
||||
if unmarshalErr == nil {
|
||||
// jsonErrValue is a *Foo if errStruct == Foo{}
|
||||
return jsonErrValue.Elem().Interface().(error)
|
||||
}
|
||||
|
||||
// we failed to unmarshal the response body, so ignore the
|
||||
// JSON error and proceed as if ErrorFactory was not provided.
|
||||
return Response(*resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package httperr
|
||||
|
||||
var (
|
||||
// BadRequest is an error that represents a static http.StatusBadRequest error
|
||||
BadRequest = Value{StatusCode: 400}
|
||||
// Unauthorized is an error that represents a static http.StatusUnauthorized error
|
||||
Unauthorized = Value{StatusCode: 401}
|
||||
// PaymentRequired is an error that represents a static http.StatusPaymentRequired error
|
||||
PaymentRequired = Value{StatusCode: 402}
|
||||
// Forbidden is an error that represents a static http.StatusForbidden error
|
||||
Forbidden = Value{StatusCode: 403}
|
||||
// NotFound is an error that represents a static http.StatusNotFound error
|
||||
NotFound = Value{StatusCode: 404}
|
||||
// MethodNotAllowed is an error that represents a static http.StatusMethodNotAllowed error
|
||||
MethodNotAllowed = Value{StatusCode: 405}
|
||||
// NotAcceptable is an error that represents a static http.StatusNotAcceptable error
|
||||
NotAcceptable = Value{StatusCode: 406}
|
||||
// ProxyAuthRequired is an error that represents a static http.StatusProxyAuthRequired error
|
||||
ProxyAuthRequired = Value{StatusCode: 407}
|
||||
// RequestTimeout is an error that represents a static http.StatusRequestTimeout error
|
||||
RequestTimeout = Value{StatusCode: 408}
|
||||
// Conflict is an error that represents a static http.StatusConflict error
|
||||
Conflict = Value{StatusCode: 409}
|
||||
// Gone is an error that represents a static http.StatusGone error
|
||||
Gone = Value{StatusCode: 410}
|
||||
// LengthRequired is an error that represents a static http.StatusLengthRequired error
|
||||
LengthRequired = Value{StatusCode: 411}
|
||||
// PreconditionFailed is an error that represents a static http.StatusPreconditionFailed error
|
||||
PreconditionFailed = Value{StatusCode: 412}
|
||||
// RequestEntityTooLarge is an error that represents a static http.StatusRequestEntityTooLarge error
|
||||
RequestEntityTooLarge = Value{StatusCode: 413}
|
||||
// RequestURITooLong is an error that represents a static http.StatusRequestURITooLong error
|
||||
RequestURITooLong = Value{StatusCode: 414}
|
||||
// UnsupportedMediaType is an error that represents a static http.StatusUnsupportedMediaType error
|
||||
UnsupportedMediaType = Value{StatusCode: 415}
|
||||
// RequestedRangeNotSatisfiable is an error that represents a static http.StatusRequestedRangeNotSatisfiable error
|
||||
RequestedRangeNotSatisfiable = Value{StatusCode: 416}
|
||||
// ExpectationFailed is an error that represents a static http.StatusExpectationFailed error
|
||||
ExpectationFailed = Value{StatusCode: 417}
|
||||
// Teapot is an error that represents a static http.StatusTeapot error
|
||||
Teapot = Value{StatusCode: 418}
|
||||
// TooManyRequests is an error that represents a static http.StatusTooManyRequests error
|
||||
TooManyRequests = Value{StatusCode: 429}
|
||||
// InternalServerError is an error that represents a static http.StatusInternalServerError error
|
||||
InternalServerError = Value{StatusCode: 500}
|
||||
// NotImplemented is an error that represents a static http.StatusNotImplemented error
|
||||
NotImplemented = Value{StatusCode: 501}
|
||||
// BadGateway is an error that represents a static http.StatusBadGateway error
|
||||
BadGateway = Value{StatusCode: 502}
|
||||
// ServiceUnavailable is an error that represents a static http.StatusServiceUnavailable error
|
||||
ServiceUnavailable = Value{StatusCode: 503}
|
||||
// GatewayTimeout is an error that represents a static http.StatusGatewayTimeout error
|
||||
GatewayTimeout = Value{StatusCode: 504}
|
||||
// HTTPVersionNotSupported is an error that represents a static http.StatusHTTPVersionNotSupported error
|
||||
HTTPVersionNotSupported = Value{StatusCode: 505}
|
||||
)
|
||||
|
||||
// New returns a new http error wrapping err with status statusCode.
|
||||
func New(statusCode int, err error) error {
|
||||
return Value{
|
||||
StatusCode: statusCode,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// Public returns a new public http error wrapping err with status statusCode.
|
||||
func Public(statusCode int, err error) error {
|
||||
return Value{
|
||||
Public: true,
|
||||
StatusCode: statusCode,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package httperr
|
||||
|
||||
import "net/http"
|
||||
|
||||
// The HandlerFunc type is an adapter to allow the use of
|
||||
// ordinary functions as HTTP handlers. If f is a function
|
||||
// with the appropriate signature, HandlerFunc(f) is a
|
||||
// Handler object that calls f.
|
||||
type HandlerFunc func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
// ServeHTTP calls f(w, r).
|
||||
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := f(w, r); err != nil {
|
||||
if v := r.Context().Value(onErrorIndex); v != nil {
|
||||
v.(func(error))(err)
|
||||
} else {
|
||||
Write(w, r, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Package httperr implements an error object that speaks HTTP.
|
||||
package httperr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type statusCodeAndTexter interface {
|
||||
StatusCodeAndText() (int, string)
|
||||
}
|
||||
|
||||
// StatusCodeAndText returns the status code and text of the error
|
||||
func StatusCodeAndText(err error) (int, string) {
|
||||
if err == nil {
|
||||
return http.StatusOK, http.StatusText(http.StatusOK)
|
||||
}
|
||||
|
||||
err = pkgerrors.Cause(err)
|
||||
|
||||
var scater statusCodeAndTexter
|
||||
if errors.As(err, &scater) {
|
||||
return scater.StatusCodeAndText()
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Writer is an interface for things that know how to write themselves
|
||||
// to an error response. This interface is implemented by Private and
|
||||
// Public to provide default error pages.
|
||||
type Writer interface {
|
||||
error
|
||||
WriteError(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Write writes the specified error to w. If err is a Writer, then
|
||||
// it's WriteError method is invoked to produce the response.
|
||||
// Otherwise a generic "500 Internal Server Error" is written.
|
||||
func Write(w http.ResponseWriter, r *http.Request, err error) {
|
||||
err = pkgerrors.Cause(err)
|
||||
|
||||
var errWriter Writer
|
||||
if errors.As(err, &errWriter) {
|
||||
errWriter.WriteError(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
genericErr := Value{
|
||||
Err: err,
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
genericErr.WriteError(w, r)
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package httperr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type onErrorIndexType int
|
||||
|
||||
const onErrorIndex onErrorIndexType = iota
|
||||
|
||||
// Middleware wraps the provided handler with middleware that captures errors which
|
||||
// are returned from HandlerFunc, or reported via ReportError, and invokes the provided
|
||||
// callback to render them. If the handler returns a status code >= 400, the response is
|
||||
// captured and passed to OnError as a Response.
|
||||
//
|
||||
type Middleware struct {
|
||||
// OnError is a function that is called then a request fails with an error. If this function
|
||||
// returns nil, then the error is assumed to be handled. If it returns a non-nil error, then
|
||||
// that error is written to the client with Write()
|
||||
OnError func(w http.ResponseWriter, r *http.Request, err error) error
|
||||
|
||||
// Handler is the next handler
|
||||
Handler http.Handler
|
||||
}
|
||||
|
||||
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var unwrappedWriter = w
|
||||
var wrappedWriter *basicWriter
|
||||
if m.OnError != nil {
|
||||
wrappedWriter, w = wrapWriter(w)
|
||||
}
|
||||
|
||||
var didCallOnError bool
|
||||
r = r.WithContext(context.WithValue(r.Context(), onErrorIndex, func(err error) {
|
||||
if m.OnError != nil {
|
||||
didCallOnError = true
|
||||
handlerErr := m.OnError(unwrappedWriter, r, err)
|
||||
if handlerErr != nil {
|
||||
Write(unwrappedWriter, r, handlerErr)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
m.Handler.ServeHTTP(w, r)
|
||||
|
||||
if wrappedWriter != nil && wrappedWriter.statusCode >= 400 && !didCallOnError {
|
||||
err := Response(*wrappedWriter.copy)
|
||||
handlerErr := m.OnError(unwrappedWriter, r, err)
|
||||
if handlerErr != nil {
|
||||
Write(unwrappedWriter, r, handlerErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReportError reports the error to the function given in
|
||||
// OnError.
|
||||
func ReportError(r *http.Request, err error) {
|
||||
if v := r.Context().Value(onErrorIndex); v != nil {
|
||||
v.(func(error))(err)
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package httperr
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Response is an alias for http.Response that implements
|
||||
// the error interface. Example:
|
||||
//
|
||||
// resp, err := http.Get("http://www.example.com")
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if resp.StatusCode != http.StatusOK {
|
||||
// return httperr.Response(*resp)
|
||||
// }
|
||||
// // ...
|
||||
//
|
||||
type Response http.Response
|
||||
|
||||
func (re Response) Error() string {
|
||||
statusText := re.Status
|
||||
if statusText == "" {
|
||||
statusText = http.StatusText(re.StatusCode)
|
||||
}
|
||||
return statusText
|
||||
}
|
||||
|
||||
// WriteError copies the Response to the ResponseWriter.
|
||||
func (re Response) WriteError(w http.ResponseWriter, r *http.Request) {
|
||||
for k, vv := range re.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(re.StatusCode)
|
||||
io.Copy(w, re.Body)
|
||||
}
|
||||
|
||||
var _ error = Response{}
|
||||
var _ Writer = Response{}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Package httperr implements an error object that speaks HTTP.
|
||||
package httperr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Value is an Error that returns that status and code
|
||||
// provided, and reveals the underlying wrapper error to
|
||||
// the caller. The text of the error is rendered to the
|
||||
// client in the body of the response, as well as in
|
||||
// the X-Error header.
|
||||
type Value struct {
|
||||
Err error // the underlying error
|
||||
StatusCode int // the HTTP status code. If not supplied, http.StatusInternalServerError is used.
|
||||
Status string // the HTTP status text. If not supplied, http.StatusText(http.StatusCode) is used.
|
||||
Public bool
|
||||
Header http.Header // extra headers to add to the response (optional)
|
||||
}
|
||||
|
||||
// StatusCodeAndText returns the status code and text of the error
|
||||
func (e Value) StatusCodeAndText() (int, string) {
|
||||
if e.StatusCode == 0 {
|
||||
e.StatusCode = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if e.Status == "" {
|
||||
if e.Err != nil && e.Public {
|
||||
e.Status = e.Err.Error()
|
||||
} else {
|
||||
e.Status = http.StatusText(e.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
return e.StatusCode, e.Status
|
||||
}
|
||||
|
||||
func (e Value) Error() string {
|
||||
statusCode, statusText := StatusCodeAndText(e)
|
||||
if e.Public {
|
||||
return fmt.Sprintf("%d %s", statusCode, statusText)
|
||||
}
|
||||
return fmt.Sprintf("%d %s: %s", statusCode, statusText, e.Err.Error())
|
||||
}
|
||||
|
||||
// WriteError writes an error response to w using the specified status code.
|
||||
func (e Value) WriteError(w http.ResponseWriter, r *http.Request) {
|
||||
for key, values := range e.Header {
|
||||
w.Header().Del(key) // overwrite headers already in the response with the ones specified
|
||||
for _, value := range values {
|
||||
w.Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
code, message := e.StatusCodeAndText()
|
||||
http.Error(w, message, code)
|
||||
}
|
||||
|
||||
// Unwrap unwraps the Value error and returns the underlying error`
|
||||
func (e Value) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
var _ error = Value{}
|
||||
var _ Writer = Value{}
|
||||
var _ statusCodeAndTexter = Value{}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package httperr
|
||||
|
||||
// Note: The writer proxies in this file are a heavily modified version of
|
||||
// code bearing the following message:
|
||||
//
|
||||
// Copyright (c) 2014, 2015, 2016 Carl Jackson (carl@avtok.com)
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// wrapWriter wraps an http.ResponseWriter, returning a proxy that
|
||||
// tracks the response.
|
||||
func wrapWriter(w http.ResponseWriter) (*basicWriter, http.ResponseWriter) {
|
||||
_, isCloseNotifier := w.(http.CloseNotifier)
|
||||
_, isFlusher := w.(http.Flusher)
|
||||
_, isHijacker := w.(http.Hijacker)
|
||||
|
||||
bw := basicWriter{ResponseWriter: w}
|
||||
if isCloseNotifier && isFlusher && isHijacker {
|
||||
rv := fancyWriter{bw}
|
||||
return &rv.basicWriter, &rv
|
||||
}
|
||||
if isFlusher {
|
||||
rv := flushWriter{bw}
|
||||
return &rv.basicWriter, &rv
|
||||
}
|
||||
return &bw, &bw
|
||||
}
|
||||
|
||||
type basicWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
statusCode int
|
||||
copy *http.Response
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *basicWriter) WriteHeader(code int) {
|
||||
b.statusCode = code
|
||||
if code < 400 {
|
||||
b.ResponseWriter.WriteHeader(code)
|
||||
return
|
||||
}
|
||||
|
||||
b.copy = &http.Response{
|
||||
StatusCode: code,
|
||||
Header: b.ResponseWriter.Header(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *basicWriter) Write(buf []byte) (int, error) {
|
||||
if b.copy == nil {
|
||||
return b.ResponseWriter.Write(buf)
|
||||
}
|
||||
|
||||
if b.body == nil {
|
||||
b.body = &bytes.Buffer{}
|
||||
b.copy.Body = ioutil.NopCloser(b.body)
|
||||
}
|
||||
return b.body.Write(buf)
|
||||
}
|
||||
|
||||
type fancyWriter struct {
|
||||
basicWriter
|
||||
}
|
||||
|
||||
func (f *fancyWriter) CloseNotify() <-chan bool {
|
||||
cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
|
||||
return cn.CloseNotify()
|
||||
}
|
||||
|
||||
func (f *fancyWriter) Flush() {
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
func (f *fancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj := f.basicWriter.ResponseWriter.(http.Hijacker)
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
var _ http.CloseNotifier = &fancyWriter{}
|
||||
var _ http.Flusher = &fancyWriter{}
|
||||
var _ http.Hijacker = &fancyWriter{}
|
||||
|
||||
type flushWriter struct {
|
||||
basicWriter
|
||||
}
|
||||
|
||||
func (f *flushWriter) Flush() {
|
||||
fl := f.basicWriter.ResponseWriter.(http.Flusher)
|
||||
fl.Flush()
|
||||
}
|
||||
|
||||
var _ http.Flusher = &flushWriter{}
|
||||
Reference in New Issue
Block a user