Skip to content

Commit f30f428

Browse files
porridgekrasi-georgiev
authored andcommitted
Add a way to return the body of a 5xx response. (#484)
expose body when handling HTTP errors Signed-off-by: Marcin Owsiany <[email protected]>
1 parent 16f375c commit f30f428

File tree

2 files changed

+152
-75
lines changed

2 files changed

+152
-75
lines changed

api/prometheus/v1/api.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const (
6060
ErrCanceled = "canceled"
6161
ErrExec = "execution"
6262
ErrBadResponse = "bad_response"
63+
ErrServer = "server_error"
64+
ErrClient = "client_error"
6365

6466
// Possible values for HealthStatus.
6567
HealthGood HealthStatus = "up"
@@ -69,8 +71,9 @@ const (
6971

7072
// Error is an error returned by the API.
7173
type Error struct {
72-
Type ErrorType
73-
Msg string
74+
Type ErrorType
75+
Msg string
76+
Detail string
7477
}
7578

7679
func (e *Error) Error() string {
@@ -460,6 +463,16 @@ func apiError(code int) bool {
460463
return code == statusAPIError || code == http.StatusBadRequest
461464
}
462465

466+
func errorTypeAndMsgFor(resp *http.Response) (ErrorType, string) {
467+
switch resp.StatusCode / 100 {
468+
case 4:
469+
return ErrClient, fmt.Sprintf("client error: %d", resp.StatusCode)
470+
case 5:
471+
return ErrServer, fmt.Sprintf("server error: %d", resp.StatusCode)
472+
}
473+
return ErrBadResponse, fmt.Sprintf("bad response code %d", resp.StatusCode)
474+
}
475+
463476
func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
464477
resp, body, err := c.Client.Do(ctx, req)
465478
if err != nil {
@@ -469,9 +482,11 @@ func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, [
469482
code := resp.StatusCode
470483

471484
if code/100 != 2 && !apiError(code) {
485+
errorType, errorMsg := errorTypeAndMsgFor(resp)
472486
return resp, body, &Error{
473-
Type: ErrBadResponse,
474-
Msg: fmt.Sprintf("bad response code %d", resp.StatusCode),
487+
Type: errorType,
488+
Msg: errorMsg,
489+
Detail: string(body),
475490
}
476491
}
477492

api/prometheus/v1/api_test.go

Lines changed: 133 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v1
1818
import (
1919
"context"
2020
"encoding/json"
21+
"errors"
2122
"fmt"
2223
"net/http"
2324
"net/url"
@@ -30,9 +31,10 @@ import (
3031
)
3132

3233
type apiTest struct {
33-
do func() (interface{}, error)
34-
inErr error
35-
inRes interface{}
34+
do func() (interface{}, error)
35+
inErr error
36+
inStatusCode int
37+
inRes interface{}
3638

3739
reqPath string
3840
reqParam url.Values
@@ -75,7 +77,9 @@ func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Respon
7577
}
7678

7779
resp := &http.Response{}
78-
if test.inErr != nil {
80+
if test.inStatusCode != 0 {
81+
resp.StatusCode = test.inStatusCode
82+
} else if test.inErr != nil {
7983
resp.StatusCode = statusAPIError
8084
} else {
8185
resp.StatusCode = http.StatusOK
@@ -194,6 +198,42 @@ func TestAPIs(t *testing.T) {
194198
},
195199
err: fmt.Errorf("some error"),
196200
},
201+
{
202+
do: doQuery("2", testTime),
203+
inRes: "some body",
204+
inStatusCode: 500,
205+
inErr: &Error{
206+
Type: ErrServer,
207+
Msg: "server error: 500",
208+
Detail: "some body",
209+
},
210+
211+
reqMethod: "GET",
212+
reqPath: "/api/v1/query",
213+
reqParam: url.Values{
214+
"query": []string{"2"},
215+
"time": []string{testTime.Format(time.RFC3339Nano)},
216+
},
217+
err: errors.New("server_error: server error: 500"),
218+
},
219+
{
220+
do: doQuery("2", testTime),
221+
inRes: "some body",
222+
inStatusCode: 404,
223+
inErr: &Error{
224+
Type: ErrClient,
225+
Msg: "client error: 404",
226+
Detail: "some body",
227+
},
228+
229+
reqMethod: "GET",
230+
reqPath: "/api/v1/query",
231+
reqParam: url.Values{
232+
"query": []string{"2"},
233+
"time": []string{testTime.Format(time.RFC3339Nano)},
234+
},
235+
err: errors.New("client_error: client error: 404"),
236+
},
197237

198238
{
199239
do: doQueryRange("2", Range{
@@ -498,29 +538,34 @@ func TestAPIs(t *testing.T) {
498538
var tests []apiTest
499539
tests = append(tests, queryTests...)
500540

501-
for _, test := range tests {
502-
client.curTest = test
503-
504-
res, err := test.do()
505-
506-
if test.err != nil {
507-
if err == nil {
508-
t.Errorf("expected error %q but got none", test.err)
509-
continue
541+
for i, test := range tests {
542+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
543+
client.curTest = test
544+
545+
res, err := test.do()
546+
547+
if test.err != nil {
548+
if err == nil {
549+
t.Fatalf("expected error %q but got none", test.err)
550+
}
551+
if err.Error() != test.err.Error() {
552+
t.Errorf("unexpected error: want %s, got %s", test.err, err)
553+
}
554+
if apiErr, ok := err.(*Error); ok {
555+
if apiErr.Detail != test.inRes {
556+
t.Errorf("%q should be %q", apiErr.Detail, test.inRes)
557+
}
558+
}
559+
return
510560
}
511-
if err.Error() != test.err.Error() {
512-
t.Errorf("unexpected error: want %s, got %s", test.err, err)
561+
if err != nil {
562+
t.Fatalf("unexpected error: %s", err)
513563
}
514-
continue
515-
}
516-
if err != nil {
517-
t.Errorf("unexpected error: %s", err)
518-
continue
519-
}
520564

521-
if !reflect.DeepEqual(res, test.res) {
522-
t.Errorf("unexpected result: want %v, got %v", test.res, res)
523-
}
565+
if !reflect.DeepEqual(res, test.res) {
566+
t.Errorf("unexpected result: want %v, got %v", test.res, res)
567+
}
568+
})
524569
}
525570
}
526571

@@ -532,10 +577,10 @@ type testClient struct {
532577
}
533578

534579
type apiClientTest struct {
535-
code int
536-
response interface{}
537-
expected string
538-
err *Error
580+
code int
581+
response interface{}
582+
expectedBody string
583+
expectedErr *Error
539584
}
540585

541586
func (c *testClient) URL(ep string, args map[string]string) *url.URL {
@@ -575,98 +620,108 @@ func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response,
575620
func TestAPIClientDo(t *testing.T) {
576621
tests := []apiClientTest{
577622
{
623+
code: statusAPIError,
578624
response: &apiResponse{
579625
Status: "error",
580626
Data: json.RawMessage(`null`),
581627
ErrorType: ErrBadData,
582628
Error: "failed",
583629
},
584-
err: &Error{
630+
expectedErr: &Error{
585631
Type: ErrBadData,
586632
Msg: "failed",
587633
},
588-
code: statusAPIError,
589-
expected: `null`,
634+
expectedBody: `null`,
590635
},
591636
{
637+
code: statusAPIError,
592638
response: &apiResponse{
593639
Status: "error",
594640
Data: json.RawMessage(`"test"`),
595641
ErrorType: ErrTimeout,
596642
Error: "timed out",
597643
},
598-
err: &Error{
644+
expectedErr: &Error{
599645
Type: ErrTimeout,
600646
Msg: "timed out",
601647
},
602-
code: statusAPIError,
603-
expected: `test`,
648+
expectedBody: `test`,
604649
},
605650
{
606-
response: "bad json",
607-
err: &Error{
608-
Type: ErrBadResponse,
609-
Msg: "bad response code 500",
651+
code: http.StatusInternalServerError,
652+
response: "500 error details",
653+
expectedErr: &Error{
654+
Type: ErrServer,
655+
Msg: "server error: 500",
656+
Detail: "500 error details",
657+
},
658+
},
659+
{
660+
code: http.StatusNotFound,
661+
response: "404 error details",
662+
expectedErr: &Error{
663+
Type: ErrClient,
664+
Msg: "client error: 404",
665+
Detail: "404 error details",
610666
},
611-
code: http.StatusInternalServerError,
612667
},
613668
{
669+
code: http.StatusBadRequest,
614670
response: &apiResponse{
615671
Status: "error",
616672
Data: json.RawMessage(`null`),
617673
ErrorType: ErrBadData,
618674
Error: "end timestamp must not be before start time",
619675
},
620-
err: &Error{
676+
expectedErr: &Error{
621677
Type: ErrBadData,
622678
Msg: "end timestamp must not be before start time",
623679
},
624-
code: http.StatusBadRequest,
625680
},
626681
{
682+
code: statusAPIError,
627683
response: "bad json",
628-
err: &Error{
684+
expectedErr: &Error{
629685
Type: ErrBadResponse,
630686
Msg: "invalid character 'b' looking for beginning of value",
631687
},
632-
code: statusAPIError,
633688
},
634689
{
690+
code: statusAPIError,
635691
response: &apiResponse{
636692
Status: "success",
637693
Data: json.RawMessage(`"test"`),
638694
},
639-
err: &Error{
695+
expectedErr: &Error{
640696
Type: ErrBadResponse,
641697
Msg: "inconsistent body for response code",
642698
},
643-
code: statusAPIError,
644699
},
645700
{
701+
code: statusAPIError,
646702
response: &apiResponse{
647703
Status: "success",
648704
Data: json.RawMessage(`"test"`),
649705
ErrorType: ErrTimeout,
650706
Error: "timed out",
651707
},
652-
err: &Error{
708+
expectedErr: &Error{
653709
Type: ErrBadResponse,
654710
Msg: "inconsistent body for response code",
655711
},
656-
code: statusAPIError,
657712
},
658713
{
714+
code: http.StatusOK,
659715
response: &apiResponse{
660716
Status: "error",
661717
Data: json.RawMessage(`"test"`),
662718
ErrorType: ErrTimeout,
663719
Error: "timed out",
664720
},
665-
err: &Error{
721+
expectedErr: &Error{
666722
Type: ErrBadResponse,
667723
Msg: "inconsistent body for response code",
668724
},
669-
code: http.StatusOK,
670725
},
671726
}
672727

@@ -677,30 +732,37 @@ func TestAPIClientDo(t *testing.T) {
677732
}
678733
client := &apiClient{tc}
679734

680-
for _, test := range tests {
681-
682-
tc.ch <- test
683-
684-
_, body, err := client.Do(context.Background(), tc.req)
685-
686-
if test.err != nil {
687-
if err == nil {
688-
t.Errorf("expected error %q but got none", test.err)
689-
continue
735+
for i, test := range tests {
736+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
737+
738+
tc.ch <- test
739+
740+
_, body, err := client.Do(context.Background(), tc.req)
741+
742+
if test.expectedErr != nil {
743+
if err == nil {
744+
t.Fatalf("expected error %q but got none", test.expectedErr)
745+
}
746+
if test.expectedErr.Error() != err.Error() {
747+
t.Errorf("unexpected error: want %q, got %q", test.expectedErr, err)
748+
}
749+
if test.expectedErr.Detail != "" {
750+
apiErr := err.(*Error)
751+
if apiErr.Detail != test.expectedErr.Detail {
752+
t.Errorf("unexpected error details: want %q, got %q", test.expectedErr.Detail, apiErr.Detail)
753+
}
754+
}
755+
return
690756
}
691-
if test.err.Error() != err.Error() {
692-
t.Errorf("unexpected error: want %q, got %q", test.err, err)
757+
if err != nil {
758+
t.Fatalf("unexpeceted error %s", err)
693759
}
694-
continue
695-
}
696-
if err != nil {
697-
t.Errorf("unexpeceted error %s", err)
698-
continue
699-
}
700760

701-
want, got := test.expected, string(body)
702-
if want != got {
703-
t.Errorf("unexpected body: want %q, got %q", want, got)
704-
}
761+
want, got := test.expectedBody, string(body)
762+
if want != got {
763+
t.Errorf("unexpected body: want %q, got %q", want, got)
764+
}
765+
})
766+
705767
}
706768
}

0 commit comments

Comments
 (0)