Skip to content

Commit e5270b1

Browse files
committed
Add content-length based body reading
1 parent d5989c8 commit e5270b1

1 file changed

Lines changed: 97 additions & 29 deletions

File tree

lib/bandit/http1_request.ex

Lines changed: 97 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule Bandit.HTTP1Request do
44
@behaviour Plug.Conn.Adapter
55
@behaviour Bandit.HTTPRequest
66

7-
defstruct state: :new, socket: nil, buffer: <<>>, version: nil
7+
defstruct state: :new, socket: nil, buffer: <<>>, body_size: nil, body_encoding: nil, connection: nil, version: nil
88

99
defmodule UnreadHeadersError do
1010
defexception message: "Headers have not been read yet"
@@ -24,42 +24,50 @@ defmodule Bandit.HTTP1Request do
2424
def request(%Socket{} = socket), do: {:ok, __MODULE__, %__MODULE__{socket: socket}}
2525

2626
@impl Bandit.HTTPRequest
27-
def read_headers(req, type \\ :http, headers \\ [], method \\ nil, path \\ nil)
27+
def read_headers(req) do
28+
case do_read_headers(req) do
29+
{:ok, headers, method, path, req} ->
30+
body_size =
31+
case get_header(headers, "content-length") do
32+
nil -> nil
33+
size -> String.to_integer(size)
34+
end
2835

29-
def read_headers(%__MODULE__{state: :new, socket: socket, buffer: buffer} = req, type, headers, method, path) do
30-
case :erlang.decode_packet(type, buffer, []) do
31-
{:more, _len} ->
32-
case Socket.recv(socket) do
33-
{:ok, more_data} -> read_headers(%{req | buffer: buffer <> more_data}, type, headers, method, path)
34-
{:error, reason} -> {:error, reason}
35-
end
36+
body_encoding = get_header(headers, "content-encoding")
37+
connection = get_header(headers, "connection")
3638

37-
{:ok, {:http_request, method, {:abs_path, path}, version}, rest} ->
38-
read_headers(%{req | buffer: rest, version: version(version)}, :httph, headers, method, path)
39-
40-
{:ok, {:http_header, _, header, _, value}, rest} ->
41-
read_headers(
42-
%{req | buffer: rest},
43-
:httph,
44-
[{header |> to_string() |> String.downcase(), to_string(value)} | headers],
45-
to_string(method),
46-
to_string(path)
47-
)
48-
49-
{:ok, :http_eoh, rest} ->
50-
{:ok, headers, to_string(method), to_string(path), %{req | state: :headers_read, buffer: rest}}
39+
{:ok, headers, method, path,
40+
%{req | body_size: body_size, body_encoding: body_encoding, connection: connection}}
5141

5242
{:error, reason} ->
5343
{:error, reason}
5444
end
5545
end
5646

57-
def read_headers(%__MODULE__{}, _, _, _, _), do: raise(AlreadyReadError)
58-
5947
@impl Plug.Conn.Adapter
60-
def read_req_body(%__MODULE__{state: :headers_read} = req, _opts) do
61-
# TODO
62-
{:ok, <<>>, %{req | state: :body_read}}
48+
def read_req_body(%__MODULE__{state: :headers_read, body_size: nil, body_encoding: nil} = req, _opts) do
49+
{:ok, nil, req}
50+
end
51+
52+
# TODO handle chunked encoding as a thing
53+
54+
def read_req_body(%__MODULE__{state: :headers_read, buffer: buffer, body_size: body_size} = req, opts)
55+
when is_number(body_size) do
56+
to_read = min(body_size, Keyword.get(opts, :length, 8_000_000)) - byte_size(buffer)
57+
58+
case do_read_req_body_by_size(req, to_read, opts) do
59+
{:ok, %__MODULE__{buffer: buffer} = req} ->
60+
remaining_bytes = body_size - byte_size(buffer)
61+
62+
if remaining_bytes > 0 do
63+
{:more, buffer, %{req | buffer: <<>>, body_size: remaining_bytes}}
64+
else
65+
{:ok, buffer, %{req | state: :body_read, buffer: <<>>, body_size: 0}}
66+
end
67+
68+
{:error, reason} ->
69+
{:error, reason}
70+
end
6371
end
6472

