Skip to content

Commit 54d35f7

Browse files
committed
feat: improve decompression middleware
closes #598
1 parent 5967b11 commit 54d35f7

File tree

4 files changed

+112
-7
lines changed

4 files changed

+112
-7
lines changed

lib/tesla/middleware/compression.ex

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ defmodule Tesla.Middleware.Compression do
1919
- `:format` - request compression format, `"gzip"` (default) or `"deflate"`
2020
"""
2121

22+
require Logger
23+
2224
@behaviour Tesla.Middleware
2325

2426
@impl Tesla.Middleware
@@ -49,6 +51,26 @@ defmodule Tesla.Middleware.Compression do
4951
end
5052
end
5153

54+
defmacrop brotli_loaded? do
55+
if Code.ensure_loaded?(:brotli) do
56+
true
57+
else
58+
quote do
59+
Code.ensure_loaded?(:brotli)
60+
end
61+
end
62+
end
63+
64+
defmacrop ezstd_loaded? do
65+
if Code.ensure_loaded?(:ezstd) do
66+
true
67+
else
68+
quote do
69+
Code.ensure_loaded?(:ezstd)
70+
end
71+
end
72+
end
73+
5274
defp compress_body(body, "gzip"), do: :zlib.gzip(body)
5375
defp compress_body(body, "deflate"), do: :zlib.zip(body)
5476

@@ -61,13 +83,84 @@ defmodule Tesla.Middleware.Compression do
6183
def decompress({:error, reason}), do: {:error, reason}
6284

6385
def decompress(env) do
86+
codecs = compression_algorithms(Tesla.get_header(env, "content-encoding"))
87+
{decompressed_body, unknown_codecs} = decompress_body(codecs, env.body, [])
88+
89+
env
90+
|> put_decompressed_body(decompressed_body)
91+
|> put_or_delete_content_encoding(unknown_codecs)
92+
end
93+
94+
defp put_or_delete_content_encoding(env, []) do
95+
Tesla.delete_header(env, "content-encoding")
96+
end
97+
98+
defp put_or_delete_content_encoding(env, unknown_codecs) do
99+
Tesla.put_header(env, "content-encoding", Enum.join(unknown_codecs, ", "))
100+
end
101+
102+
defp decompress_body([gzip | rest], body, acc) when gzip in ["gzip", "x-gzip"] do
103+
decompress_body(rest, :zlib.gunzip(body), acc)
104+
end
105+
106+
defp decompress_body(["br" | rest], body, acc) do
107+
if brotli_loaded?() do
108+
{:ok, decompressed} = :brotli.decode(body)
109+
decompress_body(rest, decompressed, acc)
110+
else
111+
Logger.debug(":brotli library not loaded, skipping brotli decompression")
112+
decompress_body(rest, body, ["br" | acc])
113+
end
114+
end
115+
116+
defp decompress_body(["zstd" | rest], body, acc) do
117+
if ezstd_loaded?() do
118+
decompress_body(rest, :ezstd.decompress(body), acc)
119+
else
120+
Logger.debug(":ezstd library not loaded, skipping zstd decompression")
121+
decompress_body(rest, body, ["zstd" | acc])
122+
end
123+
end
124+
125+
defp decompress_body(["identity" | rest], body, acc) do
126+
decompress_body(rest, body, acc)
127+
end
128+
129+
defp decompress_body([codec | rest], body, acc) do
130+
Logger.debug("algorithm #{inspect(codec)} is not supported")
131+
decompress_body(rest, body, [codec | acc])
132+
end
133+
134+
defp decompress_body([], body, acc) do
135+
{body, acc}
136+
end
137+
138+
defp compression_algorithms(nil) do
139+
[]
140+
end
141+
142+
defp compression_algorithms(value) do
143+
value
144+
|> String.downcase()
145+
|> String.split(",", trim: true)
146+
|> Enum.map(&String.trim/1)
147+
|> Enum.reverse()
148+
end
149+
150+
defp put_decompressed_body(env, body) do
151+
content_length =
152+
body
153+
|> byte_size()
154+
|> to_string()
155+
64156
env
65-
|> Tesla.put_body(decompress_body(env.body, Tesla.get_header(env, "content-encoding")))
157+
|> Tesla.put_body(body)
158+
|> Tesla.put_header("content-length", content_length)
66159
end
67160

68161
defp decompress_body(<<31, 139, 8, _::binary>> = body, "gzip"), do: :zlib.gunzip(body)
69162
defp decompress_body(body, "deflate"), do: :zlib.unzip(body)
70-
defp decompress_body(body, _content_encoding), do: body
163+
defp decompress_body(_body, _content_encoding), do: nil
71164
end
72165

73166
defmodule Tesla.Middleware.CompressRequest do

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ defmodule Tesla.Mixfile do
2020
plt_add_apps: [:mix, :inets, :idna, :ssl_verify_fun, :ex_unit],
2121
plt_add_deps: :project
2222
],
23-
docs: docs()
23+
docs: docs(),
24+
preferred_cli_env: [coveralls: :test, "coveralls.html": :test]
2425
]
2526
end
2627

test/support/test_support.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule TestSupport do
2+
def gzip_headers(env) do
3+
env.headers
4+
|> Enum.map_join("|", fn {key, value} -> "#{key}: #{value}" end)
5+
|> :zlib.gzip()
6+
end
7+
end

test/tesla/middleware/compression_test.exs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ defmodule Tesla.Middleware.CompressionTest do
6969

7070
test "decompress response body (gzip)" do
7171
assert {:ok, env} = CompressionResponseClient.get("/response-gzip")
72+
assert env.headers == [{"content-type", "text/plain"}, {"content-length", "17"}]
7273
assert env.body == "decompressed gzip"
7374
end
7475

@@ -80,6 +81,7 @@ defmodule Tesla.Middleware.CompressionTest do
8081
test "return unchanged response for unsupported content-encoding" do
8182
assert {:ok, env} = CompressionResponseClient.get("/response-identity")
8283
assert env.body == "unchanged"
84+
assert env.headers == [{"content-type", "text/plain"}, {"content-encoding", "identity"}]
8385
end
8486

8587
defmodule CompressRequestDecompressResponseClient do
@@ -114,7 +116,8 @@ defmodule Tesla.Middleware.CompressionTest do
114116
{status, headers, body} =
115117
case env.url do
116118
"/" ->
117-
{200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], env.headers}
119+
{200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}],
120+
TestSupport.gzip_headers(env)}
118121
end
119122

120123
{:ok, %{env | status: status, headers: headers, body: body}}
@@ -123,7 +126,7 @@ defmodule Tesla.Middleware.CompressionTest do
123126

124127
test "Compression headers" do
125128
assert {:ok, env} = CompressionHeadersClient.get("/")
126-
assert env.body == [{"accept-encoding", "gzip, deflate"}]
129+
assert env.body == "accept-encoding: gzip, deflate"
127130
end
128131

129132
defmodule DecompressResponseHeadersClient do
@@ -135,7 +138,8 @@ defmodule Tesla.Middleware.CompressionTest do
135138
{status, headers, body} =
136139
case env.url do
137140
"/" ->
138-
{200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], env.headers}
141+
{200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}],
142+
TestSupport.gzip_headers(env)}
139143
end
140144

141145
{:ok, %{env | status: status, headers: headers, body: body}}
@@ -144,7 +148,7 @@ defmodule Tesla.Middleware.CompressionTest do
144148

145149
test "Decompress response headers" do
146150
assert {:ok, env} = DecompressResponseHeadersClient.get("/")
147-
assert env.body == [{"accept-encoding", "gzip, deflate"}]
151+
assert env.body == "accept-encoding: gzip, deflate"
148152
end
149153

150154
defmodule CompressRequestHeadersClient do

0 commit comments

Comments
 (0)