package httpclient import ( "context" "encoding/json" "io" "net/http" "code.nochebuena.dev/go/xerrors" ) // DoJSON executes req and decodes the JSON response body into T. // Returns a xerrors-typed error for HTTP 4xx/5xx responses. func DoJSON[T any](ctx context.Context, client Client, req *http.Request) (*T, error) { req = req.WithContext(ctx) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, MapStatusToError(resp.StatusCode, "external API error") } body, err := io.ReadAll(resp.Body) if err != nil { return nil, xerrors.New(xerrors.ErrInternal, "failed to read response body").WithError(err) } var data T if err := json.Unmarshal(body, &data); err != nil { return nil, xerrors.New(xerrors.ErrInternal, "failed to decode JSON response").WithError(err) } return &data, nil } // MapStatusToError maps an HTTP status code to the matching xerrors type. func MapStatusToError(code int, msg string) error { switch code { case http.StatusNotFound: return xerrors.New(xerrors.ErrNotFound, msg) case http.StatusBadRequest: return xerrors.New(xerrors.ErrInvalidInput, msg) case http.StatusUnauthorized: return xerrors.New(xerrors.ErrUnauthorized, msg) case http.StatusForbidden: return xerrors.New(xerrors.ErrPermissionDenied, msg) case http.StatusConflict: return xerrors.New(xerrors.ErrAlreadyExists, msg) case http.StatusTooManyRequests: return xerrors.New(xerrors.ErrUnavailable, msg) default: return xerrors.New(xerrors.ErrInternal, msg) } }