Errors: Add HTTP writer for errutil.Error (#57661)

This commit is contained in:
Emil Tullstedt 2022-10-26 16:57:53 +02:00 committed by GitHub
parent 05872283b5
commit 1e2b7c5368
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 129 additions and 0 deletions

View File

@ -0,0 +1,100 @@
package errhttp
import (
"context"
"encoding/json"
"errors"
"net/http"
"reflect"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/util/errutil"
)
var ErrNonGrafanaError = errutil.NewBase(errutil.StatusInternal, "core.MalformedError")
var defaultLogger = log.New("request-errors")
// ErrorOptions is a container for functional options passed to [Write].
type ErrorOptions struct {
fallback *errutil.Error
logger log.Logger
}
// Write writes an error to the provided [http.ResponseWriter] with the
// appropriate HTTP status and JSON payload from [errutil.Error].
// Write also logs the provided error to either the contextlogger,
// the "request-errors" logger, or the logger provided as a functional
// option using [WithLogger].
// When passing errors that are not [errors.As] compatible with
// [errutil.Error], [ErrNonGrafanaError] will be used to create a
// generic 500 Internal Server Error payload by default, this is
// overrideable by providing [WithFallback] for a custom fallback
// error.
func Write(ctx context.Context, err error, w http.ResponseWriter, opts ...func(ErrorOptions) ErrorOptions) {
opt := ErrorOptions{}
for _, o := range opts {
opt = o(opt)
}
var gErr errutil.Error
if !errors.As(err, &gErr) {
gErr = fallbackOrInternalError(err, opt)
}
logError(ctx, gErr, opt)
pub := gErr.Public()
w.WriteHeader(pub.StatusCode)
w.Header().Add("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(pub)
if err != nil {
defaultLogger.FromContext(ctx).Error("error while writing error", "error", err)
}
}
// WithFallback sets the default error returned to the user if the error
// sent to [Write] is not an [errutil.Error].
func WithFallback(opt ErrorOptions, fallback errutil.Error) ErrorOptions {
opt.fallback = &fallback
return opt
}
// WithLogger sets the logger that [Write] should write log output on.
func WithLogger(opt ErrorOptions, logger log.Logger) ErrorOptions {
opt.logger = logger
return opt
}
func logError(ctx context.Context, e errutil.Error, opt ErrorOptions) {
var logger log.Logger = defaultLogger
if reqCtx := contexthandler.FromContext(ctx); reqCtx != nil && reqCtx.Logger != nil {
logger = reqCtx.Logger
}
if opt.logger != nil {
logger = opt.logger
}
kv := []any{
"messageID", e.MessageID,
"error", e.LogMessage,
}
if e.Underlying != nil {
kv = append(kv, "underlying", e.Underlying)
}
e.LogLevel.LogFunc(logger.FromContext(ctx))(
"Request error",
kv...,
)
}
func fallbackOrInternalError(err error, opt ErrorOptions) errutil.Error {
if opt.fallback != nil {
fErr := *opt.fallback
fErr.Underlying = err
return fErr
}
return ErrNonGrafanaError.Errorf("unexpected error type [%s]: %w", reflect.TypeOf(err), err)
}

View File

@ -0,0 +1,29 @@
package errhttp
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/util/errutil"
)
func TestWrite(t *testing.T) {
ctx := context.Background()
const msgID = "test.thisIsExpected"
base := errutil.NewBase(errutil.StatusTimeout, msgID)
handler := func(writer http.ResponseWriter, request *http.Request) {
Write(ctx, base.Errorf("got expected error"), writer)
}
req := httptest.NewRequest("GET", "http://localhost:3000/fake", nil)
recorder := httptest.NewRecorder()
handler(recorder, req)
assert.Equal(t, http.StatusGatewayTimeout, recorder.Code)
assert.JSONEq(t, `{"message": "Timeout", "messageId": "test.thisIsExpected", "statusCode": 504}`, recorder.Body.String())
}