diff options
-rw-r--r-- | doc/src/guide/introduction.asciidoc | 5 | ||||
-rw-r--r-- | doc/src/manual/gun.asciidoc | 14 | ||||
-rw-r--r-- | doc/src/manual/gun.connect.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.delete.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.get.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.head.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.headers.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.options.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.patch.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.post.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.put.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.request.asciidoc | 2 | ||||
-rw-r--r-- | doc/src/manual/gun.ws_upgrade.asciidoc | 2 | ||||
-rw-r--r-- | src/gun.erl | 73 | ||||
-rw-r--r-- | test/gun_SUITE.erl | 52 |
15 files changed, 121 insertions, 45 deletions
diff --git a/doc/src/guide/introduction.asciidoc b/doc/src/guide/introduction.asciidoc index 374f1f3..fa0e6c6 100644 --- a/doc/src/guide/introduction.asciidoc +++ b/doc/src/guide/introduction.asciidoc @@ -46,7 +46,4 @@ In the HTTP protocol, the method name is case sensitive. All standard method names are uppercase. Header names are case insensitive. Gun converts all the header names -to lowercase, and expects your application to provide lowercase header -names. - -The same applies to any other case insensitive value. +to lowercase, including request headers provided by your application. diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc index dff927c..2726564 100644 --- a/doc/src/manual/gun.asciidoc +++ b/doc/src/manual/gun.asciidoc @@ -267,6 +267,16 @@ ws_opts (#{}):: Options specific to the Websocket protocol. +=== req_headers() + +[source,erlang] +---- +req_headers() :: [{binary() | string() | atom(), iodata()}] + | #{binary() | string() | atom() => iodata()} +---- + +Request headers. + === req_opts() [source,erlang] @@ -315,6 +325,10 @@ undocumented and must be set to `gun_ws_h`. == Changelog +* *2.0*: Introduce the type `req_headers()` and extend the + types accepted for header names for greater + interoperability. Header names are automatically + lowercased as well. * *2.0*: Function `gun:headers/4,5` introduced. * *2.0*: The `keepalive` option is now set to `infinity` by default for the HTTP/1.1 protocol, disabling it. diff --git a/doc/src/manual/gun.connect.asciidoc b/doc/src/manual/gun.connect.asciidoc index 483b24d..de40e34 100644 --- a/doc/src/manual/gun.connect.asciidoc +++ b/doc/src/manual/gun.connect.asciidoc @@ -19,7 +19,7 @@ connect(ConnPid, Destination, Headers, ReqOpts) ConnPid :: pid() Destination :: gun:connect_destination() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() ReqOpts :: gun:req_opts() StreamRef :: reference() ---- diff --git a/doc/src/manual/gun.delete.asciidoc b/doc/src/manual/gun.delete.asciidoc index feb64ff..7367adb 100644 --- a/doc/src/manual/gun.delete.asciidoc +++ b/doc/src/manual/gun.delete.asciidoc @@ -19,7 +19,7 @@ delete(ConnPid, Path, Headers, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() ReqOpts :: gun:req_opts() StreamRef :: reference() ---- diff --git a/doc/src/manual/gun.get.asciidoc b/doc/src/manual/gun.get.asciidoc index 623c038..64feeab 100644 --- a/doc/src/manual/gun.get.asciidoc +++ b/doc/src/manual/gun.get.asciidoc @@ -19,7 +19,7 @@ get(ConnPid, Path, Headers, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() ReqOpts :: gun:req_opts() StreamRef :: reference() ---- diff --git a/doc/src/manual/gun.head.asciidoc b/doc/src/manual/gun.head.asciidoc index 3d92e38..08ce86b 100644 --- a/doc/src/manual/gun.head.asciidoc +++ b/doc/src/manual/gun.head.asciidoc @@ -19,7 +19,7 @@ head(ConnPid, Path, Headers, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() ReqOpts :: gun:req_opts() StreamRef :: reference() ---- diff --git a/doc/src/manual/gun.headers.asciidoc b/doc/src/manual/gun.headers.asciidoc index bab1b82..4abcb39 100644 --- a/doc/src/manual/gun.headers.asciidoc +++ b/doc/src/manual/gun.headers.asciidoc @@ -17,7 +17,7 @@ headers(ConnPid, Method, Path, Headers, ReqOpts) ConnPid :: pid() Method :: binary() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() ReqOpts :: gun:req_opts() StreamRef :: reference() ---- diff --git a/doc/src/manual/gun.options.asciidoc b/doc/src/manual/gun.options.asciidoc index 5b36f22..dfbba7b 100644 --- a/doc/src/manual/gun.options.asciidoc +++ b/doc/src/manual/gun.options.asciidoc @@ -19,7 +19,7 @@ options(ConnPid, Path, Headers, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() ReqOpts :: gun:req_opts() StreamRef :: reference() ---- diff --git a/doc/src/manual/gun.patch.asciidoc b/doc/src/manual/gun.patch.asciidoc index c240a4d..cdc8f97 100644 --- a/doc/src/manual/gun.patch.asciidoc +++ b/doc/src/manual/gun.patch.asciidoc @@ -22,7 +22,7 @@ patch(ConnPid, Path, Headers, Body, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() Body :: iodata() ReqOpts :: gun:req_opts() StreamRef :: reference() diff --git a/doc/src/manual/gun.post.asciidoc b/doc/src/manual/gun.post.asciidoc index aed8b3f..a545a32 100644 --- a/doc/src/manual/gun.post.asciidoc +++ b/doc/src/manual/gun.post.asciidoc @@ -22,7 +22,7 @@ post(ConnPid, Path, Headers, Body, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() Body :: iodata() ReqOpts :: gun:req_opts() StreamRef :: reference() diff --git a/doc/src/manual/gun.put.asciidoc b/doc/src/manual/gun.put.asciidoc index a60849b..7807d68 100644 --- a/doc/src/manual/gun.put.asciidoc +++ b/doc/src/manual/gun.put.asciidoc @@ -22,7 +22,7 @@ put(ConnPid, Path, Headers, Body, ReqOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() Body :: iodata() ReqOpts :: gun:req_opts() StreamRef :: reference() diff --git a/doc/src/manual/gun.request.asciidoc b/doc/src/manual/gun.request.asciidoc index f5c1cd8..0a50499 100644 --- a/doc/src/manual/gun.request.asciidoc +++ b/doc/src/manual/gun.request.asciidoc @@ -17,7 +17,7 @@ request(ConnPid, Method, Path, Headers, Body, ReqOpts) ConnPid :: pid() Method :: binary() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() Body :: iodata() ReqOpts :: gun:req_opts() StreamRef :: reference() diff --git a/doc/src/manual/gun.ws_upgrade.asciidoc b/doc/src/manual/gun.ws_upgrade.asciidoc index cb9da06..241d916 100644 --- a/doc/src/manual/gun.ws_upgrade.asciidoc +++ b/doc/src/manual/gun.ws_upgrade.asciidoc @@ -19,7 +19,7 @@ ws_upgrade(ConnPid, Path, Headers, WsOpts) ConnPid :: pid() Path :: iodata() -Headers :: [{binary(), iodata()}] +Headers :: gun:req_headers() WsOpts :: gun:ws_opts StreamRef :: reference() ---- diff --git a/src/gun.erl b/src/gun.erl index 55e70dd..75092f1 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -94,7 +94,9 @@ -export([not_connected/3]). -export([connected/3]). --type headers() :: [{binary(), iodata()}]. +-type req_headers() :: [{binary() | string() | atom(), iodata()}] + | #{binary() | string() | atom() => iodata()}. +-export_type([req_headers/0]). -type ws_close_code() :: 1000..4999. -type ws_frame() :: close | ping | pong @@ -320,11 +322,11 @@ shutdown(ServerPid) -> delete(ServerPid, Path) -> request(ServerPid, <<"DELETE">>, Path, [], <<>>). --spec delete(pid(), iodata(), headers()) -> reference(). +-spec delete(pid(), iodata(), req_headers()) -> reference(). delete(ServerPid, Path, Headers) -> request(ServerPid, <<"DELETE">>, Path, Headers, <<>>). --spec delete(pid(), iodata(), headers(), req_opts()) -> reference(). +-spec delete(pid(), iodata(), req_headers(), req_opts()) -> reference(). delete(ServerPid, Path, Headers, ReqOpts) -> request(ServerPid, <<"DELETE">>, Path, Headers, <<>>, ReqOpts). @@ -332,11 +334,11 @@ delete(ServerPid, Path, Headers, ReqOpts) -> get(ServerPid, Path) -> request(ServerPid, <<"GET">>, Path, [], <<>>). --spec get(pid(), iodata(), headers()) -> reference(). +-spec get(pid(), iodata(), req_headers()) -> reference(). get(ServerPid, Path, Headers) -> request(ServerPid, <<"GET">>, Path, Headers, <<>>). --spec get(pid(), iodata(), headers(), req_opts()) -> reference(). +-spec get(pid(), iodata(), req_headers(), req_opts()) -> reference(). get(ServerPid, Path, Headers, ReqOpts) -> request(ServerPid, <<"GET">>, Path, Headers, <<>>, ReqOpts). @@ -344,11 +346,11 @@ get(ServerPid, Path, Headers, ReqOpts) -> head(ServerPid, Path) -> request(ServerPid, <<"HEAD">>, Path, [], <<>>). --spec head(pid(), iodata(), headers()) -> reference(). +-spec head(pid(), iodata(), req_headers()) -> reference(). head(ServerPid, Path, Headers) -> request(ServerPid, <<"HEAD">>, Path, Headers, <<>>). --spec head(pid(), iodata(), headers(), req_opts()) -> reference(). +-spec head(pid(), iodata(), req_headers(), req_opts()) -> reference(). head(ServerPid, Path, Headers, ReqOpts) -> request(ServerPid, <<"HEAD">>, Path, Headers, <<>>, ReqOpts). @@ -356,82 +358,93 @@ head(ServerPid, Path, Headers, ReqOpts) -> options(ServerPid, Path) -> request(ServerPid, <<"OPTIONS">>, Path, [], <<>>). --spec options(pid(), iodata(), headers()) -> reference(). +-spec options(pid(), iodata(), req_headers()) -> reference(). options(ServerPid, Path, Headers) -> request(ServerPid, <<"OPTIONS">>, Path, Headers, <<>>). --spec options(pid(), iodata(), headers(), req_opts()) -> reference(). +-spec options(pid(), iodata(), req_headers(), req_opts()) -> reference(). options(ServerPid, Path, Headers, ReqOpts) -> request(ServerPid, <<"OPTIONS">>, Path, Headers, <<>>, ReqOpts). --spec patch(pid(), iodata(), headers()) -> reference(). +-spec patch(pid(), iodata(), req_headers()) -> reference(). patch(ServerPid, Path, Headers) -> headers(ServerPid, <<"PATCH">>, Path, Headers). --spec patch(pid(), iodata(), headers(), iodata() | req_opts()) -> reference(). +-spec patch(pid(), iodata(), req_headers(), iodata() | req_opts()) -> reference(). patch(ServerPid, Path, Headers, ReqOpts) when is_map(ReqOpts) -> headers(ServerPid, <<"PATCH">>, Path, Headers, ReqOpts); patch(ServerPid, Path, Headers, Body) -> request(ServerPid, <<"PATCH">>, Path, Headers, Body). --spec patch(pid(), iodata(), headers(), iodata(), req_opts()) -> reference(). +-spec patch(pid(), iodata(), req_headers(), iodata(), req_opts()) -> reference(). patch(ServerPid, Path, Headers, Body, ReqOpts) -> request(ServerPid, <<"PATCH">>, Path, Headers, Body, ReqOpts). --spec post(pid(), iodata(), headers()) -> reference(). +-spec post(pid(), iodata(), req_headers()) -> reference(). post(ServerPid, Path, Headers) -> headers(ServerPid, <<"POST">>, Path, Headers). --spec post(pid(), iodata(), headers(), iodata() | req_opts()) -> reference(). +-spec post(pid(), iodata(), req_headers(), iodata() | req_opts()) -> reference(). post(ServerPid, Path, Headers, ReqOpts) when is_map(ReqOpts) -> headers(ServerPid, <<"POST">>, Path, Headers, ReqOpts); post(ServerPid, Path, Headers, Body) -> request(ServerPid, <<"POST">>, Path, Headers, Body). --spec post(pid(), iodata(), headers(), iodata(), req_opts()) -> reference(). +-spec post(pid(), iodata(), req_headers(), iodata(), req_opts()) -> reference(). post(ServerPid, Path, Headers, Body, ReqOpts) -> request(ServerPid, <<"POST">>, Path, Headers, Body, ReqOpts). --spec put(pid(), iodata(), headers()) -> reference(). +-spec put(pid(), iodata(), req_headers()) -> reference(). put(ServerPid, Path, Headers) -> headers(ServerPid, <<"PUT">>, Path, Headers). --spec put(pid(), iodata(), headers(), iodata() | req_opts()) -> reference(). +-spec put(pid(), iodata(), req_headers(), iodata() | req_opts()) -> reference(). put(ServerPid, Path, Headers, ReqOpts) when is_map(ReqOpts) -> headers(ServerPid, <<"PUT">>, Path, Headers, ReqOpts); put(ServerPid, Path, Headers, Body) -> request(ServerPid, <<"PUT">>, Path, Headers, Body). --spec put(pid(), iodata(), headers(), iodata(), req_opts()) -> reference(). +-spec put(pid(), iodata(), req_headers(), iodata(), req_opts()) -> reference(). put(ServerPid, Path, Headers, Body, ReqOpts) -> request(ServerPid, <<"PUT">>, Path, Headers, Body, ReqOpts). %% Generic requests interface. --spec headers(pid(), iodata(), iodata(), headers()) -> reference(). +-spec headers(pid(), iodata(), iodata(), req_headers()) -> reference(). headers(ServerPid, Method, Path, Headers) -> headers(ServerPid, Method, Path, Headers, #{}). -%% @todo Accept header names as maps. --spec headers(pid(), iodata(), iodata(), headers(), req_opts()) -> reference(). +-spec headers(pid(), iodata(), iodata(), req_headers(), req_opts()) -> reference(). headers(ServerPid, Method, Path, Headers, ReqOpts) -> StreamRef = make_ref(), ReplyTo = maps:get(reply_to, ReqOpts, self()), - gen_statem:cast(ServerPid, {headers, ReplyTo, StreamRef, Method, Path, Headers}), + gen_statem:cast(ServerPid, {headers, ReplyTo, StreamRef, + Method, Path, normalize_headers(Headers)}), StreamRef. --spec request(pid(), iodata(), iodata(), headers(), iodata()) -> reference(). +-spec request(pid(), iodata(), iodata(), req_headers(), iodata()) -> reference(). request(ServerPid, Method, Path, Headers, Body) -> request(ServerPid, Method, Path, Headers, Body, #{}). -%% @todo Accept header names as maps. --spec request(pid(), iodata(), iodata(), headers(), iodata(), req_opts()) -> reference(). +-spec request(pid(), iodata(), iodata(), req_headers(), iodata(), req_opts()) -> reference(). request(ServerPid, Method, Path, Headers, Body, ReqOpts) -> StreamRef = make_ref(), ReplyTo = maps:get(reply_to, ReqOpts, self()), - gen_statem:cast(ServerPid, {request, ReplyTo, StreamRef, Method, Path, Headers, Body}), + gen_statem:cast(ServerPid, {request, ReplyTo, StreamRef, + Method, Path, normalize_headers(Headers), Body}), StreamRef. +normalize_headers([]) -> + []; +normalize_headers([{Name, Value}|Tail]) when is_binary(Name) -> + [{string:lowercase(Name), Value}|normalize_headers(Tail)]; +normalize_headers([{Name, Value}|Tail]) when is_list(Name) -> + [{string:lowercase(unicode:characters_to_binary(Name)), Value}|normalize_headers(Tail)]; +normalize_headers([{Name, Value}|Tail]) when is_atom(Name) -> + [{string:lowercase(atom_to_binary(Name, latin1)), Value}|normalize_headers(Tail)]; +normalize_headers(Headers) when is_map(Headers) -> + normalize_headers(maps:to_list(Headers)). + %% Streaming data. -spec data(pid(), reference(), fin | nofin, iodata()) -> ok. @@ -449,11 +462,11 @@ data(ServerPid, StreamRef, IsFin, Data) -> connect(ServerPid, Destination) -> connect(ServerPid, Destination, [], #{}). --spec connect(pid(), connect_destination(), headers()) -> reference(). +-spec connect(pid(), connect_destination(), req_headers()) -> reference(). connect(ServerPid, Destination, Headers) -> connect(ServerPid, Destination, Headers, #{}). --spec connect(pid(), connect_destination(), headers(), req_opts()) -> reference(). +-spec connect(pid(), connect_destination(), req_headers(), req_opts()) -> reference(). connect(ServerPid, Destination, Headers, ReqOpts) -> StreamRef = make_ref(), ReplyTo = maps:get(reply_to, ReqOpts, self()), @@ -639,13 +652,13 @@ cancel(ServerPid, StreamRef) -> ws_upgrade(ServerPid, Path) -> ws_upgrade(ServerPid, Path, []). --spec ws_upgrade(pid(), iodata(), headers()) -> reference(). +-spec ws_upgrade(pid(), iodata(), req_headers()) -> reference(). ws_upgrade(ServerPid, Path, Headers) -> StreamRef = make_ref(), gen_statem:cast(ServerPid, {ws_upgrade, self(), StreamRef, Path, Headers}), StreamRef. --spec ws_upgrade(pid(), iodata(), headers(), ws_opts()) -> reference(). +-spec ws_upgrade(pid(), iodata(), req_headers(), ws_opts()) -> reference(). ws_upgrade(ServerPid, Path, Headers, Opts) -> ok = gun_ws:check_options(Opts), StreamRef = make_ref(), diff --git a/test/gun_SUITE.erl b/test/gun_SUITE.erl index 957d2df..dd7b083 100644 --- a/test/gun_SUITE.erl +++ b/test/gun_SUITE.erl @@ -28,6 +28,19 @@ all() -> %% Tests. +atom_header_name(_) -> + doc("Header names may be given as atom."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + _ = gun:get(Pid, "/", [ + {'User-Agent', "Gun/atom-headers"} + ]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/atom-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). + atom_hostname(_) -> doc("Hostnames may be given as atom."), {ok, OriginPid, OriginPort} = init_origin(tcp, http), @@ -207,6 +220,32 @@ killed_streams_http(_) -> error(timeout) end. +list_header_name(_) -> + doc("Header names may be given as list."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + _ = gun:get(Pid, "/", [ + {"User-Agent", "Gun/list-headers"} + ]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/list-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). + +map_headers(_) -> + doc("Header names may be given as a map."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + _ = gun:get(Pid, "/", #{ + <<"USER-agent">> => "Gun/map-headers" + }), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/map-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). + reply_to(_) -> doc("The reply_to option allows using a separate process for requests."), do_reply_to(http), @@ -396,3 +435,16 @@ do_unix_socket_connect() -> after 250 -> error(timeout) end. + +uppercase_header_name(_) -> + doc("Header names may be given with uppercase characters."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + _ = gun:get(Pid, "/", [ + {<<"USER-agent">>, "Gun/uppercase-headers"} + ]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/uppercase-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). |