# `Bourse.HTTP`
[🔗](https://github.com/ZenHive/bourse/blob/main/lib/bourse/http.ex#L1)

HTTP client for exchange API requests.

Wraps Req with circuit breaker integration, error normalization, and
telemetry. All exchange HTTP communication goes through this module.

Error classification lives in `Bourse.HTTP.Errors`; rate-limit descriptor
shaping and header→state updates live in `Bourse.RateLimiter.Shaping`.

## Why Manual Query Encoding?

Uses `URI.encode_query/1` instead of Req's `:params` step because:

1. **Signing requires raw params** — signing patterns need params before URL encoding
2. **Sorted encoding** — some exchanges require alphabetically sorted params
3. **Consistency** — both public and private requests use the same encoding

## Features

- **Circuit breaker** — per-exchange, trips on 500+ and transport errors
- **Error normalization** — HTTP/exchange errors to `Bourse.Error` structs
- **Body-level error detection** — many exchanges return HTTP 200 with error in body
- **HTML response detection** — geo-blocks and Cloudflare return HTML instead of JSON
- **Safe retry** — only GET/HEAD, never POST/PUT/DELETE
- **Telemetry** — emits `[:bourse, :request, :start | :stop | :exception]`

## Signed Retry Boundary

`signed_request/5` keeps Req's retry policy and backoff, but attaches a
request step that obtains a fresh signature before every repeated attempt.
Req therefore never replays frozen timestamp, nonce, or deadline material.

`signed_request/4` accepts an already-signed request for compatibility and
is always single-attempt. A caller-supplied `:retry` option cannot re-enable
retries for that frozen request.

## Usage

    {:ok, exchange} = Bourse.Exchange.new("bybit")

    # Public endpoint
    {:ok, response} = Bourse.HTTP.request(exchange, :get, "/v5/market/tickers",
      params: %{"category" => "spot", "symbol" => "BTCUSDT"}
    )

# `response`

```elixir
@type response() :: %{status: integer(), headers: response_headers(), body: term()}
```

# `response_headers`

```elixir
@type response_headers() :: %{optional(String.t()) =&gt; [String.t()]}
```

HTTP response with status, headers, and decoded body

# `request`

```elixir
@spec request(Bourse.Exchange.t(), atom(), String.t(), keyword()) ::
  {:ok, response()} | {:error, Bourse.Error.t()}
```

Makes an HTTP request to an exchange API.

## Parameters

- `exchange` - Exchange configuration struct
- `method` - HTTP method (`:get`, `:post`, `:put`, `:delete`)
- `path` - API endpoint path (e.g., "/v5/market/tickers")

## Options

- `:params` - Query parameters or request body (default: `%{}`)
- `:headers` - Additional request headers (default: `[]`)
- `:timeout` - Request timeout in milliseconds (default: from `Bourse.Defaults`)
- `:base_url` - Override base URL (default: uses exchange.base_urls)
- `:body_encoding` - Endpoint body convention from the exchange spec
- `:plug` / `:adapter` - Req transport override (tests / custom adapter)
- `:retry` / `:retry_delay` / `:max_retries` - Req retry controls

Any other key is rejected as `{:error, %Bourse.Error{type: :bad_request}}`
before a request is issued. Unknown options are a caller error; they are
not forwarded to Req and they do not melt the venue circuit breaker.

## Returns

- `{:ok, response}` - Successful response with `:status`, `:headers`, `:body`
- `{:error, %Bourse.Error{}}` - Normalized error

# `signed_request`

```elixir
@spec signed_request(
  Bourse.Exchange.t(),
  Bourse.Signing.signed_request(),
  String.t(),
  keyword()
) ::
  {:ok, response()} | {:error, Bourse.Error.t()}
```

Executes an already-signed request exactly once.

This compatibility entry point forces `retry: false`, including when `opts`
contains another retry policy, because it cannot refresh time-bound signing
material. Dispatch uses `signed_request/5` instead.

# `signed_request`

```elixir
@spec signed_request(
  Bourse.Exchange.t(),
  Bourse.Signing.signed_request(),
  String.t(),
  (-&gt; Bourse.Signing.signed_request() | {:error, Bourse.Error.t()}),
  keyword()
) :: {:ok, response()} | {:error, Bourse.Error.t()}
```

Executes a signed request and refreshes its signature before every retry.

`resigner` reproduces the complete signed URL, headers, and body from the
original unsigned request. Req still controls whether and when to retry; the
Bourse request step replaces the time-bound signing material before the
repeated attempt reaches the adapter.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
