package client import ( "bytes" "context" "fmt" "io" "net/http" "strings" ) type Response struct { StatusCode int Header http.Header Body []byte } type Client struct { baseURL string httpClient *http.Client } func New(baseURL string, httpClient *http.Client) *Client { baseURL = strings.TrimSpace(baseURL) baseURL = strings.TrimRight(baseURL, "/") if httpClient == nil { httpClient = http.DefaultClient } return &Client{ baseURL: baseURL, httpClient: httpClient, } } func (c *Client) Do(ctx context.Context, method, path string, headers http.Header, body []byte) (Response, error) { url, err := c.resolveURL(path) if err != nil { return Response{}, err } req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) if err != nil { return Response{}, fmt.Errorf("build request: %w", err) } for key, values := range headers { for _, value := range values { req.Header.Add(key, value) } } resp, err := c.httpClient.Do(req) if err != nil { return Response{}, fmt.Errorf("request %s %s: %w", method, url, err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return Response{}, fmt.Errorf("read response body: %w", err) } return Response{ StatusCode: resp.StatusCode, Header: resp.Header.Clone(), Body: respBody, }, nil } func (c *Client) resolveURL(path string) (string, error) { path = strings.TrimSpace(path) if path == "" { return "", fmt.Errorf("path is required") } if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") { return path, nil } if c.baseURL == "" { return "", fmt.Errorf("base url is required for relative path %q", path) } if !strings.HasPrefix(path, "/") { path = "/" + path } return c.baseURL + path, nil }