Mint Documentation

repository·main·Indexed 23 days ago

https://github.com/elixir-mint/mint

A functional, low-level HTTP client for Elixir supporting HTTP/1 and HTTP/2. Mint uses a process-less, stateless architecture providing an immutable connection structure that wraps TCP or SSL sockets. It includes utilities for protocol negotiation via Mint.Negotiate, header manipulation through Mint.Core.Headers, and low-level frame encoding/decoding for HTTP/2.

Tokens
6.3K
Snippets
8
Records
34
Agent score
80%

What's inside Mint

  1. How Mint's process-less architecture works

    main

    Mint uses a process-less architecture where the HTTP connection is represented as a functional and immutable data structure called a connection.

    Key characteristics:

    • Immutability: Every operation performed on a connection (such as sending a request) returns a new, updated connection struct. You must always use the returned connection for subsequent operations.
    • Socket Management: A connection wraps a socket in active: :once mode. Messages from the socket are delivered to the process that created the connection.
    • Parsing: You must manually pass messages received from the socket to Mint to parse them into HTTP responses.
    • Composability: Because connections are simple data structures and not tied to a specific process, they can be stored inside any Elixir process (e.g., GenServer, GenStage) or even managed by a single process managing multiple connections simultaneously.
  2. Implement connection pooling with Mint

    main

    Mint is a low-level HTTP client and does not provide connection pooling out of the box. This design choice allows developers to implement custom pooling strategies that better fit their specific use cases.

    When building a pool, you can leverage Mint's ability to manage multiple connections within a single process, using the fact that only the connection owning a specific socket will return a response other than :unknown when messages are parsed.

  3. Implement response body decompression in Mint

    main

    Mint is a low-level client and does not provide built-in support for decompression. To handle compressed payloads (e.g., gzip, br, zstd), you must manually inspect the content-encoding or transfer-encoding headers and decompress the :data field of the response map.

    To implement this, you should:

    1. Extract the compression algorithms from the content-encoding header.
    2. Reverse the list of algorithms to ensure they are applied in the correct order (from the last applied to the first).
    3. Use an appropriate decompression library (like Erlang's built-in :zlib for gzip) to transform the data.

    Note: This implementation assumes you are decompressing the entire response body at once rather than streaming chunks.

    defp process_response({:done, request_ref}, state) do
      {%{response: response, from: from}, state} = pop_in(state.requests[request_ref])
    
      # Handle compression here.
      compression_algorithms = get_content_encoding_header(response.headers)
      response = %{response | data: decompress_data(response.data, compression_algorithms)}
    
      GenServer.reply(from, {:ok, response})
    
      state
    end
  4. Make an HTTP request with Mint.HTTP.connect/3 and Mint.HTTP.request/6

    main

    To perform an HTTP request, first establish a connection using Mint.HTTP.connect/3. This function automatically selects between HTTP/1 and HTTP/2. Then, use Mint.HTTP.request/6 to send a request.

    Important: Mint's connection API is stateless. You must always capture and use the updated connection struct returned by every function call.

  5. Wrap a Mint connection in a GenServer

    main

    To manage a connection within a long-running process, you can wrap a Mint connection in a GenServer. This pattern is useful for maintaining a persistent connection to a host or managing multiple connections within a single process.

    Implementation Details:

    1. Initialization: Call Mint.HTTP.connect(scheme, host, port) in the init/1 callback to create the initial connection.
    2. Request Handling: In handle_call/3, use Mint.HTTP.request/5. Since the connection is immutable, you must update the conn in your GenServer state with the returned connection. Store the request_ref and the caller's from pid to facilitate asynchronous replies.
    3. Response Processing: Use handle_info/2 to receive messages from the socket. Pass these messages to Mint.HTTP.stream/2.
    4. Asynchronous Reply: As Mint.HTTP.stream/2 parses messages (status, headers, data, and :done), update your state. When the :done message for a specific request_ref is received, use GenServer.reply(from, {:ok, response}) to unblock the original caller.

    Concurrency Note:

    • HTTP/1: Requests will be pipelined (sent sequentially without waiting for responses).
    • HTTP/2: Requests will be truly concurrent.
    • To avoid pipelining in HTTP/1, you must manually queue or reject requests if one is already in progress.
    defmodule ConnectionProcess do
      use GenServer
    
      require Logger
    
      defstruct [:conn, requests: %{}]
    
      def start_link({scheme, host, port}) do
        GenServer.start_link(__MODULE__, {scheme, host, port})
      end
    
      def request(pid, method, path, headers, body) do
        GenServer.call(pid, {:request, method, path, headers, body})
      end
    
      @impl true
      def init({scheme, host, port}) do
        case Mint.HTTP.connect(scheme, host, port) do
          {:ok, conn} ->
            state = %__MODULE__{conn: conn}
            {:ok, state}
    
          {:error, reason} ->
            {:stop, reason}
        end
      end
    
      @impl true
      def handle_call({:request, method, path, headers, body}, from, state) do
        case Mint.HTTP.request(state.conn, method, path, headers, body) do
          {:ok, conn, request_ref} ->
            state = put_in(state.conn, conn)
            state = put_in(state.requests[request_ref], %{from: from, response: %{}})
            {:noreply, state}
    
          {:error, conn, reason} ->
            state = put_in(state.conn, conn)
            {:reply, {:error, reason}, state}
        end
      end
    
      @impl true
      def handle_info(message, state) do
        case Mint.HTTP.stream(state.conn, message) do
          :unknown ->
            _ = Logger.error(fn -> "Received unknown message: " <> inspect(message) end)
            {:noreply, state}
    
          {:ok, conn, responses} ->
            state = put_in(state.conn, conn)
            state = Enum.reduce(responses, state, &process_response/2)
            {:noreply, state}
        end
      end
    
      defp process_response({:status, request_ref, status}, state) do
        put_in(state.requests[request_ref].response[:status], status)
      end
    
      defp process_response({:headers, request_ref, headers}, state) do
        put_in(state.requests[request_ref].response[:headers], headers)
      end
    
      defp process_response({:data, request_ref, new_data}, state) do
        update_in(state.requests[request_ref].response[:data], fn data -> (data || "") <> new_data end)
      end
    
      defp process_response({:done, request_ref}, state) do
        {%{response: response, from: from}, state} = pop_in(state.requests[request_ref])
        GenServer.reply(from, {:ok, response})
        state
      end
    end
  6. Configure SSL certificates in Mint

    main

    When using :https, Mint uses the system's CA certificate store if you are using Erlang/OTP 25+. If you are on an older version or need to provide a specific CA store, add the :castore package to your dependencies.

    defp deps do
      [
        {:castore, "~> 1.0.0"},
        {:mint, "~> 0.4.0"}
      ]
    end
  7. Handle Mint.TransportError exceptions

    main

    A Mint.TransportError is an exception raised when an error occurs at the transport level (TCP or SSL) during an HTTP connection. Because it is a standard Elixir exception, you can catch it using try/rescue or handle it if it is returned as part of an error tuple.

    iex> {:error, %Mint.TransportError{} = error} = Mint.HTTP.connect(:http, "nonexistent", 80)
          iex> Exception.message(error)
          "non-existing domain"
  8. Extract compression algorithms from headers

    main

    When a server uses multiple compression layers (e.g., content-encoding: br, gzip), the algorithms are listed in the content-encoding header. Use the following logic to extract these into a list. The list is reversed so that you can use Enum.reduce/3 to decompress the data from the outermost layer to the innermost layer.

    This function handles comma-separated values and cases where multiple headers might be present.

    defp get_content_encoding_header(headers) do
      headers
      |> Enum.flat_map(fn {name, value} ->
        if String.downcase(name, :ascii) == "content-encoding" do
          value
          |> String.downcase()
          |> String.split(",", trim: true)
          |> Stream.map(&String.trim/1)
        else
          []
        end
      end)
      |> Enum.reverse()
    end
  9. Decompress data using multiple algorithms

    main

    To decompress a payload that has been compressed multiple times, use Enum.reduce/3 to iterate through the list of algorithms. For each algorithm, apply the corresponding decompression function to the data.

    Example implementation for gzip using Erlang's :zlib and handling the identity algorithm:

    defp decompress_data(data, algorithms) do
      Enum.reduce(algorithms, data, &decompress_with_algorithm/2)
    end
    
    defp decompress_with_algorithm(gzip, data) when gzip in ["gzip", "x-gzip"],
      do: :zlib.gunzip(data)
    
    defp decompress_with_algorithm("identity", data),
      do: data
    
    defp decompress_with_algorithm(algorithm, data),
      do: raise "unsupported decompression algorithm: #{inspect(algorithm)}"