QueryService: Return application/json and better errors (#84234)

This commit is contained in:
Ryan McKinley
2024-03-19 15:52:15 +02:00
committed by GitHub
parent d1f791cf1f
commit e27c08cfa9
10 changed files with 293 additions and 92 deletions
+51 -1
View File
@@ -7,6 +7,9 @@ import (
"net/http"
"reflect"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util/errutil"
)
@@ -20,6 +23,14 @@ type ErrorOptions struct {
logger log.Logger
}
type k8sError struct {
metav1.Status `json:",inline"`
// Internal values that do not have a clean home in the standard Status object
MessageID string `json:"messageId"`
Extra map[string]any `json:"extra,omitempty"`
}
// 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 "request-errors"
@@ -43,10 +54,49 @@ func Write(ctx context.Context, err error, w http.ResponseWriter, opts ...func(E
logError(ctx, gErr, opt)
var rsp any
pub := gErr.Public()
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(pub.StatusCode)
err = json.NewEncoder(w).Encode(pub)
rsp = pub
// When running in k8s, this will return a v1 status
// Typically, k8s handlers should directly support error negotiation, however
// when implementing handlers directly this will maintain compatibility with client-go
info, ok := request.RequestInfoFrom(ctx)
if ok {
status := &k8sError{
Status: metav1.Status{
Status: metav1.StatusFailure,
Code: int32(pub.StatusCode),
Message: pub.Message,
Details: &metav1.StatusDetails{
Name: info.Name,
Group: info.APIGroup,
},
},
// Add the internal values into
MessageID: pub.MessageID,
Extra: pub.Extra,
}
switch pub.StatusCode {
case 400:
status.Reason = metav1.StatusReasonBadRequest
case 401:
status.Reason = metav1.StatusReasonUnauthorized
case 403:
status.Reason = metav1.StatusReasonForbidden
case 404:
status.Reason = metav1.StatusReasonNotFound
case 500: // many reasons things could map here
status.Reason = metav1.StatusReasonInternalError
case 504:
status.Reason = metav1.StatusReasonTimeout
}
rsp = status
}
err = json.NewEncoder(w).Encode(rsp)
if err != nil {
defaultLogger.FromContext(ctx).Error("error while writing error", "error", err)
}
+27 -5
View File
@@ -7,15 +7,39 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/util/errutil"
)
func TestWrite(t *testing.T) {
ctx := context.Background()
// Error without k8s context
recorder := doError(t, context.Background())
assert.Equal(t, http.StatusGatewayTimeout, recorder.Code)
assert.JSONEq(t, `{"message": "Timeout", "messageId": "test.thisIsExpected", "statusCode": 504}`, recorder.Body.String())
// Another request, but within the k8s framework
recorder = doError(t, request.WithRequestInfo(context.Background(), &request.RequestInfo{
APIGroup: "TestGroup",
}))
assert.Equal(t, http.StatusGatewayTimeout, recorder.Code)
assert.JSONEq(t, `{
"status": "Failure",
"reason": "Timeout",
"metadata": {},
"messageId": "test.thisIsExpected",
"message": "Timeout",
"details": { "group": "TestGroup" },
"code": 504
}`, recorder.Body.String())
}
func doError(t *testing.T, ctx context.Context) *httptest.ResponseRecorder {
t.Helper()
const msgID = "test.thisIsExpected"
base := errutil.Timeout(msgID)
handler := func(writer http.ResponseWriter, request *http.Request) {
handler := func(writer http.ResponseWriter, _ *http.Request) {
Write(ctx, base.Errorf("got expected error"), writer)
}
@@ -23,7 +47,5 @@ func TestWrite(t *testing.T) {
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())
return recorder
}