QueryService: Return application/json and better errors (#84234)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user