Packages
hackney
4.2.3
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Current section
Files
Jump to
Current section
Files
guides/middleware.md
# Middleware Guide
Hackney supports a RoundTripper-style middleware layer around
`hackney:request/1..5`. A middleware can observe, rewrite, short-circuit
or wrap a request/response pair. The API is a plain fun — no behaviour,
no registry, no deps.
```erlang
-type request() :: #{method := atom() | binary(),
url := #hackney_url{},
headers := [{binary(), binary()}],
body := term(),
options := [term()]}.
-type response() :: {ok, integer(), list(), binary()}
| {ok, integer(), list()} %% HEAD
| {ok, pid()} %% async or streaming upload
| {error, term()}.
-type next() :: fun((request()) -> response()).
-type middleware() :: fun((request(), next()) -> response()).
```
## Chain order
Outermost first. `[A, B, C]` means A wraps B wraps C: the request flows
`A → B → C → transport` and the response unwinds `transport → C → B → A`.
First in the list sees the request first and the response last — same
convention as Go's `http.RoundTripper`, Elixir Plug, Ruby Rack and
Tower-rs.
## Installing a chain
Per-request — overrides the global chain:
```erlang
hackney:request(get, URL, [], <<>>,
[{middleware, [Mw1, Mw2]}]).
```
Global fallback — applied to every request that doesn't set `middleware`:
```erlang
application:set_env(hackney, middleware, [Mw1, Mw2]).
```
Per-request **replaces** the global list. If you want to compose, merge
explicitly in your own code.
## Scope
Middleware runs around `hackney:request/1..5` only. The low-level
`hackney:connect/*` + `hackney:send_request/2` path bypasses middleware
— it's the raw transport, equivalent to Go's `http.Transport`.
Middleware sees whatever `Next` returns. For async and streaming bodies
that's a bare `{ok, Ref}` or `{ok, ConnPid}`; later message delivery is
the caller's problem. If you want to observe completion in async mode
you'll need to proxy `stream_to`.
If a middleware crashes, the exception propagates to the caller.
Hackney does not wrap user code in `try/catch`.
## Recipes
### Log every call
```erlang
Log = fun(Req, Next) ->
T0 = erlang:monotonic_time(millisecond),
Resp = Next(Req),
Status = case Resp of
{ok, S, _, _} -> S;
{ok, S, _} -> S;
_ -> error
end,
logger:info("~p ~s -> ~p (~pms)",
[maps:get(method, Req),
hackney_url:unparse_url(maps:get(url, Req)),
Status,
erlang:monotonic_time(millisecond) - T0]),
Resp
end,
hackney:get(URL, [], <<>>, [{middleware, [Log]}]).
```
### Add a header to every request
```erlang
AddHeader = fun(Req, Next) ->
H = maps:get(headers, Req),
Next(Req#{headers := [{<<"x-trace-id">>, trace_id()} | H]})
end.
```
### Retry on transient errors
```erlang
Retry = fun Self(Req, Next) ->
case Next(Req) of
{error, timeout} -> Self(Req, Next);
Other -> Other
end
end.
```
(For real retries add a counter and a backoff; kept minimal here.)
### Short-circuit / cache
A middleware that doesn't call `Next` returns its own response and the
request never leaves the process:
```erlang
Cache = fun(Req, Next) ->
Key = cache_key(Req),
case cache_get(Key) of
{ok, Resp} -> Resp;
miss -> cache_put(Key, Next(Req))
end
end.
```
## Migrating from `hackney_metrics`
The `hackney_metrics` module and its prometheus/dummy backends have been
removed. Hackney no longer emits any metrics itself. Port your metrics
into a middleware:
### Prometheus
```erlang
PromMetrics = fun(Req, Next) ->
T0 = erlang:monotonic_time(millisecond),
#hackney_url{host = Host} = maps:get(url, Req),
HostBin = iolist_to_binary(Host),
prometheus_counter:inc(hackney_requests_total, [HostBin]),
prometheus_gauge:inc(hackney_requests_active, [HostBin]),
Resp = Next(Req),
prometheus_gauge:dec(hackney_requests_active, [HostBin]),
prometheus_counter:inc(hackney_requests_finished_total, [HostBin]),
Dt = (erlang:monotonic_time(millisecond) - T0) / 1000,
prometheus_histogram:observe(hackney_request_duration_seconds,
[HostBin], Dt),
Resp
end,
application:set_env(hackney, middleware, [PromMetrics]).
```
Declare the same counter/gauge/histogram at startup as before — the
middleware just emits the numbers, it does not own the registry.
### Telemetry
```erlang
Telemetry = fun(Req, Next) ->
T0 = erlang:monotonic_time(),
telemetry:execute([hackney, request, start],
#{system_time => erlang:system_time()},
#{method => maps:get(method, Req)}),
try
Resp = Next(Req),
telemetry:execute([hackney, request, stop],
#{duration => erlang:monotonic_time() - T0},
#{result => Resp}),
Resp
catch Class:Reason:Stack ->
telemetry:execute([hackney, request, exception],
#{duration => erlang:monotonic_time() - T0},
#{kind => Class, reason => Reason,
stacktrace => Stack}),
erlang:raise(Class, Reason, Stack)
end
end.
```
### Pool observability
`hackney_pool_free_count`, `hackney_pool_in_use_count` and
`hackney_pool_checkouts_total` are gone with the metrics module — they
reflect pool state, not per-request events, and can't be expressed as
middleware. Use `hackney_pool:get_stats/1` from your own metrics
collector to sample pool state at whatever cadence you want:
```erlang
Stats = hackney_pool:get_stats(default),
%% #{name, max, in_use_count, free_count, queue_count}
```