diff --git a/include/hackney.hrl b/include/hackney.hrl index 17a8a6bb..1b957ee7 100644 --- a/include/hackney.hrl +++ b/include/hackney.hrl @@ -46,6 +46,7 @@ partial_headers = [], version, clen = nil, + ce = nil, te = nil, connection = nil, method = nil, diff --git a/src/hackney.erl b/src/hackney.erl index 48966858..33938493 100644 --- a/src/hackney.erl +++ b/src/hackney.erl @@ -94,6 +94,7 @@ cancel_request(Ref) -> %% @doc set client options. %% Options are: +%% - `compress': request compression and transparently decompress %% - `async': to fetch the response asynchronously %% - `{async, once}': to receive the response asynchronously one time. %% To receive the next message use the function `hackney:stream_next/1'. @@ -211,6 +212,8 @@ request(Method, URL, Headers, Body) -> %% directly. The response is `{ok, Status, Headers, Body}' %%
  • `max_body': sets maximum allowed size of the body if %% with_body is true
  • +%%
  • `compress': request that the server sends the body compressed +%% and instructs hackney to transparently decompress it
  • %%
  • `async': receive the response asynchronously %% The function return {ok, StreamRef}. %% When {async, once} is used the response will be received only once. To diff --git a/src/hackney_request.erl b/src/hackney_request.erl index 612a1707..5f12cd88 100644 --- a/src/hackney_request.erl +++ b/src/hackney_request.erl @@ -61,30 +61,36 @@ perform(Client0, {Method0, Path0, Headers0, Body0}) -> %% add host eventually Headers2 = maybe_add_host(Headers1, Client0#client.netloc), + %% overwrite always as we control Accept-Encoding when 'compress' is set + Headers3 = case proplists:get_value(compress, Options, false) of + true -> hackney_headers_new:store(<<"Accept-Encoding">>, <<"gzip, deflate">>, Headers2); + false -> Headers2 + end, + %% get expect headers - Expect = expectation(Headers2), + Expect = expectation(Headers3), %% build headers with the body. {FinalHeaders, ReqType, Body, Client1} = case Body0 of stream -> - {Headers2, ReqType0, stream, Client0}; + {Headers3, ReqType0, stream, Client0}; stream_multipart -> - handle_multipart_body(Headers2, ReqType0, Client0); + handle_multipart_body(Headers3, ReqType0, Client0); {stream_multipart, Size} -> - handle_multipart_body(Headers2, ReqType0, Size, Client0); + handle_multipart_body(Headers3, ReqType0, Size, Client0); {stream_multipart, Size, Boundary} -> - handle_multipart_body(Headers2, ReqType0, + handle_multipart_body(Headers3, ReqType0, Size, Boundary, Client0); <<>> when Method =:= <<"POST">> orelse Method =:= <<"PUT">> -> - handle_body(Headers2, ReqType0, Body0, Client0); + handle_body(Headers3, ReqType0, Body0, Client0); [] when Method =:= <<"POST">> orelse Method =:= <<"PUT">> -> - handle_body(Headers2, ReqType0, Body0, Client0); + handle_body(Headers3, ReqType0, Body0, Client0); <<>> -> - {Headers2, ReqType0, Body0, Client0}; + {Headers3, ReqType0, Body0, Client0}; [] -> - {Headers2, ReqType0, Body0, Client0}; + {Headers3, ReqType0, Body0, Client0}; _ -> - handle_body(Headers2, ReqType0, Body0, Client0) + handle_body(Headers3, ReqType0, Body0, Client0) end, %% build final client record diff --git a/src/hackney_response.erl b/src/hackney_response.erl index d4f7aa32..44499484 100644 --- a/src/hackney_response.erl +++ b/src/hackney_response.erl @@ -111,6 +111,25 @@ wait_headers({headers_complete, Parser}, Client, Status, Headers) -> [hackney, Client#client.host, response_time], ResponseTime), HeadersList = hackney_headers_new:to_list(Headers), + CE = case proplists:get_value(compress, Client#client.options, false) of + true -> + case hackney_headers_new:get_value(<<"content-encoding">>, Headers, nil) of + nil -> nil; + C -> + Z = zlib:open(), + %% inflateInit2 (https://www.zlib.net/manual.html#Advanced) + WindowBits = 15 + if C == <<"gzip">> -> 16; true -> 0 end, + ok = zlib:inflateInit(Z, WindowBits), + ok = case erlang:function_exported(zlib, safeInflate, 2) of + %% OTP-20.0.5 and later + true -> ok; + %% OTP-18.0 and later + false -> zlib:setBufSize(Z, 512 * 1024) + end, + {zlib,Z} + end; + false -> nil + end, TE = hackney_headers_new:get_value(<<"transfer-encoding">>, Headers, nil), CLen = case hackney_headers_new:lookup("content-length", Headers) of [] -> undefined; @@ -122,6 +141,7 @@ wait_headers({headers_complete, Parser}, Client, Status, Headers) -> end, Client2 = Client#client{parser=Parser, headers=Headers, + ce=CE, te=TE, clen=CLen}, {ok, Status, HeadersList, Client2}. @@ -147,17 +167,64 @@ stream_body(Client=#client{parser=Parser, clen=CLen, te=TE}) -> stream_body(Data, #client{parser=Parser}=Client) -> stream_body1(hackney_http:execute(Parser, Data), Client). -stream_body1({more, Parser, Buffer}, Client) -> +stream_body1({ok, Data, Parser}, Client = #client{ce={zlib,Z}}) -> + stream_body2(case stream_body_zlib(Z, Data) of + <<>> -> {more, Parser, <<>>}; + D when is_binary(D) -> {ok, D, Parser}; + E -> {error,E} + end, Client); +stream_body1({done, _Rest}, Client = #client{ce={zlib,_Z}}) -> + stream_body1(done, Client); +stream_body1(done, Client = #client{ce={zlib,Z}, parser=Parser}) -> + stream_body2(case stream_body_zlib(Z, <<>>) of + done -> done; + D when is_binary(D), size(D) > 0 -> {ok, D, Parser}; + E -> {error,E} + end, Client); +stream_body1(Result, Client) -> + stream_body2(Result, Client). + +stream_body_zlib(Z, Data) -> + case erlang:function_exported(zlib, safeInflate, 2) of + true -> + %% OTP-20.0.5 and later + case zlib:safeInflate(Z, Data) of + {continue, []} when Data == <<>> -> + data_error; + {finished, []} when Data == <<>> -> + case (catch zlib:inflateEnd(Z)) of + ok -> done; + _ -> data_error + end; + {_, Output} -> + iolist_to_binary(Output) + end; + false -> + %% OTP-18.0 and later + case zlib:inflateChunk(Z, Data) of + [] when Data == <<>> -> + case (catch zlib:inflateEnd(Z)) of + ok -> done; + _ -> data_error + end; + {more, Decompressed} -> + iolist_to_binary(Decompressed); + Decompressed -> + iolist_to_binary(Decompressed) + end + end. + +stream_body2({more, Parser, Buffer}, Client) -> stream_body_recv(Buffer, Client#client{parser=Parser}); -stream_body1({ok, Data, Parser}, Client) -> +stream_body2({ok, Data, Parser}, Client) -> {ok, Data, Client#client{parser=Parser}}; -stream_body1({done, Rest}, Client) -> +stream_body2({done, Rest}, Client) -> Client2 = end_stream_body(Rest, Client), {done, Client2}; -stream_body1(done, Client) -> +stream_body2(done, Client) -> Client2 = end_stream_body(<<>>, Client), {done, Client2}; -stream_body1(Error, _Client) -> +stream_body2(Error, _Client) -> Error. @@ -277,6 +344,9 @@ skip_body(Client) -> {error, Reason} -> {error, Reason} end. +end_stream_body(Rest, Client = #client{ce={zlib,Z}}) -> + catch zlib:close(Z), + end_stream_body(Rest, Client#client{ce=nil}); end_stream_body(Rest, Client0) -> Client = Client0#client{response_state=done, body_state=done,