Skip to content

Commit 767c4c0

Browse files
committed
feat: polymorphic helper for request body streaming
1 parent d3fee6e commit 767c4c0

10 files changed

Lines changed: 159 additions & 1 deletion

File tree

.dialyzer_ignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
lib/mint/tunnel_proxy.ex:49
2-
lib/mint/http1.ex:915
2+
lib/mint/http1.ex:927
33
lib/mint/unsafe_proxy.ex:173
44
lib/mint/unsafe_proxy.ex:198
55
test/support

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### New features
6+
7+
* Add `Mint.HTTP.request_body_window/2` to inspect the request body flow-control window for a streaming request. Returns `min(connection_window, stream_window)` for HTTP/2 and `:infinity` for HTTP/1, which has no application-level flow control.
8+
39
## v1.7.1
410

511
### Bug Fixes and Improvements

lib/mint/core/conn.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,6 @@ defmodule Mint.Core.Conn do
6262
@callback put_proxy_headers(conn(), Mint.Types.headers()) :: conn()
6363

6464
@callback put_log(conn(), boolean()) :: conn()
65+
66+
@callback request_body_window(conn(), Types.request_ref()) :: non_neg_integer() | :infinity
6567
end

lib/mint/http.ex

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ defmodule Mint.HTTP do
623623
624624
This function always returns an updated connection to be stored over the old connection.
625625
626+
When streaming a body of arbitrary size, use `request_body_window/2` to learn
627+
how many bytes you can send right now without violating HTTP/2 flow control,
628+
then split your body accordingly before passing each chunk to this function.
629+
626630
For information about transfer encoding and content length in HTTP/1, see
627631
`Mint.HTTP1.stream_request_body/3`.
628632
@@ -1065,6 +1069,53 @@ defmodule Mint.HTTP do
10651069
@impl true
10661070
def put_proxy_headers(conn, headers), do: conn_apply(conn, :put_proxy_headers, [conn, headers])
10671071

1072+
@doc """
1073+
Returns the request body flow-control window for the streaming request
1074+
identified by `request_ref`.
1075+
1076+
The semantics differ by protocol:
1077+
1078+
* In HTTP/2, returns `min(connection_window, stream_window)` — the maximum
1079+
number of body bytes that can be sent right now without violating flow
1080+
control. Exceeding this value in a single `DATA` frame would close the
1081+
connection with a `FLOW_CONTROL_ERROR`. See `Mint.HTTP2.get_window_size/2`
1082+
for the underlying primitives.
1083+
1084+
* In HTTP/1, returns `:infinity`. HTTP/1 has no application-level
1085+
flow-control mechanism: any amount of body data is protocol-valid. The
1086+
operating-system socket send buffer may still cause `stream_request_body/3`
1087+
to block once it fills up, but that is a transport concern and not
1088+
reflected here.
1089+
1090+
Raises `ArgumentError` if `request_ref` is not associated with an active
1091+
streaming request.
1092+
1093+
## Examples
1094+
1095+
Streaming a binary body in chunks that respect the protocol window:
1096+
1097+
defp stream_body(conn, ref, "") do
1098+
Mint.HTTP.stream_request_body(conn, ref, :eof)
1099+
end
1100+
1101+
defp stream_body(conn, ref, body) do
1102+
chunk_size = min(Mint.HTTP.request_body_window(conn, ref), byte_size(body))
1103+
<<chunk::binary-size(chunk_size), rest::binary>> = body
1104+
1105+
with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, ref, chunk) do
1106+
stream_body(conn, ref, rest)
1107+
end
1108+
end
1109+
1110+
Note that `min(:infinity, n) == n` thanks to Erlang term ordering, so the
1111+
same loop works on HTTP/1 (each iteration sends the entire remaining body in
1112+
a single chunk) and on HTTP/2 (each iteration sends at most the current
1113+
flow-control window).
1114+
"""
1115+
@doc since: "1.8.0"
1116+
@impl true
1117+
def request_body_window(conn, ref), do: conn_apply(conn, :request_body_window, [conn, ref])
1118+
10681119
## Helpers
10691120

10701121
defp conn_apply(%UnsafeProxy{}, fun, args), do: apply(UnsafeProxy, fun, args)

lib/mint/http1.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,18 @@ defmodule Mint.HTTP1 do
667667
%{conn | proxy_headers: headers}
668668
end
669669

670+
@doc """
671+
See `Mint.HTTP.request_body_window/2`.
672+
"""
673+
@doc since: "1.8.0"
674+
@impl true
675+
def request_body_window(%__MODULE__{streaming_request: %{ref: ref}}, ref), do: :infinity
676+
677+
def request_body_window(%__MODULE__{}, ref) do
678+
raise ArgumentError,
679+
"request with request reference #{inspect(ref)} was not found or is not streaming a body"
680+
end
681+
670682
## Helpers
671683

672684
defp decode(:status, %{request: request} = conn, data, responses) do

