Finch Documentation

repository·main·Indexed 23 days ago

https://github.com/sneako/finch

A high-performance HTTP client for Elixir built on Mint and NimblePool. Finch focuses on efficient connection pooling and minimizing memory copying, providing features such as pool tagging, custom pool selection strategies (RoundRobin, Hash, Random), and detailed pool metrics via ETS. It supports standard HTTP/HTTPS and Unix socket connections.

Tokens
5K
Snippets
14
Records
29
Agent score
80%

What's inside Finch

  1. How pool tagging works

    main

    Pool tagging allows you to isolate traffic or use different configurations for the same {scheme, host, port} or Unix socket. You define tagged pools using Finch.Pool.new/2 in your configuration. When making a request, specify the :pool_tag option. If the specified tag does not exist in the configuration, Finch falls back to the :default configuration.

    # Configuration with tagged pools
    children = [
      {Finch,
       name: MyTaggedFinch,
       pools: %{
         Finch.Pool.new("https://api.example.com") => [size: 50, count: 4],
         Finch.Pool.new("https://api.example.com", tag: :web) => [size: 20, count: 2],
         Finch.Pool.new("http+unix:///tmp/api.sock", tag: :api) => [size: 30, count: 2],
         Finch.Pool.new("http+unix:///tmp/api.sock", tag: :web) => [size: 10, count: 1],
         :default => [size: 10, count: 1]
       }}
      }
    ]
    
    # Making requests with tags
    # Uses the :api tagged pool
    request = Finch.build(:get, "https://api.example.com/users", [], nil, pool_tag: :api)
    Finch.request(request, MyTaggedFinch)
    
    # Uses the :web tagged pool
    request = Finch.build(:get, "https://api.example.com/users", [], nil, pool_tag: :web)
    Finch.request(request, MyTaggedFinch)
    
    # Uses :default tag (or falls back to default config)
    request = Finch.build(:get, "https://api.example.com/users")
    Finch.request(request, MyTaggedFinch)
    
    # Tagged Unix socket pool
    request =
      Finch.build(
        :get,
        "http://localhost/",
        [],
        nil,
        unix_socket: "/tmp/api.sock",
        pool_tag: :api
      )
    Finch.request(request, MyTaggedFinch)
  2. Start a Finch instance

    main

    Finch must be started with a unique :name. It is typically added to your application's supervision tree. In rare cases, it can be started dynamically.

    # In your supervision tree
    children = [
      {Finch, name: MyFinch}
    ]
    
    # Or dynamically
    Finch.start_link(name: MyFinch)
  3. Manage pools manually

    main

    You can manage pools outside of Finch's internal supervisor by using Finch.Pool.child_spec/1 in your own supervision tree. Alternatively, you can add pools dynamically to an existing Finch instance using Finch.start_pool/3.

    # Starting pools in your own supervision tree
    children = [
      {Finch, name: MyFinch},
      {Finch.Pool, finch: MyFinch, pool: Finch.Pool.new("https://api.internal", tag: :api), size: 10},
      {Finch.Pool, finch: MyFinch, pool: Finch.Pool.new("https://node-2.internal", tag: :node2), size: 10}
    ]
    Supervisor.start_link(children, strategy: :one_for_one)
    
    # Adding a pool dynamically
    Finch.start_pool(MyFinch, Finch.Pool.new("https://api.example.com", tag: :api), size: 10)
    
    # Checking if a pool exists
    case Finch.find_pool(MyFinch, Finch.Pool.new("https://api.internal", tag: :api)) do
      {:ok, _pid} -> # Pool exists
      :error -> # Pool not found
    end
  4. Log TLS secrets for HTTPS decryption

    main

    To decrypt HTTPS sessions (e.g., in Wireshark), Finch can log TLS secrets to a file.

    1. Specify the file using the :ssl_key_log_file connection option in your pool configuration.
    2. If using TLSv1.3, you must also set keep_secrets: true in the pool's :transport_opts.
    3. Alternatively, you can set the SSLKEYLOGFILE environment variable.
    # Example configuration for TLS secret logging
    {Finch,
     name: MyFinch,
     pools: %{
       default: [
         conn_opts: [
           transport_opts: [keep_secrets: true],
           ssl_key_log_file: "/writable/path/to/the/sslkey.log"
         ]
       ]
     }}
  5. Configure pre-defined connection pools

    main

    You can configure specific pool sizes and counts for known URLs during startup. For HTTP/1, Finch parses URLs into {scheme, host, port} tuples. Any unconfigured URL will fall back to the :default configuration.

    children = [
      {Finch,
       name: MyConfiguredFinch,
       pools: %{
         :default => [size: 10, count: 2],
         "https://hex.pm" => [size: 32, count: 8]
       }}
    ]
  6. How pool selection strategies work

    main

    When Finch is configured with count: N, multiple pool workers register under the same pool key in a :duplicate Registry. A Finch.Pool.Strategy determines which specific worker is selected for a given request.

    Strategies are passed via the pool_strategy key in the request opts. The strategy and its state are provided as a tuple: {Module, state}. The caller is responsible for managing and providing the state (e.g., an atomics counter or a specific key).

    To optimize performance-critical paths, you can pass the strategy function directly instead of the module to avoid dynamic module dispatch.

  7. Use built-in pool selection strategies

    main

    Finch provides several built-in strategies for selecting a worker from a pool:

    • Finch.Pool.Strategy.RoundRobin: Distributes requests across workers using a counter. Requires state (typically an atomics counter).
    • Finch.Pool.Strategy.Hash: Maps a specific key to the same worker every time. This is useful for maintaining connection affinity. The state should be the key used for hashing (e.g., a team_id).
    • Finch.Pool.Strategy.Random: Selects a worker at random. Requires no state.
    # Round-robin example
    counter = Finch.Pool.Strategy.RoundRobin.new()
    Finch.request(req, MyFinch, pool_strategy: {Finch.Pool.Strategy.RoundRobin, counter})
    
    # Hash-based example (affinity)
    Finch.request(req, MyFinch, pool_strategy: {Finch.Pool.Strategy.Hash, team_id})
  8. Make an HTTP request with Finch

    main

    Once started, use Finch.build/5 to construct a request and Finch.request/2 to execute it using your named Finch instance.

    Finch.build(:get, "https://hex.pm") |> Finch.request(MyFinch)
  9. Handle Finch.Error exceptions

    main

    The Finch.Error exception is used to represent errors returned by Finch that are not transport or HTTP protocol errors. When catching these errors, the reason field contains the cause of the error.

    Common error reasons include:

    • :connection_process_went_down: The connection process went down.
    • :connection_closed: The connection was closed.
    • :disconnected: The connection is disconnected.
    • :request_timeout: The request timed out.
    • :read_only: The connection is closed for writing.
    • :could_not_connect: Could not connect to the destination.
    • :connection_dead: The connection is dead.
  10. Build a request with `Finch.Request.build/5`

    main

    Use Finch.Request.build/5 to construct a %Finch.Request{} struct. This function parses the provided URL and combines it with the HTTP method, headers, and body.

    Supported HTTP methods can be passed as atoms (:get, :post, :put, :patch, :delete, :head, :options) or as arbitrary strings (e.g., "PATCH").

    Arguments:

    • method: An atom (from the supported list) or a String.t().
    • url: A String.t() or URI.t().
    • headers: Request headers (compatible with Mint.Types.headers()).
    • body: The request body. Can be iodata(), nil, or a stream: {:stream, Enumerable.t()} or {:stream, Finch.req_body_fun(term())}.
    • opts: A keyword list containing:
      • :unix_socket: A String.t() specifying the path to a Unix socket.
      • :pool_tag: A Finch.Pool.pool_tag() (defaults to :default).
  11. Use the Hash-based connection pool selection strategy

    main

    The Finch.Pool.Strategy.Hash strategy selects a pool worker by hashing a provided key using :erlang.phash2/2. This ensures that the same key always maps to the same worker, providing connection affinity. This is useful for scenarios like routing specific team IDs, session tokens, or tenant IDs to the same connection to ensure sequential processing of events for that entity.

    To use this strategy, pass a tuple containing the module and the key to the pool_strategy option in Finch.request/3.

    # Example with a specific key (e.g., a tenant_id)
    key = Finch.Pool.Strategy.Hash.new(tenant_id)
    Finch.request(req, MyFinch, pool_strategy: {Finch.Pool.Strategy.Hash, key})
    
    # Example using the current process (self()) as the key
    key = Finch.Pool.Strategy.Hash.new()
    Finch.request(req, MyFinch, pool_strategy: {Finch.Pool.Strategy.Hash, key})