Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

binance: parse error responses #3090

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}

}
Loading