mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
package httpwrapper
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/formancehq/go-libs/v3/logging"
|
|
"github.com/formancehq/payments/internal/models"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
var (
|
|
ErrStatusCodeUnexpected = errors.New("unexpected status code")
|
|
ErrStatusCodeClientError = fmt.Errorf("%w: http client error", ErrStatusCodeUnexpected)
|
|
ErrStatusCodeServerError = fmt.Errorf("%w: http server error", ErrStatusCodeUnexpected)
|
|
ErrStatusCodeTooManyRequests = fmt.Errorf("%w: http too many requests error", ErrStatusCodeUnexpected)
|
|
|
|
defaultHttpErrorCheckerFn = func(statusCode int) error {
|
|
if statusCode == http.StatusTooManyRequests {
|
|
return ErrStatusCodeTooManyRequests
|
|
}
|
|
|
|
if statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError {
|
|
return ErrStatusCodeClientError
|
|
} else if statusCode >= http.StatusInternalServerError {
|
|
return ErrStatusCodeServerError
|
|
}
|
|
return nil
|
|
}
|
|
)
|
|
|
|
// Client is a convenience wrapper that encapsulates common code related to interacting with HTTP endpoints
|
|
type Client interface {
|
|
// Do performs an HTTP request while handling errors and unmarshaling success and error responses into the provided interfaces
|
|
// expectedBody and errorBody should be pointers to structs
|
|
Do(ctx context.Context, req *http.Request, expectedBody, errorBody any) (statusCode int, err error)
|
|
}
|
|
|
|
type client struct {
|
|
httpClient *http.Client
|
|
|
|
httpErrorCheckerFn func(statusCode int) error
|
|
}
|
|
|
|
func NewClient(config *Config) Client {
|
|
if config.Timeout == 0 {
|
|
config.Timeout = models.DefaultConnectorClientTimeout
|
|
}
|
|
if config.Transport != nil {
|
|
config.Transport = otelhttp.NewTransport(config.Transport)
|
|
} else {
|
|
config.Transport = http.DefaultTransport.(*http.Transport).Clone()
|
|
}
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: config.Timeout,
|
|
Transport: config.Transport,
|
|
}
|
|
if config.OAuthConfig != nil {
|
|
// pass a pre-configured http client to oauth lib via the context
|
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
|
httpClient = config.OAuthConfig.Client(ctx)
|
|
}
|
|
|
|
if config.HttpErrorCheckerFn == nil {
|
|
config.HttpErrorCheckerFn = defaultHttpErrorCheckerFn
|
|
}
|
|
|
|
return &client{
|
|
httpErrorCheckerFn: config.HttpErrorCheckerFn,
|
|
httpClient: httpClient,
|
|
}
|
|
}
|
|
|
|
func (c *client) Do(ctx context.Context, req *http.Request, expectedBody, errorBody any) (int, error) {
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to make request: %w", err)
|
|
}
|
|
|
|
reqErr := c.httpErrorCheckerFn(resp.StatusCode)
|
|
// the caller doesn't care about the response body so we return early
|
|
if resp.Body == nil || (reqErr == nil && expectedBody == nil) || (reqErr != nil && errorBody == nil) {
|
|
return resp.StatusCode, reqErr
|
|
}
|
|
|
|
defer func() {
|
|
err = resp.Body.Close()
|
|
if err != nil {
|
|
logging.FromContext(ctx).Errorf("failed to close response body: %w", err)
|
|
}
|
|
}()
|
|
|
|
// TODO: reading everything into memory might not be optimal if we expect long responses
|
|
rawBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return resp.StatusCode, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
if reqErr != nil {
|
|
if err = json.Unmarshal(rawBody, errorBody); err != nil {
|
|
return resp.StatusCode, fmt.Errorf("failed to unmarshal error response (%w) with status %d: %w", err, resp.StatusCode, reqErr)
|
|
}
|
|
return resp.StatusCode, reqErr
|
|
}
|
|
|
|
// TODO: assuming json bodies for now, but may need to handle other body types
|
|
if err = json.Unmarshal(rawBody, expectedBody); err != nil {
|
|
return resp.StatusCode, fmt.Errorf("failed to unmarshal response with status %d: %w", resp.StatusCode, err)
|
|
}
|
|
return resp.StatusCode, nil
|
|
}
|