github.com/argoproj/argo-cd/v2@v2.10.9/applicationset/services/internal/http/client.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "strings" 11 "time" 12 ) 13 14 const ( 15 userAgent = "argocd-applicationset" 16 defaultTimeout = 30 17 ) 18 19 type Client struct { 20 // URL is the URL used for API requests. 21 baseURL string 22 23 // UserAgent is the user agent to include in HTTP requests. 24 UserAgent string 25 26 // Token is used to make authenticated API calls. 27 token string 28 29 // Client is an HTTP client used to communicate with the API. 30 client *http.Client 31 } 32 33 type ErrorResponse struct { 34 Body []byte 35 Response *http.Response 36 Message string 37 } 38 39 func NewClient(baseURL string, options ...ClientOptionFunc) (*Client, error) { 40 client, err := newClient(baseURL, options...) 41 if err != nil { 42 return nil, err 43 } 44 return client, nil 45 } 46 47 func newClient(baseURL string, options ...ClientOptionFunc) (*Client, error) { 48 c := &Client{baseURL: baseURL, UserAgent: userAgent} 49 50 // Configure the HTTP client. 51 c.client = &http.Client{ 52 Timeout: time.Duration(defaultTimeout) * time.Second, 53 } 54 55 // Apply any given client options. 56 for _, fn := range options { 57 if fn == nil { 58 continue 59 } 60 if err := fn(c); err != nil { 61 return nil, err 62 } 63 } 64 65 return c, nil 66 } 67 68 func (c *Client) NewRequest(method, path string, body interface{}, options []ClientOptionFunc) (*http.Request, error) { 69 70 // Make sure the given URL end with a slash 71 if !strings.HasSuffix(c.baseURL, "/") { 72 c.baseURL += "/" 73 } 74 75 var buf io.ReadWriter 76 if body != nil { 77 buf = &bytes.Buffer{} 78 enc := json.NewEncoder(buf) 79 enc.SetEscapeHTML(false) 80 err := enc.Encode(body) 81 if err != nil { 82 return nil, err 83 } 84 } 85 86 req, err := http.NewRequest(method, c.baseURL+path, buf) 87 if err != nil { 88 return nil, err 89 } 90 91 if body != nil { 92 req.Header.Set("Content-Type", "application/json") 93 } 94 95 if len(c.token) != 0 { 96 req.Header.Set("Authorization", "Bearer "+c.token) 97 } 98 99 if c.UserAgent != "" { 100 req.Header.Set("User-Agent", c.UserAgent) 101 } 102 103 return req, nil 104 } 105 106 func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { 107 resp, err := c.client.Do(req) 108 if err != nil { 109 return nil, err 110 } 111 112 defer resp.Body.Close() 113 114 if err := CheckResponse(resp); err != nil { 115 return resp, err 116 } 117 118 switch v := v.(type) { 119 case nil: 120 case io.Writer: 121 _, err = io.Copy(v, resp.Body) 122 default: 123 buf := new(bytes.Buffer) 124 teeReader := io.TeeReader(resp.Body, buf) 125 decErr := json.NewDecoder(teeReader).Decode(v) 126 if decErr == io.EOF { 127 decErr = nil // ignore EOF errors caused by empty response body 128 } 129 if decErr != nil { 130 err = fmt.Errorf("%s: %s", decErr.Error(), buf.String()) 131 } 132 } 133 return resp, err 134 } 135 136 // CheckResponse checks the API response for errors, and returns them if present. 137 func CheckResponse(resp *http.Response) error { 138 139 if c := resp.StatusCode; 200 <= c && c <= 299 { 140 return nil 141 } 142 143 data, err := io.ReadAll(resp.Body) 144 if err != nil { 145 return fmt.Errorf("API error with status code %d: %v", resp.StatusCode, err) 146 } 147 148 var raw map[string]interface{} 149 if err := json.Unmarshal(data, &raw); err != nil { 150 return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, string(data)) 151 } 152 153 message := "" 154 if value, ok := raw["message"].(string); ok { 155 message = value 156 } else if value, ok := raw["error"].(string); ok { 157 message = value 158 } 159 160 return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, message) 161 }