Current section

Files

Jump to
hackney src hackney_middleware.erl
Raw

src/hackney_middleware.erl

%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% @doc RoundTripper-style middleware for hackney:request/1..5.
%%%
%%% A middleware is a fun `fun((Request, Next) -> Response)'. `Next' is
%%% itself a fun that takes a (possibly rewritten) request and returns
%%% the response. Middleware can observe, rewrite, short-circuit, or
%%% wrap the downstream call.
%%%
%%% Chains are outermost-first: `[A, B, C]' means A wraps B wraps C, so
%%% the request flows `A -> B -> C -> transport' and the response
%%% unwinds the same way. This matches Go's `http.RoundTripper', Plug,
%%% Rack and Tower-rs.
%%%
%%% Middleware wraps the user-facing `hackney:request/1..5' only. The
%%% low-level `hackney:connect/*' + `hackney:send_request/2' path is
%%% intentionally unchanged.
%%%
%%% Example — a minimal duration logger:
%%% ```
%%% Log = fun(Req, Next) ->
%%% T0 = erlang:monotonic_time(millisecond),
%%% Resp = Next(Req),
%%% logger:info("~p ~s -> ~pms",
%%% [maps:get(method, Req),
%%% hackney_url:unparse_url(maps:get(url, Req)),
%%% erlang:monotonic_time(millisecond) - T0]),
%%% Resp
%%% end,
%%% hackney:get(URL, [], <<>>, [{middleware, [Log]}]).
%%% '''
-module(hackney_middleware).
-export([apply_chain/3,
resolve_chain/1]).
-export_type([request/0, response/0, next/0, middleware/0]).
-include("hackney_lib.hrl").
-type request() :: #{method := atom() | binary(),
url := #hackney_url{},
headers := [{binary(), binary()}],
body := term(),
options := [term()]}.
-type response() :: {ok, integer(), list(), binary()}
| {ok, integer(), list()}
| {ok, pid()}
| {error, term()}.
-type next() :: fun((request()) -> response()).
-type middleware() :: fun((request(), next()) -> response()).
%% @doc Apply `Chain' (outermost-first) around `Terminal'.
%%
%% The returned response is whatever the outermost middleware produces;
%% crashes propagate to the caller.
-spec apply_chain([middleware()], request(), next()) -> response().
apply_chain([], Req, Terminal) ->
Terminal(Req);
apply_chain(Chain, Req, Terminal) when is_list(Chain) ->
Next = build(Chain, Terminal),
Next(Req).
%% @doc Pick the middleware chain for a request.
%%
%% Per-request `{middleware, List}' in `Options' replaces the global
%% `application:get_env(hackney, middleware)' value. No implicit merge.
-spec resolve_chain([term()]) -> [middleware()].
resolve_chain(Options) ->
case lists:keyfind(middleware, 1, Options) of
{middleware, List} when is_list(List) ->
List;
false ->
case application:get_env(hackney, middleware) of
{ok, List} when is_list(List) -> List;
_ -> []
end
end.
%% ============================================================================
%% Internal
%% ============================================================================
%% Fold from the right so the head of Chain becomes the outermost call.
build(Chain, Terminal) ->
lists:foldr(
fun(M, Next) when is_function(M, 2) ->
fun(R) -> M(R, Next) end
end,
Terminal,
Chain).