From 9357280fce5c5d57111d690a336cca5f89e34da6 Mon Sep 17 00:00:00 2001 From: ryanc414 Date: Wed, 21 Apr 2021 14:51:30 +0100 Subject: [PATCH] rpc: add HTTPError type for HTTP error responses (#22677) The new error type is returned by client operations contains details of the response error code and response body. Co-authored-by: Felix Lange --- rpc/errors.go | 29 +++++++++++++++++++++++++++++ rpc/http.go | 24 +++++++++++++----------- rpc/http_test.go | 39 +++++++++++++++++++++++++++++++++++++++ rpc/types.go | 12 ------------ 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/rpc/errors.go b/rpc/errors.go index dbfde8b19..4c06a745f 100644 --- a/rpc/errors.go +++ b/rpc/errors.go @@ -18,6 +18,35 @@ package rpc import "fmt" +// HTTPError is returned by client operations when the HTTP status code of the +// response is not a 2xx status. +type HTTPError struct { + StatusCode int + Status string + Body []byte +} + +func (err HTTPError) Error() string { + if len(err.Body) == 0 { + return err.Status + } + return fmt.Sprintf("%v: %s", err.Status, err.Body) +} + +// Error wraps RPC errors, which contain an error code in addition to the message. +type Error interface { + Error() string // returns the message + ErrorCode() int // returns the code +} + +// A DataError contains some data in addition to the error message. +type DataError interface { + Error() string // returns the message + ErrorData() interface{} // returns the error data +} + +// Error types defined below are the built-in JSON-RPC errors. + var ( _ Error = new(methodNotFoundError) _ Error = new(subscriptionNotFoundError) diff --git a/rpc/http.go b/rpc/http.go index 87a96e49e..32f4e7d90 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -134,19 +134,11 @@ func DialHTTP(endpoint string) (*Client, error) { func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error { hc := c.writeConn.(*httpConn) respBody, err := hc.doRequest(ctx, msg) - if respBody != nil { - defer respBody.Close() - } - if err != nil { - if respBody != nil { - buf := new(bytes.Buffer) - if _, err2 := buf.ReadFrom(respBody); err2 == nil { - return fmt.Errorf("%v: %v", err, buf.String()) - } - } return err } + defer respBody.Close() + var respmsg jsonrpcMessage if err := json.NewDecoder(respBody).Decode(&respmsg); err != nil { return err @@ -194,7 +186,17 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return resp.Body, errors.New(resp.Status) + var buf bytes.Buffer + var body []byte + if _, err := buf.ReadFrom(resp.Body); err == nil { + body = buf.Bytes() + } + + return nil, HTTPError{ + Status: resp.Status, + StatusCode: resp.StatusCode, + Body: body, + } } return resp.Body, nil } diff --git a/rpc/http_test.go b/rpc/http_test.go index b75af67c5..97f8d44c3 100644 --- a/rpc/http_test.go +++ b/rpc/http_test.go @@ -123,3 +123,42 @@ func TestHTTPRespBodyUnlimited(t *testing.T) { t.Fatalf("response has wrong length %d, want %d", len(r), respLength) } } + +// Tests that an HTTP error results in an HTTPError instance +// being returned with the expected attributes. +func TestHTTPErrorResponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "error has occurred!", http.StatusTeapot) + })) + defer ts.Close() + + c, err := DialHTTP(ts.URL) + if err != nil { + t.Fatal(err) + } + + var r string + err = c.Call(&r, "test_method") + if err == nil { + t.Fatal("error was expected") + } + + httpErr, ok := err.(HTTPError) + if !ok { + t.Fatalf("unexpected error type %T", err) + } + + if httpErr.StatusCode != http.StatusTeapot { + t.Error("unexpected status code", httpErr.StatusCode) + } + if httpErr.Status != "418 I'm a teapot" { + t.Error("unexpected status text", httpErr.Status) + } + if body := string(httpErr.Body); body != "error has occurred!\n" { + t.Error("unexpected body", body) + } + + if errMsg := httpErr.Error(); errMsg != "418 I'm a teapot: error has occurred!\n" { + t.Error("unexpected error message", errMsg) + } +} diff --git a/rpc/types.go b/rpc/types.go index bab1b3957..d1b878c78 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -35,18 +35,6 @@ type API struct { Public bool // indication if the methods must be considered safe for public use } -// Error wraps RPC errors, which contain an error code in addition to the message. -type Error interface { - Error() string // returns the message - ErrorCode() int // returns the code -} - -// A DataError contains some data in addition to the error message. -type DataError interface { - Error() string // returns the message - ErrorData() interface{} // returns the error data -} - // ServerCodec implements reading, parsing and writing RPC messages for the server side of // a RPC session. Implementations must be go-routine safe since the codec can be called in // multiple go-routines concurrently.