github.com/argoproj/argo-cd/v3@v3.2.1/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) NewRequestWithContext(ctx context.Context, method, path string, body any) (*http.Request, error) { 69 // Make sure the given URL end with a slash 70 if !strings.HasSuffix(c.baseURL, "/") { 71 c.baseURL += "/" 72 } 73 74 var buf io.ReadWriter 75 if body != nil { 76 buf = &bytes.Buffer{} 77 enc := json.NewEncoder(buf) 78 enc.SetEscapeHTML(false) 79 err := enc.Encode(body) 80 if err != nil { 81 return nil, err 82 } 83 } 84 85 req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, buf) 86 if err != nil { 87 return nil, err 88 } 89 90 if body != nil { 91 req.Header.Set("Content-Type", "application/json") 92 } 93 94 if c.token != "" { 95 req.Header.Set("Authorization", "Bearer "+c.token) 96 } 97 98 if c.UserAgent != "" { 99 req.Header.Set("User-Agent", c.UserAgent) 100 } 101 102 return req, nil 103 } 104 105 func (c *Client) Do(req *http.Request, v any) (*http.Response, error) { 106 resp, err := c.client.Do(req) 107 if err != nil { 108 return nil, err 109 } 110 111 defer resp.Body.Close() 112 113 if err := CheckResponse(resp); err != nil { 114 return resp, err 115 } 116 117 switch v := v.(type) { 118 case nil: 119 case io.Writer: 120 _, err = io.Copy(v, resp.Body) 121 default: 122 buf := new(bytes.Buffer) 123 teeReader := io.TeeReader(resp.Body, buf) 124 decErr := json.NewDecoder(teeReader).Decode(v) 125 if decErr == io.EOF { 126 decErr = nil // ignore EOF errors caused by empty response body 127 } 128 if decErr != nil { 129 err = fmt.Errorf("%s: %s", decErr.Error(), buf.String()) 130 } 131 } 132 return resp, err 133 } 134 135 // CheckResponse checks the API response for errors, and returns them if present. 136 func CheckResponse(resp *http.Response) error { 137 if c := resp.StatusCode; http.StatusOK <= c && c < http.StatusMultipleChoices { 138 return nil 139 } 140 141 data, err := io.ReadAll(resp.Body) 142 if err != nil { 143 return fmt.Errorf("API error with status code %d: %w", resp.StatusCode, err) 144 } 145 146 var raw map[string]any 147 if err := json.Unmarshal(data, &raw); err != nil { 148 return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, string(data)) 149 } 150 151 message := "" 152 if value, ok := raw["message"].(string); ok { 153 message = value 154 } else if value, ok := raw["error"].(string); ok { 155 message = value 156 } 157 158 return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, message) 159 }