From aa713445cd6b459ec9397824d07ac0dd961ad513 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 8 Oct 2023 16:51:36 -0400 Subject: [PATCH] feat: improve decompression middleware (#606) closes #598 Signed-off-by: Yordis Prieto --- lib/tesla/middleware/compression.ex | 63 +++++++++++++++++++--- mix.exs | 3 +- test/support/test_support.ex | 7 +++ test/tesla/middleware/compression_test.exs | 12 +++-- 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 test/support/test_support.ex diff --git a/lib/tesla/middleware/compression.ex b/lib/tesla/middleware/compression.ex index c2cb24ad..5ba1fcd0 100644 --- a/lib/tesla/middleware/compression.ex +++ b/lib/tesla/middleware/compression.ex @@ -25,11 +25,16 @@ defmodule Tesla.Middleware.Compression do def call(env, next, opts) do env |> compress(opts) - |> Tesla.put_headers([{"accept-encoding", "gzip, deflate"}]) + |> add_accept_encoding() |> Tesla.run(next) |> decompress() end + @doc false + def add_accept_encoding(env) do + Tesla.put_headers(env, [{"accept-encoding", "gzip, deflate, identity"}]) + end + defp compressible?(body), do: is_binary(body) @doc """ @@ -61,13 +66,59 @@ defmodule Tesla.Middleware.Compression do def decompress({:error, reason}), do: {:error, reason} def decompress(env) do + codecs = compression_algorithms(Tesla.get_header(env, "content-encoding")) + {decompressed_body, unknown_codecs} = decompress_body(codecs, env.body, []) + env - |> Tesla.put_body(decompress_body(env.body, Tesla.get_header(env, "content-encoding"))) + |> put_decompressed_body(decompressed_body) + |> put_or_delete_content_encoding(unknown_codecs) + end + + defp put_or_delete_content_encoding(env, []) do + Tesla.delete_header(env, "content-encoding") + end + + defp put_or_delete_content_encoding(env, unknown_codecs) do + Tesla.put_header(env, "content-encoding", Enum.join(unknown_codecs, ", ")) + end + + defp decompress_body([gzip | rest], body, acc) when gzip in ["gzip", "x-gzip"] do + decompress_body(rest, :zlib.gunzip(body), acc) end - defp decompress_body(<<31, 139, 8, _::binary>> = body, "gzip"), do: :zlib.gunzip(body) - defp decompress_body(body, "deflate"), do: :zlib.unzip(body) - defp decompress_body(body, _content_encoding), do: body + defp decompress_body(["deflate" | rest], body, acc) do + decompress_body(rest, :zlib.unzip(body), acc) + end + + defp decompress_body(["identity" | rest], body, acc) do + decompress_body(rest, body, acc) + end + + defp decompress_body([codec | rest], body, acc) do + decompress_body(rest, body, [codec | acc]) + end + + defp decompress_body([], body, acc) do + {body, acc} + end + + defp compression_algorithms(nil) do + [] + end + + defp compression_algorithms(value) do + value + |> String.downcase() + |> String.split(",", trim: true) + |> Enum.map(&String.trim/1) + |> Enum.reverse() + end + + defp put_decompressed_body(env, body) do + env + |> Tesla.put_body(body) + |> Tesla.delete_header("content-length") + end end defmodule Tesla.Middleware.CompressRequest do @@ -99,7 +150,7 @@ defmodule Tesla.Middleware.DecompressResponse do @impl Tesla.Middleware def call(env, next, _opts) do env - |> Tesla.put_headers([{"accept-encoding", "gzip, deflate"}]) + |> Tesla.Middleware.Compression.add_accept_encoding() |> Tesla.run(next) |> Tesla.Middleware.Compression.decompress() end diff --git a/mix.exs b/mix.exs index b872eff0..15635e1c 100644 --- a/mix.exs +++ b/mix.exs @@ -20,7 +20,8 @@ defmodule Tesla.Mixfile do plt_add_apps: [:mix, :inets, :idna, :ssl_verify_fun, :ex_unit], plt_add_deps: :project ], - docs: docs() + docs: docs(), + preferred_cli_env: [coveralls: :test, "coveralls.html": :test] ] end diff --git a/test/support/test_support.ex b/test/support/test_support.ex new file mode 100644 index 00000000..cdd0f17d --- /dev/null +++ b/test/support/test_support.ex @@ -0,0 +1,7 @@ +defmodule TestSupport do + def gzip_headers(env) do + env.headers + |> Enum.map_join("|", fn {key, value} -> "#{key}: #{value}" end) + |> :zlib.gzip() + end +end diff --git a/test/tesla/middleware/compression_test.exs b/test/tesla/middleware/compression_test.exs index a82ecfe7..47aa0aba 100644 --- a/test/tesla/middleware/compression_test.exs +++ b/test/tesla/middleware/compression_test.exs @@ -69,6 +69,7 @@ defmodule Tesla.Middleware.CompressionTest do test "decompress response body (gzip)" do assert {:ok, env} = CompressionResponseClient.get("/response-gzip") + assert env.headers == [{"content-type", "text/plain"}] assert env.body == "decompressed gzip" end @@ -80,6 +81,7 @@ defmodule Tesla.Middleware.CompressionTest do test "return unchanged response for unsupported content-encoding" do assert {:ok, env} = CompressionResponseClient.get("/response-identity") assert env.body == "unchanged" + assert env.headers == [{"content-type", "text/plain"}] end defmodule CompressRequestDecompressResponseClient do @@ -114,7 +116,8 @@ defmodule Tesla.Middleware.CompressionTest do {status, headers, body} = case env.url do "/" -> - {200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], env.headers} + {200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], + TestSupport.gzip_headers(env)} end {:ok, %{env | status: status, headers: headers, body: body}} @@ -123,7 +126,7 @@ defmodule Tesla.Middleware.CompressionTest do test "Compression headers" do assert {:ok, env} = CompressionHeadersClient.get("/") - assert env.body == [{"accept-encoding", "gzip, deflate"}] + assert env.body == "accept-encoding: gzip, deflate, identity" end defmodule DecompressResponseHeadersClient do @@ -135,7 +138,8 @@ defmodule Tesla.Middleware.CompressionTest do {status, headers, body} = case env.url do "/" -> - {200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], env.headers} + {200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], + TestSupport.gzip_headers(env)} end {:ok, %{env | status: status, headers: headers, body: body}} @@ -144,7 +148,7 @@ defmodule Tesla.Middleware.CompressionTest do test "Decompress response headers" do assert {:ok, env} = DecompressResponseHeadersClient.get("/") - assert env.body == [{"accept-encoding", "gzip, deflate"}] + assert env.body == "accept-encoding: gzip, deflate, identity" end defmodule CompressRequestHeadersClient do