github.com/square/finch@v0.0.0-20240412205204-6530c03e2b96/proto/proto.go (about) 1 // Copyright 2023 Block, Inc. 2 3 package proto 4 5 import ( 6 "bytes" 7 "context" 8 "encoding/json" 9 "errors" 10 "io" 11 "log" 12 "net/http" 13 "net/url" 14 "strings" 15 "time" 16 17 "github.com/square/finch" 18 ) 19 20 var ErrFailed = errors.New("request failed after attempts, or context cancelled") 21 22 type R struct { 23 Timeout time.Duration 24 Wait time.Duration 25 Tries int 26 } 27 28 type Client struct { 29 name string 30 serverAddr string 31 // -- 32 client *http.Client 33 StageId string 34 PrintErrors bool 35 } 36 37 func NewClient(name, server string) *Client { 38 return &Client{ 39 name: name, 40 serverAddr: server, 41 // -- 42 client: finch.MakeHTTPClient(), 43 } 44 } 45 46 func (c *Client) Get(ctx context.Context, endpoint string, params [][]string, r R) (*http.Response, []byte, error) { 47 return c.request(ctx, "GET", endpoint, params, nil, r) 48 } 49 50 func (c *Client) Send(ctx context.Context, endpoint string, data interface{}, r R) error { 51 _, _, err := c.request(ctx, "POST", endpoint, nil, data, r) 52 return err 53 } 54 55 func (c *Client) request(ctx context.Context, method string, endpoint string, params [][]string, data interface{}, r R) (*http.Response, []byte, error) { 56 url := c.URL(endpoint, params) 57 finch.Debug("%s %s", method, url) 58 59 buf := new(bytes.Buffer) 60 if data != nil { 61 json.NewEncoder(buf).Encode(data) 62 } 63 64 var err error 65 var body []byte 66 var req *http.Request 67 var resp *http.Response 68 try := 0 69 for r.Tries == -1 || try < r.Tries { 70 try += 1 71 ctxReq, cancelReq := context.WithTimeout(ctx, r.Timeout) 72 req, _ = http.NewRequestWithContext(ctxReq, method, url, buf) 73 resp, err = c.client.Do(req) 74 cancelReq() 75 if err != nil { 76 goto RETRY 77 } 78 79 body, err = io.ReadAll(resp.Body) 80 resp.Body.Close() 81 if err != nil { 82 return nil, nil, err 83 } 84 85 switch resp.StatusCode { 86 case http.StatusOK: 87 return resp, body, nil // success 88 case http.StatusResetContent: 89 return resp, nil, nil // reset 90 default: 91 goto RETRY 92 } 93 94 RETRY: 95 if ctx.Err() != nil { 96 return nil, nil, ctx.Err() 97 } 98 finch.Debug("%v", err) 99 if c.PrintErrors && try%20 == 0 { 100 log.Printf("Request error, retrying: %v", err) 101 } 102 time.Sleep(r.Wait) 103 } 104 return nil, nil, ErrFailed 105 } 106 107 func (c *Client) URL(path string, params [][]string) string { 108 // Every request requires 'name=...' to tell server this client's name. 109 // It's not a hostname, just a user-defined name for the remote compute instance. 110 u := c.serverAddr + path + "?name=" + url.QueryEscape(c.name) 111 n := len(params) + 1 112 escaped := make([]string, len(params)+1) 113 for i := range params { 114 escaped[i] = params[i][0] + "=" + url.QueryEscape(params[i][1]) 115 } 116 escaped[n-1] = "stage-id=" + c.StageId 117 u += "&" + strings.Join(escaped, "&") 118 return u 119 }