code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when calling external HTTP APIs with Req — client modules, Req.new options, retries and Retry-After, timeouts, Finch pools, idempotency keys, Req.Test stubs, SSRF-safe redirects. Not for Oban job structure (use oban-essentials) or serving JSON APIs (use phoenix-json-api).
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill http-clients-req --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/http-clients-reqContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when calling external HTTP APIs with Req — client modules, Req.new options, retries and Retry-After, timeouts, Finch pools, idempotency keys, Req.Test stubs, SSRF-safe redirects. Not for Oban job structure (use oban-essentials) or serving JSON APIs (use phoenix-json-api).
name: http-clients-req description: "Use when calling external HTTP APIs with Req — client modules, Req.new options, retries and Retry-After, timeouts, Finch pools, idempotency keys, Req.Test stubs, SSRF-safe redirects. Not for Oban job structure (use oban-essentials) or serving JSON APIs (use phoenix-json-api)."
> Verified against Elixir 1.20, Phoenix 1.8, Req 0.7, Finch 0.24 — October 2026
1. **One client module per external API.** Build the `%Req.Request{}` in a private function from runtime config (`Application.fetch_env!/2`), never in a module attribute. Merge test-only options (`plug:`) from config, and return domain values (`{:ok, celsius}`), not `%Req.Response{}`. 2. **Match every outcome of `Req.request/2`:** each status you expect, a catch-all `{:ok, %Req.Response{status: status}}`, and `{:error, exception}`. 4xx/5xx responses arrive as `{:ok, response}`, and `Req.get!/2` doesn't raise on them either. Use bang functions only where a crash is fine (scripts, seeds, mix tasks). 3. **Know the default retry.** `retry: :safe_transient` retries GET/HEAD up to 3 times on 408/429/500/502/503/504 and on transport timeouts or refused/closed connections. It sleeps in the calling process and obeys `Retry-After` with no upper limit. Cap that delay, and lower `max_retries` on user-facing paths. 4. **`retry: false` inside Oban workers.** Oban already retries the job with backoff; Req retries on top multiply the attempts and hold the queue slot while they sleep. 5. **Retry POST/PUT/PATCH only with an idempotency key** built from the business record (`"order-#{id}-charge"`), so it stays the same across Req retries and Oban attempts. Then `retry: :transient` is safe. 6. **Set every timeout on purpose.** Connect (5 s by default), `receive_timeout` (15 s per read), `request_timeout` (total time, new in 0.7), and `finch: [pool_timeout: ...]`. Never block a LiveView event handler on a slow API: use `start_async`. 7. **Expect bodies of any shape.** `json:` encodes with Jason. Responses are decoded by `content-type` into maps with string keys. A malformed JSON body is `{:error, %Jason.DecodeError{}}`, and an HTML error page from a proxy stays a binary. 8. **Never fetch a user-supplied URL without a host allowlist and `redirect: false`.** On a redirect to another host, Req drops only `authorization`, so custom API-key headers go along. Never log request headers or bodies, and never use `verify: :verify_none`. 9. **Test with `plug: {Req.Test, MyApp.Client}` and `retry: false` in `config/test.exs`.** Use Mox only at a behaviour boundary, when callers shouldn't see HTTP at all. 10. **Use a named Finch pool only for pool needs** (size, HTTP/2, per-host config). Pass it as `finch: [name: MyApp.Finch]` (the 0.7 form) and put connect options in the pool config: combining `finch:` with `connect_options:` raises.
---
defmodule MyApp.Weather do
@moduledoc "Client for the Example Weather API."
@doc "Current temperature in °C. `opts` are extra Req options, e.g. `retry: false` in Oban workers."
def current(city, opts \\ []) when is_binary(city) do
case Req.request(new(), [url: "/v1/current", params: [city: city]] ++ opts) do
{:ok, %Req.Response{status: 200, body: %{"celsius" => celsius}}} -> {:ok, celsius}
{:ok, %Req.Response{status: 404}} -> {:error, :not_found}
{:ok, %Req.Response{status: 429}} -> {:error, :rate_limited}
{:ok, %Req.Response{status: status}} -> {:error, {:unexpected_response, status}}
{:error, exception} -> {:error, exception}
end
end
defp new do
config = Application.fetch_env!(:my_app, __MODULE__)
Req.new(
base_url: Keyword.fetch!(config, :base_url),
auth: {:bearer, Keyword.fetch!(config, :api_key)},
receive_timeout: 5_000,
max_retries: 2
)
|> Req.merge(Keyword.get(config, :req_options, []))
end
end# config/config.exs
config :my_app, MyApp.Weather, base_url: "https://api.weather.example"
# config/runtime.exs: secrets are read at boot, not at compile time
if config_env() == :prod do
config :my_app, MyApp.Weather, api_key: System.fetch_env!("WEATHER_API_KEY")
end
# config/test.exs
config :my_app, MyApp.Weather,
api_key: "test-key",
req_options: [plug: {Req.Test, MyApp.Weather}, retry: false]| | `retry: :safe_transient` (default) | |---|---| | Methods | GET, HEAD (`:transient` retries every method) | | Responses | 408, 429, 500, 502, 503, 504 | | Exceptions | `Req.TransportError` with `:timeout`, `:econnrefused`, `:closed`; HTTP/2 `:unprocessed`, `:pool_not_available` | | Attempts | `max_retries: 3`, so up to 4 requests in total | | Delay | exponential with jitter (~1 s, 2 s, 4 s); on 429/503 the `Retry-After` value (second
· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…