6573
def read_req_body(%__MODULE__{state: :new}, _opts), do: raise(UnreadHeadersError)
@@ -119,7 +127,46 @@ defmodule Bandit.HTTP1Request do
119127
def get_http_protocol(%__MODULE__{version: version}), do: version
120128

121129
@impl Bandit.HTTPRequest
122-
def keepalive?(%__MODULE__{version: version}), do: version == "HTTP/1.1"
130+
def keepalive?(%__MODULE__{version: version}), do: version == :"HTTP/1.1"
131+
132+
defp do_read_headers(req, type \\ :http, headers \\ [], method \\ nil, path \\ nil)
133+
134+
defp do_read_headers(%__MODULE__{state: :new, socket: socket, buffer: buffer} = req, type, headers, method, path) do
135+
case :erlang.decode_packet(type, buffer, []) do
136+
{:more, _len} ->
137+
case Socket.recv(socket) do
138+
{:ok, more_data} -> do_read_headers(%{req | buffer: buffer <> more_data}, type, headers, method, path)
139+
{:error, reason} -> {:error, reason}
140+
end
141+
142+
{:ok, {:http_request, method, {:abs_path, path}, version}, rest} ->
143+
do_read_headers(%{req | buffer: rest, version: version(version)}, :httph, headers, method, path)
144+
145+
{:ok, {:http_header, _, header, _, value}, rest} ->
146+
do_read_headers(
147+
%{req | buffer: rest},
148+
:httph,
149+
[{header |> to_string() |> String.downcase(), to_string(value)} | headers],
150+
to_string(method),
151+
to_string(path)
152+
)
153+
154+
{:ok, :http_eoh, rest} ->
155+
{:ok, headers, method, path, %{req | state: :headers_read, buffer: rest}}
156+
157+
{:error, reason} ->
158+
{:error, reason}
159+
end
160+
end
161+
162+
defp do_read_headers(%__MODULE__{}, _, _, _, _), do: raise(AlreadyReadError)
163+
164+
defp get_header(headers, header, default \\ nil) do
165+
case List.keyfind(headers, header, 0) do
166+
{_, value} -> value
167+
nil -> default
168+
end
169+
end
123170

124171
defp format_headers(headers, body) do
125172
[{"content-length", body |> byte_size() |> to_string()} | headers]
@@ -128,4 +175,25 @@ defmodule Bandit.HTTP1Request do
128175

129176
defp version({1, 1}), do: :"HTTP/1.1"
130177
defp version({1, 0}), do: :"HTTP/1.0"
178+
179+
defp do_read_req_body_by_size(%__MODULE__{} = req, 0, _opts), do: {:ok, req}
180+
181+
defp do_read_req_body_by_size(%__MODULE__{socket: socket, buffer: buffer} = req, to_read, opts) do
182+
read_size = min(to_read, Keyword.get(opts, :read_length, 1_000_000))
183+
read_timeout = Keyword.get(opts, :read_timeout, 15_000)
184+
185+
case Socket.recv(socket, read_size, read_timeout) do
186+
{:ok, chunk} ->
187+
remaining_bytes = to_read - byte_size(chunk)
188+
189+
if remaining_bytes > 0 do
190+
do_read_req_body_by_size(%{req | buffer: buffer <> chunk}, remaining_bytes, opts)
191+
else
192+
{:ok, %{req | buffer: buffer <> chunk}}
193+
end
194+
195+
{:error, reason} ->
196+
{:error, reason}
197+
end
198+
end
131199
end

0 commit comments

Comments
 (0)