3
0
corteza/pkg/errors/http.go
Denis Arh d74239c735 Improve HTTP server startup, add wrapping handler
HTTP server now initializes much earlier and mounts "waiting"
router with debugging, version and health check routes.

When server is fully activated it switches to "active"
router.

Shutdown stage is also handled with catch-all route.
2022-02-13 18:52:59 +01:00

156 lines
3.8 KiB
Go

package errors
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"github.com/cortezaproject/corteza-server/pkg/locale"
)
type (
translatable interface {
Translate(tr func(string, string, ...string) string) error
}
)
// ServeHTTP Prepares and encodes given error for HTTP transport
//
// mask arg hides extra/debug info
//
// Proper HTTP status codes are generally not used in the API due to compatibility issues
// This should be addressed in the future versions when/if we restructure the API
func ServeHTTP(w http.ResponseWriter, r *http.Request, err error, mask bool) {
// due to backward compatibility,
// custom HTTP statuses are disabled for now.
ServeHTTPWithCode(w, r, http.StatusOK, err, mask)
}
// ProperlyServeHTTP Prepares and encodes given error for HTTP transport, same as ServeHTTP but with proper status codes
func ProperlyServeHTTP(w http.ResponseWriter, r *http.Request, err error, mask bool) {
var (
code = http.StatusInternalServerError
)
if e, is := err.(*Error); is {
code = e.kind.httpStatus()
}
ServeHTTPWithCode(w, r, code, err, mask)
}
// Serves error via
func ServeHTTPWithCode(w http.ResponseWriter, r *http.Request, code int, err error, mask bool) {
var (
// Very naive approach on parsing accept headers
acceptsJson = strings.Contains(r.Header.Get("accept"), "application/json")
)
if !mask && !acceptsJson {
// Prettify error for plain text debug output
w.Header().Set("Content-Type", "plain/text")
w.WriteHeader(code)
writeHttpPlain(w, err, mask)
_, _ = fmt.Fprintln(w, "Note: you are seeing this because system is running in development mode")
_, _ = fmt.Fprintln(w, "and HTTP request is made without \"Accept: .../json\" headers")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
writeHttpJSON(r.Context(), w, err, mask)
}
func writeHttpPlain(w io.Writer, err error, mask bool) {
var (
dlmt = strings.Repeat("-", 80)
write = func(s string, a ...interface{}) {
if _, err := fmt.Fprintf(w, s, a...); err != nil {
panic(fmt.Errorf("failed to write HTTP response: %w", err))
}
}
)
write("Error: ")
write(err.Error())
write("\n")
if _, is := err.(interface{ Safe() bool }); !is || mask {
// do not output any details on un-safe errors or
// when a masked output is preferred
return
}
if err, is := err.(*Error); is {
write(dlmt + "\n")
var (
ml, kk = err.meta.StringKeys()
)
sort.Strings(kk)
for _, key := range kk {
write("%s:", key)
write(strings.Repeat(" ", ml-len(key)+1))
write("%v\n", err.meta[key])
}
if len(err.stack) > 0 {
write(dlmt + "\n")
write("Call stack:\n")
for i, f := range err.stack {
write("%3d. %s:%d\n %s()\n", len(err.stack)-i, f.File, f.Line, f.Func)
}
}
if we := err.Unwrap(); we != nil {
write(dlmt + "\n")
write("Wrapped error:\n\n")
writeHttpPlain(w, we, mask)
}
}
write(dlmt + "\n")
}
// writeHttpJSON
func writeHttpJSON(ctx context.Context, w io.Writer, err error, mask bool) {
var (
wrap = struct {
Error interface{} `json:"error"`
}{}
)
if se, is := err.(interface{ Safe() bool }); !is || !se.Safe() || mask {
// trim error details when not debugging or error is not safe or maske
err = fmt.Errorf(err.Error())
}
// if error is translatable, pass in the lambda that returns
// translated error
//
// we're using global locale service directly for now
if tr, is := err.(translatable); is {
err = tr.Translate(func(ns string, key string, pairs ...string) string {
return locale.Global().T(ctx, ns, key, pairs...)
})
}
if c, is := err.(json.Marshaler); is {
// take advantage of JSON marshaller on error
wrap.Error = c
} else {
wrap.Error = map[string]string{"message": err.Error()}
}
if err = json.NewEncoder(w).Encode(wrap); err != nil {
panic(fmt.Errorf("failed to encode error: %w", err))
}
}