Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions test/finch/alpn_integration_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
defmodule Finch.ALPNIntegrationTest do
use ExUnit.Case, async: false

@moduletag :capture_log

setup_all do
{:ok, listen_socket} = :ssl.listen(0, mode: :binary)
{:ok, {_address, port}} = :ssl.sockname(listen_socket)
:ssl.close(listen_socket)

{:ok, _} = Finch.ALPNServer.start(port)

{:ok, url: "https://localhost:#{port}"}
end

# This test reproduces issue #265 where sending POST requests with bodies larger than 64KB
# fails when using protocols: [:http1, :http2] due to HTTP/2 window size constraints.
# The HTTP/1 pool doesn't implement HTTP/2 flow control, causing the request to exceed
# the HTTP/2 window size (65535 bytes).
@tag :skip
test "POST request with body larger than 64KB using ALPN negotiation", %{url: url} do
# Start Finch with ALPN negotiation (both HTTP/1 and HTTP/2)
start_supervised!(
{Finch,
name: ALPNFinch,
pools: %{
default: [
protocols: [:http1, :http2],
conn_opts: [
transport_opts: [
verify: :verify_none
]
]
]
}}
)

# Create a body larger than 64KB (65538 bytes as per issue)
large_body = :crypto.strong_rand_bytes(65_538)

# This should fail with {:exceeds_window_size, :request, 65535}
# when the connection upgrades to HTTP/2 via ALPN
result =
Finch.build(:post, "#{url}/echo", [], large_body)
|> Finch.request(ALPNFinch)

# Currently this fails with the window size error
# Once fixed, this should succeed
case result do
{:ok, response} ->
assert response.status == 200
body = Jason.decode!(response.body)
assert body["received_bytes"] == 65_538

{:error, %Mint.HTTPError{reason: {:exceeds_window_size, :request, 65_535}}} ->
flunk("""
Request failed with window size error - this is the bug we're trying to fix.
The HTTP/1 pool doesn't implement HTTP/2 flow control when connections upgrade via ALPN.
""")

{:error, error} ->
flunk("Unexpected error: #{inspect(error)}")
end
end

# Test that confirms HTTP/2-only works correctly with large bodies
test "POST request with body larger than 64KB using HTTP/2 only (should work)", %{url: url} do
start_supervised!(
{Finch,
name: HTTP2Finch,
pools: %{
default: [
protocols: [:http2],
conn_opts: [
transport_opts: [
verify: :verify_none
]
]
]
}}
)

large_body = :crypto.strong_rand_bytes(65_538)

{:ok, response} =
Finch.build(:post, "#{url}/echo", [], large_body)
|> Finch.request(HTTP2Finch)

assert response.status == 200
body = Jason.decode!(response.body)
assert body["received_bytes"] == 65_538
end

# Test that confirms HTTP/1-only works correctly with large bodies
test "POST request with body larger than 64KB using HTTP/1 only (should work)", %{url: url} do
start_supervised!(
{Finch,
name: HTTP1Finch,
pools: %{
default: [
protocols: [:http1],
conn_opts: [
transport_opts: [
verify: :verify_none
]
]
]
}}
)

large_body = :crypto.strong_rand_bytes(65_538)

{:ok, response} =
Finch.build(:post, "#{url}/echo", [], large_body)
|> Finch.request(HTTP1Finch)

assert response.status == 200
body = Jason.decode!(response.body)
assert body["received_bytes"] == 65_538
end
end
60 changes: 60 additions & 0 deletions test/support/alpn_server.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule Finch.ALPNServer do
@moduledoc false
# A test server that supports ALPN negotiation between HTTP/1 and HTTP/2

@fixtures_dir Path.expand("../fixtures", __DIR__)

def child_spec(opts) do
Plug.Cowboy.child_spec(
scheme: :https,
plug: Finch.ALPNServer.PlugRouter,
options: [
port: Keyword.fetch!(opts, :port),
cipher_suite: :strong,
certfile: Path.join([@fixtures_dir, "selfsigned.pem"]),
keyfile: Path.join([@fixtures_dir, "selfsigned_key.pem"]),
# Enable ALPN negotiation between HTTP/2 and HTTP/1.1
alpn_preferred_protocols: ["h2", "http/1.1"],
otp_app: :finch,
protocol_options: [
idle_timeout: 3_000,
inactivity_timeout: 5_000,
max_keepalive: 1_000,
request_timeout: 10_000,
shutdown_timeout: 10_000
]
]
)
end

def start(port) do
Supervisor.start_link([child_spec(port: port)], strategy: :one_for_one)
end
end

defmodule Finch.ALPNServer.PlugRouter do
@moduledoc false

use Plug.Router

plug(:match)
plug(:dispatch)

get "/" do
conn
|> send_resp(200, "Hello world!")
|> halt()
end

post "/echo" do
{:ok, body, conn} = read_body(conn)
body_size = byte_size(body)

response = Jason.encode!(%{received_bytes: body_size})

conn
|> put_resp_content_type("application/json")
|> send_resp(200, response)
|> halt()
end
end
Loading