Improve error creation & handling by API
This commit is contained in:
+17
-35
@@ -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
@@ -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)))
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
package api
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user