github.com/status-im/status-go@v1.1.0/services/wallet/thirdparty/opensea/http_client.go (about)

     1  package opensea
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/ethereum/go-ethereum/log"
    12  )
    13  
    14  const requestTimeout = 5 * time.Second
    15  const getRequestRetryMaxCount = 15
    16  const getRequestWaitTime = 300 * time.Millisecond
    17  
    18  type HTTPClient struct {
    19  	client         *http.Client
    20  	getRequestLock sync.RWMutex
    21  }
    22  
    23  func NewHTTPClient() *HTTPClient {
    24  	return &HTTPClient{
    25  		client: &http.Client{
    26  			Timeout: requestTimeout,
    27  		},
    28  	}
    29  }
    30  
    31  func (o *HTTPClient) doGetRequest(ctx context.Context, url string, apiKey string) ([]byte, error) {
    32  	// Ensure only one thread makes a request at a time
    33  	o.getRequestLock.Lock()
    34  	defer o.getRequestLock.Unlock()
    35  
    36  	retryCount := 0
    37  	statusCode := http.StatusOK
    38  
    39  	// Try to do the request without an apiKey first
    40  	tmpAPIKey := ""
    41  
    42  	for {
    43  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    44  		if err != nil {
    45  			return nil, err
    46  		}
    47  
    48  		req.Header.Set("Content-Type", "application/json")
    49  		req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0")
    50  		if len(tmpAPIKey) > 0 {
    51  			req.Header.Set("X-API-KEY", tmpAPIKey)
    52  		}
    53  
    54  		resp, err := o.client.Do(req)
    55  		if err != nil {
    56  			return nil, err
    57  		}
    58  		defer func() {
    59  			if err := resp.Body.Close(); err != nil {
    60  				log.Error("failed to close opensea request body", "err", err)
    61  			}
    62  		}()
    63  
    64  		statusCode = resp.StatusCode
    65  		switch resp.StatusCode {
    66  		case http.StatusOK:
    67  			body, err := ioutil.ReadAll(resp.Body)
    68  			return body, err
    69  		case http.StatusBadRequest:
    70  			// The OpenSea v2 API will return error 400 if the account holds no collectibles on
    71  			// the requested chain. This shouldn't be treated as an error, return an empty body.
    72  			return nil, nil
    73  		case http.StatusTooManyRequests:
    74  			if retryCount < getRequestRetryMaxCount {
    75  				// sleep and retry
    76  				time.Sleep(getRequestWaitTime)
    77  				retryCount++
    78  				continue
    79  			}
    80  			// break and error
    81  		case http.StatusForbidden:
    82  			// Request requires an apiKey, set it and retry
    83  			if tmpAPIKey == "" && apiKey != "" {
    84  				tmpAPIKey = apiKey
    85  				// sleep and retry
    86  				time.Sleep(getRequestWaitTime)
    87  				continue
    88  			}
    89  			// break and error
    90  		default:
    91  			// break and error
    92  		}
    93  		break
    94  	}
    95  	return nil, fmt.Errorf("unsuccessful request: %d %s", statusCode, http.StatusText(statusCode))
    96  }