diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 604a84dd6da..744a8f0ab45 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -239,6 +239,22 @@ func Err(err error) *NormalResponse { return resp } +// ErrOrFallback uses the information in an errutil.Error if available +// and otherwise falls back to the status and message provided as +// arguments. +// +// The signature is equivalent to that of Error which allows us to +// rename this to Error when we're confident that that would be safe to +// do. +func ErrOrFallback(status int, message string, err error) *NormalResponse { + grafanaErr := &errutil.Error{} + if errors.As(err, grafanaErr) { + return Err(err) + } + + return Error(status, message, err) +} + // Empty creates an empty NormalResponse. func Empty(status int) *NormalResponse { return Respond(status, nil) diff --git a/pkg/api/response/response_test.go b/pkg/api/response/response_test.go new file mode 100644 index 00000000000..67658cb497a --- /dev/null +++ b/pkg/api/response/response_test.go @@ -0,0 +1,127 @@ +package response + +import ( + "errors" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/util/errutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestErrors(t *testing.T) { + const fakeNotFoundMessage = "I looked, but did not find the thing" + const genericErrorMessage = "Something went wrong in parsing the request" + + cases := []struct { + name string + + // inputs + err error + statusCode int + message string + + // responses + legacyResponse *NormalResponse + newResponse *NormalResponse + fallbackUseNew bool + compareErr bool + }{ + { + name: "base case", + + legacyResponse: &NormalResponse{}, + newResponse: &NormalResponse{ + status: http.StatusInternalServerError, + }, + }, + { + name: "not found error", + + err: errors.New("not found"), + statusCode: http.StatusNotFound, + message: fakeNotFoundMessage, + + legacyResponse: &NormalResponse{ + status: http.StatusNotFound, + errMessage: fakeNotFoundMessage, + }, + newResponse: &NormalResponse{ + status: http.StatusInternalServerError, + }, + }, + { + name: "grafana error with fallback to other error", + + err: errutil.NewBase(errutil.StatusTimeout, "thing.timeout").Errorf("whoops"), + statusCode: http.StatusBadRequest, + message: genericErrorMessage, + + legacyResponse: &NormalResponse{ + status: http.StatusBadRequest, + errMessage: genericErrorMessage, + }, + newResponse: &NormalResponse{ + status: http.StatusGatewayTimeout, + errMessage: errutil.StatusTimeout.String(), + }, + fallbackUseNew: true, + }, + } + + compareResponses := func(expected *NormalResponse, actual *NormalResponse, compareErr bool) func(t *testing.T) { + return func(t *testing.T) { + if expected == nil { + require.Nil(t, actual) + return + } + + require.NotNil(t, actual) + assert.Equal(t, expected.status, actual.status) + if expected.body != nil { + assert.Equal(t, expected.body.Bytes(), actual.body.Bytes()) + } + if expected.header != nil { + assert.EqualValues(t, expected.header, actual.header) + } + assert.Equal(t, expected.errMessage, actual.errMessage) + if compareErr { + assert.ErrorIs(t, expected.err, actual.err) + } + } + } + + for _, tc := range cases { + tc := tc + t.Run( + tc.name+" Error", + compareResponses(tc.legacyResponse, Error( + tc.statusCode, + tc.message, + tc.err, + ), tc.compareErr), + ) + + t.Run( + tc.name+" Err", + compareResponses(tc.newResponse, Err( + tc.err, + ), tc.compareErr), + ) + + fallbackResponse := tc.legacyResponse + if tc.fallbackUseNew { + fallbackResponse = tc.newResponse + } + t.Run( + tc.name+" ErrOrFallback", + compareResponses(fallbackResponse, ErrOrFallback( + tc.statusCode, + tc.message, + tc.err, + ), tc.compareErr), + ) + } +} diff --git a/pkg/util/errutil/status.go b/pkg/util/errutil/status.go index 379c23e54c3..a1d02f2eae1 100644 --- a/pkg/util/errutil/status.go +++ b/pkg/util/errutil/status.go @@ -110,6 +110,10 @@ func (s CoreStatus) LogLevel() LogLevel { } } +func (s CoreStatus) String() string { + return string(s) +} + // ProxyStatus implies that an error originated from the data source // proxy. type ProxyStatus CoreStatus