defmodule Polymarket.HTTP do @moduledoc """ Generic Req-based transport helper for the Polymarket SDK. This module is the shared HTTP layer used by `Polymarket.Gamma`, `Polymarket.Data`, and any future read-mostly Polymarket public-API surface in this package. User code should prefer the higher-level modules; this is documented mainly so it is testable in isolation and so the response contract is recorded once instead of repeated in each consumer. This module deliberately does not depend on `polymarket_clob`'s HTTP layer. The two have different host defaults and different error semantics: `polymarket_clob`'s `HTTP.normalize/2` returns `%PolymarketClob.Error{}` structs, while this SDK's wrappers return flat tuples. Keeping the layers separate avoids leaking the lower package's namespace into this package's user-facing types. ## Response contract Every call returns one of: * `{:ok, body}` — 2xx, body is the JSON-decoded response (Req's built-in `:decode_body` step handles JSON automatically when the response carries `content-type: application/json`). * `{:error, {:http_error, status, body}}` — non-2xx HTTP response. `status` is the integer status code; `body` is the upstream payload (typically a JSON map, sometimes a raw string). * `{:error, {:transport_error, reason}}` — Req-level transport failure (DNS, TCP, TLS, connection timeout, HTTP-2 protocol error). `reason` is the underlying exception or atom. ## Options * `:params` — keyword list serialized into the query string (default `[]`). * `:plug` — Req.Test plug, for tests. * `:timeout` — `receive_timeout` in milliseconds (default `15_000`). * `:retry` — Req retry policy. Defaults to `:transient` for GETs and `false` for everything else, mirroring `polymarket_clob`'s posture. * `:max_retries` — Req max retries (default `2`). * `:headers` — extra request headers (default `[]`). """ @type result :: {:ok, term()} | {:error, term()} @default_timeout 15_000 @default_max_retries 2 @doc """ Issues a GET against `host <> path` and returns the normalized result. """ @spec get(String.t(), String.t(), keyword()) :: result() def get(host, path, opts \\ []) do request(:get, host, path, opts) end # Generic dispatch is private for now: only GET-based callers ship # today. When a non-GET caller lands, lift this to public. defp request(method, host, path, opts) do url = url(host, path) req_opts = [ method: method, url: url, params: Keyword.get(opts, :params, []), headers: Keyword.get(opts, :headers, []), receive_timeout: Keyword.get(opts, :timeout, @default_timeout), retry: Keyword.get(opts, :retry, default_retry(method)), max_retries: Keyword.get(opts, :max_retries, @default_max_retries) ] |> maybe_put(:plug, Keyword.get(opts, :plug)) req_opts |> Req.request() |> normalize() end defp normalize({:ok, %{status: status, body: body}}) when status in 200..299 do {:ok, body} end defp normalize({:ok, %{status: status, body: body}}) do {:error, {:http_error, status, body}} end defp normalize({:error, reason}) do {:error, {:transport_error, reason}} end defp default_retry(:get), do: :transient defp default_retry(_), do: false defp url(host, path) do String.trim_trailing(host, "/") <> "/" <> String.trim_leading(path, "/") end defp maybe_put(opts, _key, nil), do: opts defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value) end