lib/mint/http2.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,15 @@ defmodule Mint.HTTP2 do
10391039
%{conn | proxy_headers: headers}
10401040
end
10411041

1042+
@doc """
1043+
See `Mint.HTTP.request_body_window/2`.
1044+
"""
1045+
@doc since: "1.8.0"
1046+
@impl true
1047+
def request_body_window(%__MODULE__{} = conn, ref) do
1048+
min(get_window_size(conn, :connection), get_window_size(conn, {:request, ref}))
1049+
end
1050+
10421051
## Helpers
10431052

10441053
defp handle_closed(conn) do

lib/mint/unsafe_proxy.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,9 @@ defmodule Mint.UnsafeProxy do
199199
def put_proxy_headers(%__MODULE__{}, _headers) do
200200
raise "invalid function for proxy unsafe proxy connections"
201201
end
202+
203+
@impl true
204+
def request_body_window(%__MODULE__{module: module, state: state}, ref) do
205+
module.request_body_window(state, ref)
206+
end
202207
end

test/http_test.exs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
defmodule Mint.HTTPTest do
22
use ExUnit.Case, async: true
33
doctest Mint.HTTP
4+
5+
alias Mint.{HTTP, HTTP1.TestServer}
6+
7+
setup do
8+
{:ok, port, server_ref} = TestServer.start()
9+
assert {:ok, conn} = HTTP.connect(:http, "localhost", port)
10+
assert_receive {^server_ref, server_socket}
11+
12+
[conn: conn, server_socket: server_socket]
13+
end
14+
15+
describe "request_body_window/2" do
16+
test "returns :infinity for an HTTP/1 streaming request", %{conn: conn} do
17+
{:ok, conn, ref} = HTTP.request(conn, "GET", "/", [], :stream)
18+
assert HTTP.request_body_window(conn, ref) == :infinity
19+
end
20+
21+
test "raises ArgumentError for an unknown request ref", %{conn: conn} do
22+
assert_raise ArgumentError, fn ->
23+
HTTP.request_body_window(conn, make_ref())
24+
end
25+
end
26+
end
427
end

test/mint/http1/conn_test.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,27 @@ defmodule Mint.HTTP1Test do
11511151
{:ok, conn, responses}
11521152
end
11531153

1154+
describe "request_body_window/2" do
1155+
test "returns :infinity for an active streaming request", %{conn: conn} do
1156+
{:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], :stream)
1157+
assert HTTP1.request_body_window(conn, ref) == :infinity
1158+
end
1159+
1160+
test "raises if no request is currently streaming a body", %{conn: conn} do
1161+
assert_raise ArgumentError, ~r/was not found or is not streaming a body/, fn ->
1162+
HTTP1.request_body_window(conn, make_ref())
1163+
end
1164+
end
1165+
1166+
test "raises if the ref does not match the active streaming request", %{conn: conn} do
1167+
{:ok, conn, _ref} = HTTP1.request(conn, "GET", "/", [], :stream)
1168+
1169+
assert_raise ArgumentError, ~r/was not found or is not streaming a body/, fn ->
1170+
HTTP1.request_body_window(conn, make_ref())
1171+
end
1172+
end
1173+
end
1174+
11541175
@mint_user_agent "mint/#{Mix.Project.config()[:version]}"
11551176
defp mint_user_agent, do: @mint_user_agent
11561177
end

test/mint/http2/conn_test.exs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,35 @@ defmodule Mint.HTTP2Test do
17721772
HTTP2.get_window_size(conn, {:request, make_ref()})
17731773
end
17741774
end
1775+
1776+
test "request_body_window/2 returns the minimum of connection and request window sizes",
1777+
%{conn: conn} do
1778+
{conn, ref} = open_request(conn, :stream)
1779+
1780+
send_window = HTTP2.request_body_window(conn, ref)
1781+
conn_window = HTTP2.get_window_size(conn, :connection)
1782+
request_window = HTTP2.get_window_size(conn, {:request, ref})
1783+
1784+
assert send_window == min(conn_window, request_window)
1785+
end
1786+
1787+
test "request_body_window/2 decreases after streaming body data", %{conn: conn} do
1788+
{conn, ref} = open_request(conn, :stream)
1789+
1790+
initial_send_window = HTTP2.request_body_window(conn, ref)
1791+
assert initial_send_window > 0
1792+
1793+
body_chunk = "hello"
1794+
{:ok, conn} = HTTP2.stream_request_body(conn, ref, body_chunk)
1795+
1796+
assert HTTP2.request_body_window(conn, ref) == initial_send_window - byte_size(body_chunk)
1797+
end
1798+
1799+
test "request_body_window/2 raises if the request is not found", %{conn: conn} do
1800+
assert_raise ArgumentError, ~r/request with request reference .+ was not found/, fn ->
1801+
HTTP2.request_body_window(conn, make_ref())
1802+
end
1803+
end
17751804
end
17761805

17771806
describe "settings" do

0 commit comments

Comments
 (0)