From 90fbd8b6f328f5f4ab24c5e4ac1e711cc5ecb57b Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Mon, 18 Nov 2024 06:15:17 -0600 Subject: [PATCH 1/2] parse binance error responses --- client/mm/libxc/binance.go | 49 +++++++++++++----------------------- dex/dexnet/http.go | 15 +++++++++++ dex/dexnet/http_live_test.go | 32 +++++++++++++++++++++++ dex/dexnet/http_test.go | 34 +++++++++++-------------- 4 files changed, 79 insertions(+), 51 deletions(-) create mode 100644 dex/dexnet/http_live_test.go diff --git a/client/mm/libxc/binance.go b/client/mm/libxc/binance.go index 739d8621bf..e340819789 100644 --- a/client/mm/libxc/binance.go +++ b/client/mm/libxc/binance.go @@ -1064,12 +1064,7 @@ func (bnc *binance) CancelTrade(ctx context.Context, baseID, quoteID uint32, tra v.Add("symbol", slug) v.Add("origClientOrderId", tradeID) - req, err := bnc.generateRequest(ctx, "DELETE", "/api/v3/order", v, nil, true, true) - if err != nil { - return err - } - - return requestInto(req, &struct{}{}) + return bnc.request(ctx, "DELETE", "/api/v3/order", v, nil, true, true, nil) } func (bnc *binance) Balances(ctx context.Context) (map[uint32]*ExchangeBalance, error) { @@ -1195,22 +1190,14 @@ func (bnc *binance) MatchedMarkets(ctx context.Context) (_ []*MarketMatch, err e } func (bnc *binance) getAPI(ctx context.Context, endpoint string, query url.Values, key, sign bool, thing interface{}) error { - req, err := bnc.generateRequest(ctx, http.MethodGet, endpoint, query, nil, key, sign) - if err != nil { - return fmt.Errorf("generateRequest error: %w", err) - } - return requestInto(req, thing) + return bnc.request(ctx, http.MethodGet, endpoint, query, nil, key, sign, thing) } func (bnc *binance) postAPI(ctx context.Context, endpoint string, query, form url.Values, key, sign bool, thing interface{}) error { - req, err := bnc.generateRequest(ctx, http.MethodPost, endpoint, query, form, key, sign) - if err != nil { - return fmt.Errorf("generateRequest error: %w", err) - } - return requestInto(req, thing) + return bnc.request(ctx, http.MethodPost, endpoint, query, form, key, sign, thing) } -func (bnc *binance) generateRequest(ctx context.Context, method, endpoint string, query, form url.Values, key, sign bool) (*http.Request, error) { +func (bnc *binance) request(ctx context.Context, method, endpoint string, query, form url.Values, key, sign bool, thing interface{}) error { var fullURL string if strings.Contains(endpoint, "sapi") { fullURL = bnc.accountsURL + endpoint @@ -1240,7 +1227,7 @@ func (bnc *binance) generateRequest(ctx context.Context, method, endpoint string raw := queryString + bodyString mac := hmac.New(sha256.New, []byte(bnc.secretKey)) if _, err := mac.Write([]byte(raw)); err != nil { - return nil, fmt.Errorf("hmax Write error: %w", err) + return fmt.Errorf("hmax Write error: %w", err) } v := url.Values{} v.Set("signature", hex.EncodeToString(mac.Sum(nil))) @@ -1256,12 +1243,21 @@ func (bnc *binance) generateRequest(ctx context.Context, method, endpoint string req, err := http.NewRequestWithContext(ctx, method, fullURL, body) if err != nil { - return nil, fmt.Errorf("NewRequestWithContext error: %w", err) + return fmt.Errorf("NewRequestWithContext error: %w", err) } req.Header = header - return req, nil + // bnc.log.Tracef("Sending request: %+v", req) + var errPayload struct { + Code int `json:"code"` + Msg string `json:"msg"` + } + if err := dexnet.Do(req, thing, dexnet.WithSizeLimit(1<<24), dexnet.WithErrorParsing(errPayload)); err != nil { + bnc.log.Errorf("request error from endpoint %q with query = %q, body = %q", endpoint, queryString, bodyString) + return fmt.Errorf("%w, bn code = %d, msg = %q", err, errPayload.Code, errPayload.Msg) + } + return nil } func (bnc *binance) handleOutboundAccountPosition(update *bntypes.StreamUpdate) { @@ -1487,13 +1483,7 @@ func (bnc *binance) getUserDataStream(ctx context.Context) (err error) { q := make(url.Values) q.Add("listenKey", bnc.listenKey.Load().(string)) // Doing a PUT on a listenKey will extend its validity for 60 minutes. - req, err := bnc.generateRequest(ctx, http.MethodPut, "/api/v3/userDataStream", q, nil, true, false) - if err != nil { - bnc.log.Errorf("Error generating keep-alive request: %v. Trying again in 10 seconds.", err) - retryKeepAlive = time.After(time.Second * 10) - return - } - if err := requestInto(req, nil); err != nil { + if err := bnc.request(ctx, http.MethodPut, "/api/v3/userDataStream", q, nil, true, false, nil); err != nil { bnc.log.Errorf("Error sending keep-alive request: %v. Trying again in 10 seconds", err) retryKeepAlive = time.After(time.Second * 10) return @@ -2139,8 +2129,3 @@ func binanceMarketToDexMarkets(binanceBaseSymbol, binanceQuoteSymbol string, tok return markets } - -func requestInto(req *http.Request, thing interface{}) error { - // bnc.log.Tracef("Sending request: %+v", req) - return dexnet.Do(req, thing, dexnet.WithSizeLimit(1<<24)) -} diff --git a/dex/dexnet/http.go b/dex/dexnet/http.go index c650b5745b..26759eff87 100644 --- a/dex/dexnet/http.go +++ b/dex/dexnet/http.go @@ -19,6 +19,7 @@ type RequestOption struct { responseSizeLimit int64 statusFunc func(int) header *[2]string + errThing interface{} } // WithSizeLimit sets a size limit for a response. See defaultResponseSizeLimit @@ -39,6 +40,11 @@ func WithRequestHeader(k, v string) *RequestOption { return &RequestOption{header: &h} } +// WithErrorParsing adds parsing of response bodies for HTTP error responses. +func WithErrorParsing(thing interface{}) *RequestOption { + return &RequestOption{errThing: thing} +} + // Post peforms an HTTP POST request. If thing is non-nil, the response will // be JSON-unmarshaled into thing. func Post(ctx context.Context, uri string, thing interface{}, body []byte, opts ...*RequestOption) error { @@ -67,6 +73,7 @@ func Get(ctx context.Context, uri string, thing interface{}, opts ...*RequestOpt func Do(req *http.Request, thing interface{}, opts ...*RequestOption) error { var sizeLimit int64 = defaultResponseSizeLimit var statusFunc func(int) + var errThing interface{} for _, opt := range opts { switch { case opt.responseSizeLimit > 0: @@ -77,6 +84,8 @@ func Do(req *http.Request, thing interface{}, opts ...*RequestOption) error { h := *opt.header k, v := h[0], h[1] req.Header.Add(k, v) + case opt.errThing != nil: + errThing = opt.errThing } } resp, err := http.DefaultClient.Do(req) @@ -88,6 +97,12 @@ func Do(req *http.Request, thing interface{}, opts ...*RequestOption) error { statusFunc(resp.StatusCode) } if resp.StatusCode != http.StatusOK { + if errThing != nil { + reader := io.LimitReader(resp.Body, sizeLimit) + if err = json.NewDecoder(reader).Decode(errThing); err != nil { + return fmt.Errorf("HTTP error: %q (code %d). error encountered parsing error body: %w", resp.Status, resp.StatusCode, err) + } + } return fmt.Errorf("HTTP error: %q (code %d)", resp.Status, resp.StatusCode) } if thing == nil { diff --git a/dex/dexnet/http_live_test.go b/dex/dexnet/http_live_test.go new file mode 100644 index 0000000000..bd2a0ff1e9 --- /dev/null +++ b/dex/dexnet/http_live_test.go @@ -0,0 +1,32 @@ +//go:build live + +package dexnet + +import ( + "context" + "net/http" + "testing" +) + +func TestGet(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + uri := "https://dcrdata.decred.org/api/block/best" + var resp struct { + Height int64 `json:"height"` + } + var code int + if err := Get(ctx, uri, &resp, WithStatusFunc(func(c int) { code = c })); err != nil { + t.Fatalf("Get error: %v", err) + } + if resp.Height == 0 { + t.Fatal("Height not parsed") + } + if code != http.StatusOK { + t.Fatalf("expected code 200, got %d", code) + } + // Check size limit + if err := Get(ctx, uri, &resp, WithSizeLimit(1)); err == nil { + t.Fatal("Didn't get parse error for low size limit") + } +} diff --git a/dex/dexnet/http_test.go b/dex/dexnet/http_test.go index 4fe4b14fd6..44b278f6ed 100644 --- a/dex/dexnet/http_test.go +++ b/dex/dexnet/http_test.go @@ -1,33 +1,29 @@ -//go:build live - package dexnet import ( "context" "net/http" + "net/http/httptest" "testing" ) -func TestGet(t *testing.T) { +func TestErrorParsing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - uri := "https://dcrdata.decred.org/api/block/best" - var resp struct { - Height int64 `json:"height"` - } - var code int - if err := Get(ctx, uri, &resp, WithStatusFunc(func(c int) { code = c })); err != nil { - t.Fatalf("Get error: %v", err) - } - if resp.Height == 0 { - t.Fatal("Height not parsed") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, `{"code": -150, "msg": "you messed up, bruh"}`, http.StatusBadRequest) + })) + defer ts.Close() + + var errPayload struct { + Code int `json:"code"` + Msg string `json:"msg"` } - if code != http.StatusOK { - t.Fatalf("expected code 200, got %d", code) + if err := Get(ctx, ts.URL, nil, WithErrorParsing(&errPayload)); err == nil { + t.Fatal("didn't get an http error") } - // Check size limit - if err := Get(ctx, uri, &resp, WithSizeLimit(1)); err == nil { - t.Fatal("Didn't get parse error for low size limit") + if errPayload.Code != -150 || errPayload.Msg != "you messed up, bruh" { + t.Fatal("unexpected error body") } - } From 521e2c52e9a550a77237e0017dc71c4cad8c5134 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 20 Nov 2024 16:19:03 -0600 Subject: [PATCH 2/2] fix pointer --- client/mm/libxc/binance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mm/libxc/binance.go b/client/mm/libxc/binance.go index e340819789..3539c02f73 100644 --- a/client/mm/libxc/binance.go +++ b/client/mm/libxc/binance.go @@ -1253,7 +1253,7 @@ func (bnc *binance) request(ctx context.Context, method, endpoint string, query, Code int `json:"code"` Msg string `json:"msg"` } - if err := dexnet.Do(req, thing, dexnet.WithSizeLimit(1<<24), dexnet.WithErrorParsing(errPayload)); err != nil { + if err := dexnet.Do(req, thing, dexnet.WithSizeLimit(1<<24), dexnet.WithErrorParsing(&errPayload)); err != nil { bnc.log.Errorf("request error from endpoint %q with query = %q, body = %q", endpoint, queryString, bodyString) return fmt.Errorf("%w, bn code = %d, msg = %q", err, errPayload.Code, errPayload.Msg) }