package httpx import ( "context" "io" "net/http" "strings" "git.codelab.vc/pkg/httpx/balancer" "git.codelab.vc/pkg/httpx/circuitbreaker" "git.codelab.vc/pkg/httpx/middleware" "git.codelab.vc/pkg/httpx/retry" ) // Client is a high-level HTTP client that composes middleware for retry, // circuit breaking, load balancing, logging, and more. type Client struct { httpClient *http.Client baseURL string errorMapper ErrorMapper } // New creates a new Client with the given options. // // The middleware chain is assembled as (outermost → innermost): // // Logging → User Middlewares → Retry → Circuit Breaker → Balancer → Base Transport func New(opts ...Option) *Client { o := &clientOptions{ transport: http.DefaultTransport, } for _, opt := range opts { opt(o) } // Build the middleware chain from inside out. var chain []middleware.Middleware // Balancer (innermost, wraps base transport). if len(o.endpoints) > 0 { chain = append(chain, balancer.Transport(o.endpoints, o.balancerOpts...)) } // Circuit breaker wraps balancer. if o.enableCB { chain = append(chain, circuitbreaker.Transport(o.cbOpts...)) } // Retry wraps circuit breaker + balancer. if o.enableRetry { chain = append(chain, retry.Transport(o.retryOpts...)) } // User middlewares. for i := len(o.middlewares) - 1; i >= 0; i-- { chain = append(chain, o.middlewares[i]) } // Logging (outermost). if o.logger != nil { chain = append(chain, middleware.Logging(o.logger)) } // Assemble: chain[last] is outermost. rt := o.transport for _, mw := range chain { rt = mw(rt) } return &Client{ httpClient: &http.Client{ Transport: rt, Timeout: o.timeout, }, baseURL: o.baseURL, errorMapper: o.errorMapper, } } // Do executes an HTTP request. func (c *Client) Do(ctx context.Context, req *http.Request) (*Response, error) { req = req.WithContext(ctx) c.resolveURL(req) resp, err := c.httpClient.Do(req) if err != nil { return nil, &Error{ Op: req.Method, URL: req.URL.String(), Err: err, } } r := newResponse(resp) if c.errorMapper != nil { if mapErr := c.errorMapper(resp); mapErr != nil { return r, &Error{ Op: req.Method, URL: req.URL.String(), StatusCode: resp.StatusCode, Err: mapErr, } } } return r, nil } // Get performs a GET request to the given URL. func (c *Client) Get(ctx context.Context, url string) (*Response, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } return c.Do(ctx, req) } // Post performs a POST request to the given URL with the given body. func (c *Client) Post(ctx context.Context, url string, body io.Reader) (*Response, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return nil, err } return c.Do(ctx, req) } // Put performs a PUT request to the given URL with the given body. func (c *Client) Put(ctx context.Context, url string, body io.Reader) (*Response, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) if err != nil { return nil, err } return c.Do(ctx, req) } // Delete performs a DELETE request to the given URL. func (c *Client) Delete(ctx context.Context, url string) (*Response, error) { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) if err != nil { return nil, err } return c.Do(ctx, req) } // HTTPClient returns the underlying *http.Client for advanced use cases. func (c *Client) HTTPClient() *http.Client { return c.httpClient } func (c *Client) resolveURL(req *http.Request) { if c.baseURL == "" { return } // Only resolve relative URLs (no scheme). if req.URL.Scheme == "" && req.URL.Host == "" { path := req.URL.String() if !strings.HasPrefix(path, "/") { path = "/" + path } base := strings.TrimRight(c.baseURL, "/") req.URL, _ = req.URL.Parse(base + path) } }