Skip to content

Commit fa0c87b

Browse files
committed
fix: update content-length header after decompression
Or keep the original for HEAD requests. HEAD requests can be used to check the size of remote content to decide ahead of time whether it is worth fetching. Of course there’ll be some differences between the compressed and ucompressed size later, but depending on usecase only the transfer size might be relevant. Furthermore, content-length is mandatory in HTTP1.0 and in HTTP1.1 mandatory except for chunked transfers. Thus deleting the header after decompression might be unexpected for later processing steps. Instead, update the header with the decompressed size. TODO: - Question: keep empty body clause in decompression - Question: can body be non-binary (for update content-length function)
1 parent be8a573 commit fa0c87b

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed

lib/tesla/middleware/compression.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ defmodule Tesla.Middleware.Compression do
6767
def decompress({:ok, env}), do: {:ok, decompress(env)}
6868
def decompress({:error, reason}), do: {:error, reason}
6969

70+
# HEAD requests may be used to obtain information on the transfer size and properties
71+
# and their empty bodies are not actually valid for the possibly indicated encodings
72+
# thus we want to preserve them unchanged.
73+
def decompress(%Tesla.Env{method: :head} = env), do: env
74+
7075
def decompress(env) do
7176
codecs = compression_algorithms(Tesla.get_header(env, "content-encoding"))
7277
{decompressed_body, unknown_codecs} = decompress_body(codecs, env.body)
@@ -123,7 +128,27 @@ defmodule Tesla.Middleware.Compression do
123128
defp put_decompressed_body(env, body) do
124129
env
125130
|> Tesla.put_body(body)
126-
|> Tesla.delete_header("content-length")
131+
|> update_content_length(body)
132+
end
133+
134+
# The value of the content-length header wil be inaccurate after decompression.
135+
# But setting it is mandatory or strongly encouraged in HTTP/1.0 and HTTP/1.1.
136+
# Except, when transfer-encoding is used defining content-length is invalid.
137+
# Thus we can neither just drop it nor indiscriminately add it, but will update it if it already exist.
138+
# Furthermore, content-length is technically allowed to be specified mutliple times if all values match,
139+
# to ensure consistency we must therefore make sure to drop any duplicate definitions while updating.
140+
defp update_content_length(env, body) when is_binary(body) do
141+
if Tesla.get_header(env, "content-length") != nil do
142+
env
143+
|> Tesla.delete_header("content-length")
144+
|> Tesla.put_header("content-length", "#{byte_size(body)}")
145+
else
146+
env
147+
end
148+
end
149+
150+
defp update_content_length(env, _) do
151+
env
127152
end
128153
end
129154

test/tesla/middleware/compression_test.exs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ defmodule Tesla.Middleware.CompressionTest do
6868

6969
"/response-empty" ->
7070
{200, [{"content-type", "text/plain"}, {"content-encoding", "gzip"}], ""}
71+
72+
"/response-with-content-length" ->
73+
body = :zlib.gzip("decompressed gzip")
74+
75+
{200,
76+
[
77+
{"content-type", "text/plain"},
78+
{"content-encoding", "gzip"},
79+
{"content-length", "#{byte_size(body)}"}
80+
], body}
81+
82+
"/response-empty-with-content-length" ->
83+
{200,
84+
[
85+
{"content-type", "text/plain"},
86+
{"content-encoding", "gzip"},
87+
{"content-length", "4194304"}
88+
], ""}
7189
end
7290

7391
{:ok, %{env | status: status, headers: headers, body: body}}
@@ -103,6 +121,28 @@ defmodule Tesla.Middleware.CompressionTest do
103121
assert env.headers == [{"content-type", "text/plain"}]
104122
end
105123

124+
test "updates existing content-length header" do
125+
expected_body = "decompressed gzip"
126+
assert {:ok, env} = CompressionResponseClient.get("/response-with-content-length")
127+
assert env.body == expected_body
128+
129+
assert env.headers == [
130+
{"content-type", "text/plain"},
131+
{"content-length", "#{byte_size(expected_body)}"}
132+
]
133+
end
134+
135+
test "preserves compression headers for HEAD requests" do
136+
assert {:ok, env} = CompressionResponseClient.head("/response-empty-with-content-length")
137+
assert env.body == ""
138+
139+
assert env.headers == [
140+
{"content-type", "text/plain"},
141+
{"content-encoding", "gzip"},
142+
{"content-length", "4194304"}
143+
]
144+
end
145+
106146
defmodule CompressRequestDecompressResponseClient do
107147
use Tesla
108148

0 commit comments

Comments
 (0)