Skip to content

Commit

Permalink
binance: parse error responses (#3090)
Browse files Browse the repository at this point in the history
* parse binance error responses

* fix pointer
  • Loading branch information
buck54321 authored Nov 29, 2024
1 parent 504d3a9 commit ccd0221
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 51 deletions.
49 changes: 17 additions & 32 deletions client/mm/libxc/binance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)))
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
15 changes: 15 additions & 0 deletions dex/dexnet/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions dex/dexnet/http_live_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
34 changes: 15 additions & 19 deletions dex/dexnet/http_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

}

0 comments on commit ccd0221

Please sign in to comment.