3
0
corteza/pkg/errors/http.go

148 lines
3.6 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.
serveHTTP(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()
}
serveHTTP(w, r, code, err, mask)
}
func serveHTTP(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)
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) {
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 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)
}
}
write(dlmt + "\n")
}
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() {
// trim error details when not debugging or error is not safe
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))
}
}