Improve error creation & handling by API

This commit is contained in:
Denis Arh
2020-11-04 14:18:33 +01:00
parent 8ba5e22d72
commit ecb7f52c31
160 changed files with 7931 additions and 11070 deletions
+17 -35
View File
@@ -1,46 +1,28 @@
package api
import (
"fmt"
"github.com/cortezaproject/corteza-server/pkg/corredor"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/davecgh/go-spew/spew"
"context"
"net/http"
"reflect"
"runtime"
"github.com/go-chi/chi"
)
func debugRoutes(r chi.Routes) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var printRoutes func(chi.Routes, string)
// Debug context
type ctxKeyDebug struct{}
printRoutes = func(r chi.Routes, pfix string) {
routes := r.Routes()
for _, route := range routes {
if route.SubRoutes != nil && len(route.SubRoutes.Routes()) > 0 {
printRoutes(route.SubRoutes, pfix+route.Pattern[:len(route.Pattern)-2])
} else {
for method, fn := range route.Handlers {
fmt.Fprintf(w, "%-8s %-80s -> %s\n", method, pfix+route.Pattern, runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name())
}
}
}
}
printRoutes(r, "")
// Packs remote address to context
func DebugToContext(next http.Handler) http.Handler {
if true {
// debug disabled
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), ctxKeyDebug{}, true)))
})
}
func debugEventbus() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
spew.Fdump(w, eventbus.Service().Debug())
}
}
func debugCorredor() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
spew.Fdump(w, corredor.Service().Debug())
}
// DebugFromContext returns remote IP address from context
func DebugFromContext(ctx context.Context) bool {
return true
debug, ok := ctx.Value(ctxKeyDebug{}).(bool)
return ok && debug
}
+1 -1
View File
@@ -12,7 +12,7 @@ type ctxKeyRemoteAddr int
const remoteAddrKey ctxKeyRemoteAddr = 0
// Packs remote address to context
func remoteAddrToContext(next http.Handler) http.Handler {
func RemoteAddrToContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), remoteAddrKey, req.RemoteAddr)))
})
+169
View File
@@ -0,0 +1,169 @@
package api
// This code is modified version from
// https://github.com/titpetric/factory/tree/master/resputil
//
// Parts of the code are rewritten to allow greater flexibility
// but the general logic stays the same for now
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/errors"
"net/http"
)
type (
successWrap struct {
Success struct {
Message string `json:"message"`
} `json:"success"`
}
CallFrame struct {
Function string `json:"function"`
File string `json:"file"`
Line int `json:"line"`
}
ErrorPayload struct {
Message string `json:"message"`
Context map[string]interface{} `json:"context,omitempty"`
Callstack []*CallFrame `json:"callstack,omitempty"`
}
)
// Success returns a structured success message for API responses
func Success(success ...string) *successWrap {
response := &successWrap{}
response.Success.Message = "OK"
if len(success) > 0 {
response.Success.Message = success[0]
}
return response
}
// OK returns the default Success message
func OK() *successWrap {
return Success()
}
// Writes response, according to type
//
// Primarily this function encodes given payload (directly or indirectly) as compact JSON
//
// In some specific scenarios, when:
// - debug mode is enabled,
// - no explicit accept header with /json mime-type is sent
// and,
//
// if payload is an error:
// it outputs formatted error with extended info
//
// if payload is non-error:
// it outputs formatted and indented JSON
//
func encode(w http.ResponseWriter, r *http.Request, payload interface{}) {
var (
err error
enc = json.NewEncoder(w)
)
switch c := payload.(type) {
case error:
err = c
case *successWrap:
// main key is "success"
if err = enc.Encode(c); err != nil {
err = fmt.Errorf("failed to encode response: %w", err)
}
default:
// main key is "response"
aux := struct {
Response interface{} `json:"response"`
}{c}
if err = enc.Encode(aux); err != nil {
err = fmt.Errorf("failed to encode response: %w", err)
}
}
if err != nil {
if err, is := err.(*errors.Error); is {
// trim out the base stack we don't care about...
_ = err.Apply(errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"))
}
errors.ServeHTTP(w, r, err, DebugFromContext(r.Context()))
}
w.Header().Set("Content-Type", "application/json")
}
// Send handles first non-nil (and non-empty) payload and encodes it or it's results (when fn)
//
// See encode() for details
func Send(w http.ResponseWriter, r *http.Request, rr ...interface{}) {
for _, rsp := range rr {
switch c := rsp.(type) {
case nil:
// this will match a nil error
continue
case *successWrap:
encode(w, r, c)
case func(http.ResponseWriter, *http.Request):
c(w, r)
case func() ([]byte, error):
result, err := c()
Send(w, r, err, result)
case func() (interface{}, error):
result, err := c()
Send(w, r, err, result)
case func() error:
err := c()
if err == nil {
continue
}
encode(w, r, err)
case error:
encode(w, r, c)
case []byte:
if len(c) == 0 {
continue
}
if _, err := w.Write(c); err != nil {
panic(err)
}
return
case string:
if c == "" {
continue
}
encode(w, r, c)
case bool:
if !c {
continue
}
encode(w, r, c)
default:
encode(w, r, c)
}
// Exit on the first output...
return
}
encode(w, r, false)
}
+66
View File
@@ -0,0 +1,66 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"io/ioutil"
"net/http/httptest"
)
func TestTests(t *testing.T) {
testResponse := func(output interface{}) string {
w := httptest.NewRecorder()
r := &http.Request{Header: http.Header{}}
r.Header.Add("accept", "application/json")
Send(w, r, output)
body, _ := ioutil.ReadAll(w.Result().Body)
return string(body)
}
var cc = []struct {
name string
inp interface{}
out string
}{
{"nil", nil, `{"response":false}`},
{"bool true", true, `{"response":true}`},
{"bool false", false, `{"response":false}`},
{"string empty", "", `{"response":false}`},
{"string", "string", `{"response":"string"}`},
{"int zero", 0, `{"response":0}`},
{"int non-zero", 1337, `{"response":1337}`},
{"int sub-zero", -1, `{"response":-1}`},
{"error nil", func() error { return nil }, `{"response":false}`},
{"error", func() error { return fmt.Errorf("error response") }, `{"error":{"message":"error response"}}`},
{"value + error", func() (interface{}, error) { return "string response", fmt.Errorf("error response") }, `{"error":{"message":"error response"}}`},
{"empty value + error", func() (interface{}, error) { return "", fmt.Errorf("error response") }, `{"error":{"message":"error response"}}`},
{"value + empty error", func() (interface{}, error) { return "string response", nil }, `{"response":"string response"}`},
{"success default", Success(), `{"success":{"message":"OK"}}`},
{"ok", OK(), `{"success":{"message":"OK"}}`},
{"success custom", Success("string"), `{"success":{"message":"string"}}`},
{"error stdlib", fmt.Errorf("string"), `{"error":{"message":"string"}}`},
{"error stdlib nil", func() interface{} { return func() error { return nil }() }(), `{"response":false}`},
{"func json nil", func() ([]byte, error) { return json.Marshal(nil) }, `null`},
{"func json false", func() ([]byte, error) { return json.Marshal(false) }, `false`},
{"func json 0", func() ([]byte, error) { return json.Marshal(0) }, `0`},
{"func json empty string", func() ([]byte, error) { return json.Marshal("") }, `""`},
{"func writer/req", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("foo")) }, `foo`},
{"custom struct", struct {
Name string `json:"name"`
}{"Corteza"}, `{"response":{"name":"Corteza"}}`},
}
for _, c := range cc {
t.Run(c.name, func(t *testing.T) {
got := strings.TrimSpace(testResponse(c.inp))
if got != strings.TrimSpace(c.out) {
t.Errorf("got %#v, expected %#v", got, c.out)
}
})
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
package api
package server
import (
"net/http"
+46
View File
@@ -0,0 +1,46 @@
package server
import (
"fmt"
"github.com/cortezaproject/corteza-server/pkg/corredor"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/davecgh/go-spew/spew"
"net/http"
"reflect"
"runtime"
"github.com/go-chi/chi"
)
func debugRoutes(r chi.Routes) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
var printRoutes func(chi.Routes, string)
printRoutes = func(r chi.Routes, pfix string) {
routes := r.Routes()
for _, route := range routes {
if route.SubRoutes != nil && len(route.SubRoutes.Routes()) > 0 {
printRoutes(route.SubRoutes, pfix+route.Pattern[:len(route.Pattern)-2])
} else {
for method, fn := range route.Handlers {
fmt.Fprintf(w, "%-8s %-80s -> %s\n", method, pfix+route.Pattern, runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name())
}
}
}
}
printRoutes(r, "")
}
}
func debugEventbus() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
spew.Fdump(w, eventbus.Service().Debug())
}
}
func debugCorredor() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
spew.Fdump(w, corredor.Service().Debug())
}
}
@@ -1,14 +1,12 @@
package api
package server
import (
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/go-chi/chi/middleware"
"go.uber.org/zap"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/middleware"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/pkg/logger"
)
// contextLogger middleware binds logger to request's context.
@@ -1,4 +1,4 @@
package api
package server
import (
"net/http"
@@ -1,6 +1,7 @@
package api
package server
import (
"github.com/cortezaproject/corteza-server/pkg/api"
"net/http"
"os"
"runtime/debug"
@@ -17,7 +18,7 @@ func BaseMiddleware(log *zap.Logger) []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
handleCORS,
middleware.RealIP,
remoteAddrToContext,
api.RemoteAddrToContext,
middleware.RequestID,
contextLogger(log),
}
+12 -17
View File
@@ -1,19 +1,17 @@
package api
package server
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/healthcheck"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/cortezaproject/corteza-server/pkg/version"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"go.uber.org/zap"
"net"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/options"
)
type (
@@ -41,14 +39,6 @@ func (s *server) MountRoutes(mm ...func(chi.Router)) {
func (s server) Serve(ctx context.Context) {
s.log.Info("Starting HTTP server with REST API", zap.String("address", s.httpOpt.Addr))
// configure resputil options
resputil.SetConfig(resputil.Options{
Trace: s.httpOpt.Tracing,
Logger: func(err error) {
// @todo: error logging
},
})
listener, err := net.Listen("tcp", s.httpOpt.Addr)
if err != nil {
s.log.Error("Can not start server", zap.Error(err))
@@ -139,7 +129,12 @@ func (s server) bindMiscRoutes(router chi.Router) {
}
if s.httpOpt.EnableVersionRoute {
router.Get("/version", version.HttpHandler)
router.Get("/version", func(w http.ResponseWriter, r *http.Request) {
api.Send(w, r, struct {
BuildTime string `json:"buildTime"`
Version string `json:"version"`
}{version.BuildTime, version.Version})
})
}
if s.httpOpt.EnableHealthcheckRoute {
@@ -1,4 +1,4 @@
package api
package server
import (
"context"