Skip to content

feat: polymorphic helper for request body streaming#484

Closed
tank-bohr wants to merge 1 commit intoelixir-mint:mainfrom
tank-bohr:next_body_chunk
Closed

feat: polymorphic helper for request body streaming#484
tank-bohr wants to merge 1 commit intoelixir-mint:mainfrom
tank-bohr:next_body_chunk

Conversation

@tank-bohr
Copy link
Copy Markdown
Contributor

Continues the discussion from the #477

Before

defmodule MintHttpStreaming do
  @default_chunk_size 16_384

  def run(method, %URI{scheme: scheme} = uri, headers, body, opts)
      when scheme in ~w[http https] do
    scheme = String.to_existing_atom(scheme)
    port = uri.port || URI.default_port(uri.scheme)
    path = URI.to_string(%URI{uri | scheme: nil, host: nil, authority: nil})

    with {:ok, conn} <- Mint.HTTP.connect(scheme, uri.host, port, opts) do
      stream(conn, method, path, headers, body)
    end
  end

  defp stream(conn, method, path, headers, body) do
    with {:ok, conn, ref} <- Mint.HTTP.request(conn, method, path, headers, :stream) do
      stream_body(conn, ref, body)
    end
  end

  defp stream_body(conn, ref, "") do
    Mint.HTTP.stream_request_body(conn, ref, :eof)
  end

  defp stream_body(conn, ref, body) do
    chunk_size = min(chunk_size(conn, ref), byte_size(body))
    <<chunk::binary-size(chunk_size), rest::binary>> = body

    with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, ref, chunk) do
      stream_body(conn, ref, rest)
    end
  end

  defp chunk_size(conn, ref) do
    case Mint.HTTP.protocol(conn) do
      :http1 ->
        @default_chunk_size

      :http2 ->
        chunk_size_http2(conn, ref)
    end
  end

  defp chunk_size_http2(conn, ref) do
    min(
      Mint.HTTP2.get_window_size(conn, :connection),
      Mint.HTTP2.get_window_size(conn, {:request, ref})
    )
  end
end

After

defmodule MintHttpStreaming do
  def run(method, %URI{scheme: scheme} = uri, headers, body, opts)
      when scheme in ~w[http https] do
    scheme = String.to_existing_atom(scheme)
    port = uri.port || URI.default_port(uri.scheme)
    path = URI.to_string(%URI{uri | scheme: nil, host: nil, authority: nil})

    with {:ok, conn} <- Mint.HTTP.connect(scheme, uri.host, port, opts) do
      stream(conn, method, path, headers, body)
    end
  end

  defp stream(conn, method, path, headers, body) do
    with {:ok, conn, ref} <- Mint.HTTP.request(conn, method, path, headers, :stream) do
      stream_body(conn, ref, body)
    end
  end

  defp stream_body(conn, ref, "") do
    Mint.HTTP.stream_request_body(conn, ref, :eof)
  end

  defp stream_body(conn, ref, body) do
    {chunk, rest} = Mint.HTTP.next_body_chunk(conn, ref, body)

    with {:ok, conn} <- Mint.HTTP.stream_request_body(conn, ref, chunk) do
      stream_body(conn, ref, rest)
    end
  end
end

Wins:

  • allows to keep stream_request_body/3 protocol-agnostic
  • provides convenient API for the most obvious and straightforward use-case
  • keeps the API a low-level and allow caller to have all the control

@ericmj
Copy link
Copy Markdown
Member

ericmj commented Apr 26, 2026

You are treating H2 send window and sndbuf as two implementations of "how big should the next chunk be," but they're values from different layers that serve different purposes.

The H2 send window is a contract with the peer, if we exceed it we get a protocol error, Mint checks it because violating it is a wire-level bug, not for performance or to avoid blocking.

sndbuf is the size of the kernel send buffer. It's not a contract and no error if you "exceed" it. gen_tcp.send just blocks until the kernel drains enough bytes to accept more.

For H1, sndbuf is the buffer's total capacity, not its current free space (unlike H2 windows). Send one sndbuf-sized chunk and it sits in the kernel buffer until the receiver ACKs, if we send a second one immediately and it blocks waiting for ACKs. Chunking by sndbuf only sizes the first send to fit an empty buffer, every send after that depends on how fast ACKs come back.

For H2, the window can easily exceed sndbuf. When the window is bigger, sending up to it fills the kernel buffer and blocks anyway. The H2 code only guarantees protocol compliance, not non-blocking sends.

For these reasons the proposed implementation is a leaky abstraction over H1 and H2. With the change in #483 users can implement streaming and it's up to the user to pick a chunk size that's appropriate for their application's needs.

@ericmj ericmj closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants