693 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			693 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
defmodule Finch do
 | 
						|
  @external_resource "README.md"
 | 
						|
  @moduledoc "README.md"
 | 
						|
             |> File.read!()
 | 
						|
             |> String.split("<!-- MDOC !-->")
 | 
						|
             |> Enum.fetch!(1)
 | 
						|
 | 
						|
  alias Finch.{PoolManager, Request, Response}
 | 
						|
  require Finch.Pool
 | 
						|
 | 
						|
  use Supervisor
 | 
						|
 | 
						|
  @default_pool_size 50
 | 
						|
  @default_pool_count 1
 | 
						|
 | 
						|
  @default_connect_timeout 5_000
 | 
						|
 | 
						|
  @pool_config_schema [
 | 
						|
    protocol: [
 | 
						|
      type: {:in, [:http2, :http1]},
 | 
						|
      deprecated: "Use `:protocols` instead."
 | 
						|
    ],
 | 
						|
    protocols: [
 | 
						|
      type: {:list, {:in, [:http1, :http2]}},
 | 
						|
      doc: """
 | 
						|
      The type of connections to support.
 | 
						|
 | 
						|
      If using `:http1` only, an HTTP1 pool without multiplexing is used. \
 | 
						|
      If using `:http2` only, an HTTP2 pool with multiplexing is used. \
 | 
						|
      If both are listed, then both HTTP1/HTTP2 connections are \
 | 
						|
      supported (via ALPN), but there is no multiplexing.
 | 
						|
      """,
 | 
						|
      default: [:http1]
 | 
						|
    ],
 | 
						|
    size: [
 | 
						|
      type: :pos_integer,
 | 
						|
      doc: """
 | 
						|
      Number of connections to maintain in each pool. Used only by HTTP1 pools \
 | 
						|
      since HTTP2 is able to multiplex requests through a single connection. In \
 | 
						|
      other words, for HTTP2, the size is always 1 and the `:count` should be \
 | 
						|
      configured in order to increase capacity.
 | 
						|
      """,
 | 
						|
      default: @default_pool_size
 | 
						|
    ],
 | 
						|
    count: [
 | 
						|
      type: :pos_integer,
 | 
						|
      doc: """
 | 
						|
      Number of pools to start. HTTP1 pools are able to re-use connections in the \
 | 
						|
      same pool and establish new ones only when necessary. However, if there is a \
 | 
						|
      high pool count and few requests are made, these requests will be scattered \
 | 
						|
      across pools, reducing connection reuse. It is recommended to increase the pool \
 | 
						|
      count for HTTP1 only if you are experiencing high checkout times.
 | 
						|
      """,
 | 
						|
      default: @default_pool_count
 | 
						|
    ],
 | 
						|
    max_idle_time: [
 | 
						|
      type: :timeout,
 | 
						|
      doc: """
 | 
						|
      The maximum number of milliseconds an HTTP1 connection is allowed to be idle \
 | 
						|
      before being closed during a checkout attempt.
 | 
						|
      """,
 | 
						|
      deprecated: "Use :conn_max_idle_time instead."
 | 
						|
    ],
 | 
						|
    conn_opts: [
 | 
						|
      type: :keyword_list,
 | 
						|
      doc: """
 | 
						|
      These options are passed to `Mint.HTTP.connect/4` whenever a new connection is established. \
 | 
						|
      `:mode` is not configurable as Finch must control this setting. Typically these options are \
 | 
						|
      used to configure proxying, https settings, or connect timeouts.
 | 
						|
      """,
 | 
						|
      default: []
 | 
						|
    ],
 | 
						|
    pool_max_idle_time: [
 | 
						|
      type: :timeout,
 | 
						|
      doc: """
 | 
						|
      The maximum number of milliseconds that a pool can be idle before being terminated, used only by HTTP1 pools. \
 | 
						|
      This options is forwarded to NimblePool and it starts and idle verification cycle that may impact \
 | 
						|
      performance if misused. For instance setting a very low timeout may lead to pool restarts. \
 | 
						|
      For more information see NimblePool's `handle_ping/2` documentation.
 | 
						|
      """,
 | 
						|
      default: :infinity
 | 
						|
    ],
 | 
						|
    conn_max_idle_time: [
 | 
						|
      type: :timeout,
 | 
						|
      doc: """
 | 
						|
      The maximum number of milliseconds an HTTP1 connection is allowed to be idle \
 | 
						|
      before being closed during a checkout attempt.
 | 
						|
      """,
 | 
						|
      default: :infinity
 | 
						|
    ],
 | 
						|
    start_pool_metrics?: [
 | 
						|
      type: :boolean,
 | 
						|
      doc: "When true, pool metrics will be collected and available through `get_pool_status/2`",
 | 
						|
      default: false
 | 
						|
    ]
 | 
						|
  ]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  The `:name` provided to Finch in `start_link/1`.
 | 
						|
  """
 | 
						|
  @type name() :: atom()
 | 
						|
 | 
						|
  @type scheme() :: :http | :https
 | 
						|
 | 
						|
  @type scheme_host_port() :: {scheme(), host :: String.t(), port :: :inet.port_number()}
 | 
						|
 | 
						|
  @type request_opt() ::
 | 
						|
          {:pool_timeout, timeout()}
 | 
						|
          | {:receive_timeout, timeout()}
 | 
						|
          | {:request_timeout, timeout()}
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  Options used by request functions.
 | 
						|
  """
 | 
						|
  @type request_opts() :: [request_opt()]
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  The reference used to identify a request sent using `async_request/3`.
 | 
						|
  """
 | 
						|
  @opaque request_ref() :: Finch.Pool.request_ref()
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  The stream function given to `stream/5`.
 | 
						|
  """
 | 
						|
  @type stream(acc) ::
 | 
						|
          ({:status, integer}
 | 
						|
           | {:headers, Mint.Types.headers()}
 | 
						|
           | {:data, binary}
 | 
						|
           | {:trailers, Mint.Types.headers()},
 | 
						|
           acc ->
 | 
						|
             acc)
 | 
						|
 | 
						|
  @typedoc """
 | 
						|
  The stream function given to `stream_while/5`.
 | 
						|
  """
 | 
						|
  @type stream_while(acc) ::
 | 
						|
          ({:status, integer}
 | 
						|
           | {:headers, Mint.Types.headers()}
 | 
						|
           | {:data, binary}
 | 
						|
           | {:trailers, Mint.Types.headers()},
 | 
						|
           acc ->
 | 
						|
             {:cont, acc} | {:halt, acc})
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Start an instance of Finch.
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
    * `:name` - The name of your Finch instance. This field is required.
 | 
						|
 | 
						|
    * `:pools` - A map specifying the configuration for your pools. The keys should be URLs
 | 
						|
    provided as binaries, a tuple `{scheme, {:local, unix_socket}}` where `unix_socket` is the path for
 | 
						|
    the socket, or the atom `:default` to provide a catch-all configuration to be used for any
 | 
						|
    unspecified URLs - meaning that new pools for unspecified URLs will be started using the `:default`
 | 
						|
    configuration. See "Pool Configuration Options" below for details on the possible map
 | 
						|
    values. Default value is `%{default: [size: #{@default_pool_size}, count: #{@default_pool_count}]}`.
 | 
						|
 | 
						|
  ### Pool Configuration Options
 | 
						|
 | 
						|
  #{NimbleOptions.docs(@pool_config_schema)}
 | 
						|
  """
 | 
						|
  def start_link(opts) do
 | 
						|
    name = finch_name!(opts)
 | 
						|
    pools = Keyword.get(opts, :pools, []) |> pool_options!()
 | 
						|
    {default_pool_config, pools} = Map.pop(pools, :default)
 | 
						|
 | 
						|
    config = %{
 | 
						|
      registry_name: name,
 | 
						|
      manager_name: manager_name(name),
 | 
						|
      supervisor_name: pool_supervisor_name(name),
 | 
						|
      default_pool_config: default_pool_config,
 | 
						|
      pools: pools
 | 
						|
    }
 | 
						|
 | 
						|
    Supervisor.start_link(__MODULE__, config, name: supervisor_name(name))
 | 
						|
  end
 | 
						|
 | 
						|
  def child_spec(opts) do
 | 
						|
    %{
 | 
						|
      id: finch_name!(opts),
 | 
						|
      start: {__MODULE__, :start_link, [opts]}
 | 
						|
    }
 | 
						|
  end
 | 
						|
 | 
						|
  @impl true
 | 
						|
  def init(config) do
 | 
						|
    children = [
 | 
						|
      {Registry, [keys: :duplicate, name: config.registry_name, meta: [config: config]]},
 | 
						|
      {DynamicSupervisor, name: config.supervisor_name, strategy: :one_for_one},
 | 
						|
      {PoolManager, config}
 | 
						|
    ]
 | 
						|
 | 
						|
    Supervisor.init(children, strategy: :one_for_all)
 | 
						|
  end
 | 
						|
 | 
						|
  defp finch_name!(opts) do
 | 
						|
    Keyword.get(opts, :name) || raise(ArgumentError, "must supply a name")
 | 
						|
  end
 | 
						|
 | 
						|
  defp pool_options!(pools) do
 | 
						|
    {:ok, default} = NimbleOptions.validate([], @pool_config_schema)
 | 
						|
 | 
						|
    Enum.reduce(pools, %{default: valid_opts_to_map(default)}, fn {destination, opts}, acc ->
 | 
						|
      with {:ok, valid_destination} <- cast_destination(destination),
 | 
						|
           {:ok, valid_pool_opts} <- cast_pool_opts(opts) do
 | 
						|
        Map.put(acc, valid_destination, valid_pool_opts)
 | 
						|
      else
 | 
						|
        {:error, reason} ->
 | 
						|
          raise reason
 | 
						|
      end
 | 
						|
    end)
 | 
						|
  end
 | 
						|
 | 
						|
  defp cast_destination(destination) do
 | 
						|
    case destination do
 | 
						|
      :default ->
 | 
						|
        {:ok, destination}
 | 
						|
 | 
						|
      {scheme, {:local, path}} when is_atom(scheme) and is_binary(path) ->
 | 
						|
        {:ok, {scheme, {:local, path}, 0}}
 | 
						|
 | 
						|
      url when is_binary(url) ->
 | 
						|
        cast_binary_destination(url)
 | 
						|
 | 
						|
      _ ->
 | 
						|
        {:error, %ArgumentError{message: "invalid destination: #{inspect(destination)}"}}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp cast_binary_destination(url) when is_binary(url) do
 | 
						|
    {scheme, host, port, _path, _query} = Finch.Request.parse_url(url)
 | 
						|
    {:ok, {scheme, host, port}}
 | 
						|
  end
 | 
						|
 | 
						|
  defp cast_pool_opts(opts) do
 | 
						|
    with {:ok, valid} <- NimbleOptions.validate(opts, @pool_config_schema) do
 | 
						|
      {:ok, valid_opts_to_map(valid)}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp valid_opts_to_map(valid) do
 | 
						|
    # We need to enable keepalive and set the nodelay flag to true by default.
 | 
						|
    transport_opts =
 | 
						|
      valid
 | 
						|
      |> get_in([:conn_opts, :transport_opts])
 | 
						|
      |> List.wrap()
 | 
						|
      |> Keyword.put_new(:timeout, @default_connect_timeout)
 | 
						|
      |> Keyword.put_new(:nodelay, true)
 | 
						|
      |> Keyword.put(:keepalive, true)
 | 
						|
 | 
						|
    conn_opts = valid[:conn_opts] |> List.wrap()
 | 
						|
 | 
						|
    ssl_key_log_file =
 | 
						|
      Keyword.get(conn_opts, :ssl_key_log_file) || System.get_env("SSLKEYLOGFILE")
 | 
						|
 | 
						|
    ssl_key_log_file_device = ssl_key_log_file && File.open!(ssl_key_log_file, [:append])
 | 
						|
 | 
						|
    conn_opts =
 | 
						|
      conn_opts
 | 
						|
      |> Keyword.put(:ssl_key_log_file_device, ssl_key_log_file_device)
 | 
						|
      |> Keyword.put(:transport_opts, transport_opts)
 | 
						|
      |> Keyword.put(:protocols, valid[:protocols])
 | 
						|
 | 
						|
    # TODO: Remove :protocol on v0.18
 | 
						|
    mod =
 | 
						|
      case valid[:protocol] do
 | 
						|
        :http1 ->
 | 
						|
          Finch.HTTP1.Pool
 | 
						|
 | 
						|
        :http2 ->
 | 
						|
          Finch.HTTP2.Pool
 | 
						|
 | 
						|
        nil ->
 | 
						|
          if :http1 in valid[:protocols] do
 | 
						|
            Finch.HTTP1.Pool
 | 
						|
          else
 | 
						|
            Finch.HTTP2.Pool
 | 
						|
          end
 | 
						|
      end
 | 
						|
 | 
						|
    %{
 | 
						|
      mod: mod,
 | 
						|
      size: valid[:size],
 | 
						|
      count: valid[:count],
 | 
						|
      conn_opts: conn_opts,
 | 
						|
      conn_max_idle_time: to_native(valid[:max_idle_time] || valid[:conn_max_idle_time]),
 | 
						|
      pool_max_idle_time: valid[:pool_max_idle_time],
 | 
						|
      start_pool_metrics?: valid[:start_pool_metrics?]
 | 
						|
    }
 | 
						|
  end
 | 
						|
 | 
						|
  defp to_native(:infinity), do: :infinity
 | 
						|
  defp to_native(time), do: System.convert_time_unit(time, :millisecond, :native)
 | 
						|
 | 
						|
  defp supervisor_name(name), do: :"#{name}.Supervisor"
 | 
						|
  defp manager_name(name), do: :"#{name}.PoolManager"
 | 
						|
  defp pool_supervisor_name(name), do: :"#{name}.PoolSupervisor"
 | 
						|
 | 
						|
  defmacrop request_span(request, name, do: block) do
 | 
						|
    quote do
 | 
						|
      start_meta = %{request: unquote(request), name: unquote(name)}
 | 
						|
 | 
						|
      Finch.Telemetry.span(:request, start_meta, fn ->
 | 
						|
        result = unquote(block)
 | 
						|
        end_meta = Map.put(start_meta, :result, result)
 | 
						|
        {result, end_meta}
 | 
						|
      end)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Builds an HTTP request to be sent with `request/3` or `stream/4`.
 | 
						|
 | 
						|
  It is possible to send the request body in a streaming fashion. In order to do so, the
 | 
						|
  `body` parameter needs to take form of a tuple `{:stream, body_stream}`, where `body_stream`
 | 
						|
  is a `Stream`.
 | 
						|
  """
 | 
						|
  @spec build(Request.method(), Request.url(), Request.headers(), Request.body(), Keyword.t()) ::
 | 
						|
          Request.t()
 | 
						|
  defdelegate build(method, url, headers \\ [], body \\ nil, opts \\ []), to: Request
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Streams an HTTP request and returns the accumulator.
 | 
						|
 | 
						|
  A function of arity 2 is expected as argument. The first argument
 | 
						|
  is a tuple, as listed below, and the second argument is the
 | 
						|
  accumulator. The function must return a potentially updated
 | 
						|
  accumulator.
 | 
						|
 | 
						|
  See also `stream_while/5`.
 | 
						|
 | 
						|
  > ### HTTP2 streaming and back-pressure {: .warning}
 | 
						|
  >
 | 
						|
  > At the moment, streaming over HTTP2 connections do not provide
 | 
						|
  > any back-pressure mechanism: this means the response will be
 | 
						|
  > sent to the client as quickly as possible. Therefore, you must
 | 
						|
  > not use streaming over HTTP2 for non-terminating responses or
 | 
						|
  > when streaming large responses which you do not intend to keep
 | 
						|
  > in memory.
 | 
						|
 | 
						|
  ## Stream commands
 | 
						|
 | 
						|
    * `{:status, status}` - the http response status
 | 
						|
    * `{:headers, headers}` - the http response headers
 | 
						|
    * `{:data, data}` - a streaming section of the http response body
 | 
						|
    * `{:trailers, trailers}` - the http response trailers
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
  Shares options with `request/3`.
 | 
						|
 | 
						|
  ## Examples
 | 
						|
 | 
						|
      path = "/tmp/archive.zip"
 | 
						|
      file = File.open!(path, [:write, :exclusive])
 | 
						|
      url = "https://example.com/archive.zip"
 | 
						|
      request = Finch.build(:get, url)
 | 
						|
 | 
						|
      Finch.stream(request, MyFinch, nil, fn
 | 
						|
        {:status, status}, _acc ->
 | 
						|
          IO.inspect(status)
 | 
						|
 | 
						|
        {:headers, headers}, _acc ->
 | 
						|
          IO.inspect(headers)
 | 
						|
 | 
						|
        {:data, data}, _acc ->
 | 
						|
          IO.binwrite(file, data)
 | 
						|
      end)
 | 
						|
 | 
						|
      File.close(file)
 | 
						|
  """
 | 
						|
  @spec stream(Request.t(), name(), acc, stream(acc), request_opts()) ::
 | 
						|
          {:ok, acc} | {:error, Exception.t(), acc}
 | 
						|
        when acc: term()
 | 
						|
  def stream(%Request{} = req, name, acc, fun, opts \\ []) when is_function(fun, 2) do
 | 
						|
    fun = fn entry, acc ->
 | 
						|
      {:cont, fun.(entry, acc)}
 | 
						|
    end
 | 
						|
 | 
						|
    stream_while(req, name, acc, fun, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Streams an HTTP request until it finishes or `fun` returns `{:halt, acc}`.
 | 
						|
 | 
						|
  A function of arity 2 is expected as argument. The first argument
 | 
						|
  is a tuple, as listed below, and the second argument is the
 | 
						|
  accumulator.
 | 
						|
 | 
						|
  The function must return:
 | 
						|
 | 
						|
    * `{:cont, acc}` to continue streaming
 | 
						|
    * `{:halt, acc}` to halt streaming
 | 
						|
 | 
						|
  See also `stream/5`.
 | 
						|
 | 
						|
  > ### HTTP2 streaming and back-pressure {: .warning}
 | 
						|
  >
 | 
						|
  > At the moment, streaming over HTTP2 connections do not provide
 | 
						|
  > any back-pressure mechanism: this means the response will be
 | 
						|
  > sent to the client as quickly as possible. Therefore, you must
 | 
						|
  > not use streaming over HTTP2 for non-terminating responses or
 | 
						|
  > when streaming large responses which you do not intend to keep
 | 
						|
  > in memory.
 | 
						|
 | 
						|
  ## Stream commands
 | 
						|
 | 
						|
    * `{:status, status}` - the http response status
 | 
						|
    * `{:headers, headers}` - the http response headers
 | 
						|
    * `{:data, data}` - a streaming section of the http response body
 | 
						|
    * `{:trailers, trailers}` - the http response trailers
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
  Shares options with `request/3`.
 | 
						|
 | 
						|
  ## Examples
 | 
						|
 | 
						|
      path = "/tmp/archive.zip"
 | 
						|
      file = File.open!(path, [:write, :exclusive])
 | 
						|
      url = "https://example.com/archive.zip"
 | 
						|
      request = Finch.build(:get, url)
 | 
						|
 | 
						|
      Finch.stream_while(request, MyFinch, nil, fn
 | 
						|
        {:status, status}, acc ->
 | 
						|
          IO.inspect(status)
 | 
						|
          {:cont, acc}
 | 
						|
 | 
						|
        {:headers, headers}, acc ->
 | 
						|
          IO.inspect(headers)
 | 
						|
          {:cont, acc}
 | 
						|
 | 
						|
        {:data, data}, acc ->
 | 
						|
          IO.binwrite(file, data)
 | 
						|
          {:cont, acc}
 | 
						|
      end)
 | 
						|
 | 
						|
      File.close(file)
 | 
						|
  """
 | 
						|
  @spec stream_while(Request.t(), name(), acc, stream_while(acc), request_opts()) ::
 | 
						|
          {:ok, acc} | {:error, Exception.t(), acc}
 | 
						|
        when acc: term()
 | 
						|
  def stream_while(%Request{} = req, name, acc, fun, opts \\ []) when is_function(fun, 2) do
 | 
						|
    request_span req, name do
 | 
						|
      __stream__(req, name, acc, fun, opts)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  defp __stream__(%Request{} = req, name, acc, fun, opts) do
 | 
						|
    {pool, pool_mod} = get_pool(req, name)
 | 
						|
    pool_mod.request(pool, req, acc, fun, name, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Sends an HTTP request and returns a `Finch.Response` struct.
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
    * `:pool_timeout` - This timeout is applied when we check out a connection from the pool.
 | 
						|
      Default value is `5_000`.
 | 
						|
 | 
						|
    * `:receive_timeout` - The maximum time to wait for each chunk to be received before returning an error.
 | 
						|
      Default value is `15_000`.
 | 
						|
 | 
						|
    * `:request_timeout` - The amount of time to wait for a complete response before returning an error.
 | 
						|
      This timeout only applies to HTTP/1, and its current implementation is a best effort timeout,
 | 
						|
      it does not guarantee the call will return precisely when the time has elapsed.
 | 
						|
      Default value is `:infinity`.
 | 
						|
 | 
						|
  """
 | 
						|
  @spec request(Request.t(), name(), request_opts()) ::
 | 
						|
          {:ok, Response.t()}
 | 
						|
          | {:error, Exception.t()}
 | 
						|
  def request(req, name, opts \\ [])
 | 
						|
 | 
						|
  def request(%Request{} = req, name, opts) do
 | 
						|
    request_span req, name do
 | 
						|
      acc = {nil, [], [], []}
 | 
						|
 | 
						|
      fun = fn
 | 
						|
        {:status, value}, {_, headers, body, trailers} ->
 | 
						|
          {:cont, {value, headers, body, trailers}}
 | 
						|
 | 
						|
        {:headers, value}, {status, headers, body, trailers} ->
 | 
						|
          {:cont, {status, headers ++ value, body, trailers}}
 | 
						|
 | 
						|
        {:data, value}, {status, headers, body, trailers} ->
 | 
						|
          {:cont, {status, headers, [body | value], trailers}}
 | 
						|
 | 
						|
        {:trailers, value}, {status, headers, body, trailers} ->
 | 
						|
          {:cont, {status, headers, body, trailers ++ value}}
 | 
						|
      end
 | 
						|
 | 
						|
      case __stream__(req, name, acc, fun, opts) do
 | 
						|
        {:ok, {status, headers, body, trailers}} ->
 | 
						|
          {:ok,
 | 
						|
           %Response{
 | 
						|
             status: status,
 | 
						|
             headers: headers,
 | 
						|
             body: IO.iodata_to_binary(body),
 | 
						|
             trailers: trailers
 | 
						|
           }}
 | 
						|
 | 
						|
        {:error, error, _acc} ->
 | 
						|
          {:error, error}
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  # Catch-all for backwards compatibility below
 | 
						|
  def request(name, method, url) do
 | 
						|
    request(name, method, url, [])
 | 
						|
  end
 | 
						|
 | 
						|
  @doc false
 | 
						|
  def request(name, method, url, headers, body \\ nil, opts \\ []) do
 | 
						|
    IO.warn("Finch.request/6 is deprecated, use Finch.build/5 + Finch.request/3 instead")
 | 
						|
 | 
						|
    build(method, url, headers, body)
 | 
						|
    |> request(name, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Sends an HTTP request and returns a `Finch.Response` struct
 | 
						|
  or raises an exception in case of failure.
 | 
						|
 | 
						|
  See `request/3` for more detailed information.
 | 
						|
  """
 | 
						|
  @spec request!(Request.t(), name(), request_opts()) ::
 | 
						|
          Response.t()
 | 
						|
  def request!(%Request{} = req, name, opts \\ []) do
 | 
						|
    case request(req, name, opts) do
 | 
						|
      {:ok, resp} -> resp
 | 
						|
      {:error, exception} -> raise exception
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Sends an HTTP request asynchronously, returning a request reference.
 | 
						|
 | 
						|
  If the request is sent using HTTP1, an extra process is spawned to
 | 
						|
  consume messages from the underlying socket. The messages are sent
 | 
						|
  to the current process as soon as they arrive, as a firehose.  If
 | 
						|
  you wish to maximize request rate or have more control over how
 | 
						|
  messages are streamed, a strategy using `request/3` or `stream/5`
 | 
						|
  should be used instead.
 | 
						|
 | 
						|
  ## Receiving the response
 | 
						|
 | 
						|
  Response information is sent to the calling process as it is received
 | 
						|
  in `{ref, response}` tuples.
 | 
						|
 | 
						|
  If the calling process exits before the request has completed, the
 | 
						|
  request will be canceled.
 | 
						|
 | 
						|
  Responses include:
 | 
						|
 | 
						|
    * `{:status, status}` - HTTP response status
 | 
						|
    * `{:headers, headers}` - HTTP response headers
 | 
						|
    * `{:data, data}` - section of the HTTP response body
 | 
						|
    * `{:error, exception}` - an error occurred during the request
 | 
						|
    * `:done` - request has completed successfully
 | 
						|
 | 
						|
  On a successful request, a single `:status` message will be followed
 | 
						|
  by a single `:headers` message, after which more than one `:data`
 | 
						|
  messages may be sent. If trailing headers are present, a final
 | 
						|
  `:headers` message may be sent. Any `:done` or `:error` message
 | 
						|
  indicates that the request has succeeded or failed and no further
 | 
						|
  messages are expected.
 | 
						|
 | 
						|
  ## Example
 | 
						|
 | 
						|
      iex> req = Finch.build(:get, "https://httpbin.org/stream/5")
 | 
						|
      iex> ref = Finch.async_request(req, MyFinch)
 | 
						|
      iex> flush()
 | 
						|
      {ref, {:status, 200}}
 | 
						|
      {ref, {:headers, [...]}}
 | 
						|
      {ref, {:data, "..."}}
 | 
						|
      {ref, :done}
 | 
						|
 | 
						|
  ## Options
 | 
						|
 | 
						|
  Shares options with `request/3`.
 | 
						|
  """
 | 
						|
  @spec async_request(Request.t(), name(), request_opts()) :: request_ref()
 | 
						|
  def async_request(%Request{} = req, name, opts \\ []) do
 | 
						|
    {pool, pool_mod} = get_pool(req, name)
 | 
						|
    pool_mod.async_request(pool, req, name, opts)
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Cancels a request sent with `async_request/3`.
 | 
						|
  """
 | 
						|
  @spec cancel_async_request(request_ref()) :: :ok
 | 
						|
  def cancel_async_request(request_ref) when Finch.Pool.is_request_ref(request_ref) do
 | 
						|
    {pool_mod, _cancel_ref} = request_ref
 | 
						|
    pool_mod.cancel_async_request(request_ref)
 | 
						|
  end
 | 
						|
 | 
						|
  defp get_pool(%Request{scheme: scheme, unix_socket: unix_socket}, name)
 | 
						|
       when is_binary(unix_socket) do
 | 
						|
    PoolManager.get_pool(name, {scheme, {:local, unix_socket}, 0})
 | 
						|
  end
 | 
						|
 | 
						|
  defp get_pool(%Request{scheme: scheme, host: host, port: port}, name) do
 | 
						|
    PoolManager.get_pool(name, {scheme, host, port})
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Get pool metrics list.
 | 
						|
 | 
						|
  The number of items present on the metrics list depends on the `:count` option
 | 
						|
  each metric will have a `pool_index` going from 1 to `:count`.
 | 
						|
 | 
						|
  The metrics struct depends on the pool scheme defined on the `:protocols` option
 | 
						|
  `Finch.HTTP1.PoolMetrics` for `:http1` and `Finch.HTTP2.PoolMetrics` for `:http2`.
 | 
						|
 | 
						|
  See the `Finch.HTTP1.PoolMetrics` and `Finch.HTTP2.PoolMetrics` for more details.
 | 
						|
 | 
						|
  `{:error, :not_found}` may return on 2 scenarios:
 | 
						|
    - There is no pool registered for the given pair finch instance and url
 | 
						|
    - The pool is configured with `start_pool_metrics?` option false (default)
 | 
						|
 | 
						|
  ## Example
 | 
						|
 | 
						|
      iex> Finch.get_pool_status(MyFinch, "https://httpbin.org")
 | 
						|
      {:ok, [
 | 
						|
        %Finch.HTTP1.PoolMetrics{
 | 
						|
          pool_index: 1,
 | 
						|
          pool_size: 50,
 | 
						|
          available_connections: 43,
 | 
						|
          in_use_connections: 7
 | 
						|
        },
 | 
						|
        %Finch.HTTP1.PoolMetrics{
 | 
						|
          pool_index: 2,
 | 
						|
          pool_size: 50,
 | 
						|
          available_connections: 37,
 | 
						|
          in_use_connections: 13
 | 
						|
        }]
 | 
						|
      }
 | 
						|
  """
 | 
						|
  @spec get_pool_status(name(), url :: String.t() | scheme_host_port()) ::
 | 
						|
          {:ok, list(Finch.HTTP1.PoolMetrics.t())}
 | 
						|
          | {:ok, list(Finch.HTTP2.PoolMetrics.t())}
 | 
						|
          | {:error, :not_found}
 | 
						|
  def get_pool_status(finch_name, url) when is_binary(url) do
 | 
						|
    {s, h, p, _, _} = Request.parse_url(url)
 | 
						|
    get_pool_status(finch_name, {s, h, p})
 | 
						|
  end
 | 
						|
 | 
						|
  def get_pool_status(finch_name, shp) when is_tuple(shp) do
 | 
						|
    case PoolManager.get_pool(finch_name, shp, auto_start?: false) do
 | 
						|
      {_pool, pool_mod} ->
 | 
						|
        pool_mod.get_pool_status(finch_name, shp)
 | 
						|
 | 
						|
      :not_found ->
 | 
						|
        {:error, :not_found}
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  @doc """
 | 
						|
  Stops the pool of processes associated with the given scheme, host, port (aka SHP).
 | 
						|
 | 
						|
  This function can be invoked to manually stop the pool to the given SHP when you know it's not
 | 
						|
  going to be used anymore.
 | 
						|
 | 
						|
  Note that this function is not safe with respect to concurrent requests. Invoking it while
 | 
						|
  another request to the same SHP is taking place might result in the failure of that request. It
 | 
						|
  is the responsibility of the client to ensure that no request to the same SHP is taking place
 | 
						|
  while this function is being invoked.
 | 
						|
  """
 | 
						|
  @spec stop_pool(name(), url :: String.t() | scheme_host_port()) :: :ok | {:error, :not_found}
 | 
						|
  def stop_pool(finch_name, url) when is_binary(url) do
 | 
						|
    {s, h, p, _, _} = Request.parse_url(url)
 | 
						|
    stop_pool(finch_name, {s, h, p})
 | 
						|
  end
 | 
						|
 | 
						|
  def stop_pool(finch_name, shp) when is_tuple(shp) do
 | 
						|
    case PoolManager.all_pool_instances(finch_name, shp) do
 | 
						|
      [] ->
 | 
						|
        {:error, :not_found}
 | 
						|
 | 
						|
      children ->
 | 
						|
        Enum.each(
 | 
						|
          children,
 | 
						|
          fn {pid, _module} ->
 | 
						|
            DynamicSupervisor.terminate_child(pool_supervisor_name(finch_name), pid)
 | 
						|
          end
 | 
						|
        )
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |