Tesla HTTP Client

repository·master·Indexed 24 days ago

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

An Elixir HTTP client featuring a middleware-based architecture that allows developers to compose custom request pipelines and swap underlying HTTP adapters such as Mint, Finch, Hackney, Gun, and Ibrowse. Tesla provides a common interface for making requests and includes a variety of built-in middleware for handling JSON, authentication, logging, and error handling.

Tokens
17.1K
Snippets
58
Records
88
Agent score
83%

What's inside Tesla

  1. What is a Tesla client?

    master

    In Tesla, a client is the central entity that combines middleware and an adapter.

    • Middleware: Components that modify or enhance requests and responses (e.g., adding headers, handling authentication, or logging).
    • Adapters: Components that handle the actual underlying HTTP communication.

    You create a client using Tesla.client/2 to bundle these behaviors together for making HTTP requests.

    client = Tesla.client([Tesla.Middleware.PathParams, Tesla.Middleware.Logger])
  2. How to implement OpenAPI parameter serialization in Tesla

    master

    Tesla does not parse OpenAPI documents or generate client modules automatically. Instead, it provides the necessary request values and middleware to allow you to build or use generated clients that correctly represent OpenAPI parameter serialization (path, query, header, and cookie parameters).

    To implement this, you must follow a pattern of building specialized modules for each parameter location, an operation module to coordinate them, and a client stack configured with specific middleware.

  3. Map OpenAPI parameter locations to Tesla APIs

    master

    Tesla does not parse OpenAPI documents directly. Instead, it provides specific API modules and middleware that you can use when building or hand-writing clients to implement OpenAPI parameter serialization. The in field in OpenAPI determines which Tesla API to use.

    OpenAPI locationTesla API
    pathTesla.OpenAPI.PathTemplate, Tesla.OpenAPI.PathParam, Tesla.OpenAPI.PathParams, and Tesla.Middleware.PathParams (in :modern mode)
    queryTesla.OpenAPI.QueryParam, Tesla.OpenAPI.QueryParams, and Tesla.Middleware.Query (in :modern mode)
    querystringTesla.OpenAPI.QueryString (passed directly as the request :query)
    headerTesla.OpenAPI.HeaderParam and Tesla.OpenAPI.HeaderParams.to_headers/2
    cookieTesla.OpenAPI.CookieParam and Tesla.OpenAPI.CookieParams.to_headers/2

    Note: path and query parameters use middleware for transformation, while header and cookie collections produce raw header tuples before entering the middleware stack.

  4. How Tesla middleware works

    master

    Middleware in Tesla extends the request/response pipeline. Requests pass through a stack of middleware before reaching the adapter, allowing you to modify both requests and responses.

    There is no distinction between 'request' and 'response' middleware; instead, the distinction depends on when you call Tesla.run(env, next).

    • Actions before Tesla.run/2: These occur during the request phase (before the adapter is called).
    • Actions after Tesla.run/2: These occur during the response phase (after the adapter returns).

    The middleware stack is composed such that the structure follows: adapter(middleware3(middleware2(middleware1(env, next, options)))).

  5. Implement Query parameters with Query middleware

    master

    For in: "query" parameters, create a module that stores Tesla.OpenAPI.QueryParams metadata. Use Tesla.OpenAPI.QueryParam to define styles. Supported styles include:

    • :form
    • :space_delimited
    • :pipe_delimited
    • :deep_object

    Crucial: Your Tesla client must include Tesla.Middleware.Query configured with mode: :modern to process the metadata passed via Tesla.Env.private/0.

    defmodule MyApi.Operation.GetItem.Query do
      alias Tesla.OpenAPI.{QueryParam, QueryParams}
    
      defstruct color: nil, filter: nil, "$additional": %{}
    
      @query_params QueryParams.new!([
                      QueryParam.new!("color", style: :pipe_delimited),
                      QueryParam.new!("filter", style: :deep_object)
                    ])
    
      def query_params, do: @query_params
    
      def to_query(nil), do: %{}
    
      def to_query(%__MODULE__{} = query) do
        additional = query."$additional" || %{}
    
        Map.merge(additional, %{
          "color" => query.color,
          "filter" => query.filter
        })
      end
    end
  6. Migrate from v1 Macro syntax to Client-based usage

    master

    To migrate away from the legacy v1 macro syntax (which uses use Tesla), you must transition to a functional approach where you explicitly manage a Tesla.Client struct. This involves removing the use Tesla macro and replacing plug, adapter, and HTTP method macros with explicit functions and passing a client instance to Tesla functions.

    Migration Steps:

    1. Remove use Tesla: Delete the use Tesla line from your modules.
    2. Convert plug to middleware/0: Instead of using the plug macro, create a function (e.g., middleware/0) that returns a list of middleware modules.
    3. Convert adapter to adapter/0: Instead of using the adapter macro, create a function (e.g., adapter/0) that returns the desired adapter module or configuration.
    4. Implement a client/0 function: Create a function that returns a Tesla.Client struct by calling Tesla.client(middleware(), adapter()).
    5. Update HTTP calls: Replace direct macro calls like get/2 or post/2 with Tesla.get!(client, path), Tesla.post!(client, path, body), etc., passing your new client as the first argument.
    # The new pattern for a Tesla client module
    defmodule MyApp.MyTeslaClient do
      def client do
        Tesla.client(middleware(), adapter())
      end
    
      defp middleware do
        [Tesla.Middleware.KeepRequest, Tesla.Middleware.PathParams, Tesla.Middleware.JSON]
      end
    
      defp adapter do
        # Returning nil uses the default global Tesla adapter
        :my_app
        |> Application.get_env(__MODULE__, [])
        |> Keyword.get(:adapter)
      end
    
      def do_something do
        # Use the client explicitly in API calls
        Tesla.get!(client(), "/endpoint")
      end
    end
  7. Install Tesla and required dependencies

    master

    Add :tesla to your mix.exs dependencies. While :tesla can run with the default Erlang :httpc adapter, it is highly recommended to use a more robust adapter like Mint for production. If you plan to use JSON middleware, you must also include :jason.

    defp deps do
      [
         # or latest version
        {:tesla, "~> 1.11"},
        # optional, required by JSON middleware
        {:jason, "~> 1.4"},
        # optional, required by Mint adapter, recommended
        {:mint, "~> 1.0"}
      ]
    end
  8. Implement Header parameters

    master

    For in: "header" parameters, use Tesla.OpenAPI.HeaderParam for metadata and convert your request struct into a map compatible with Tesla.OpenAPI.HeaderParams. The supported style is :simple.

    defmodule MyApi.Operation.GetItem.Header do
      alias Tesla.OpenAPI.{HeaderParam, HeaderParams}
    
      defstruct [:request_id]
    
      @header_params HeaderParams.new!([
                       HeaderParam.new!("X-Request-ID")
                     ])
    
      def header_params, do: @header_params
    
      def to_header_params(nil), do: %{}
    
      def to_header_params(%__MODULE__{} = headers) do
        %{"X-Request-ID" => headers.request_id}
      end
    end
  9. Handle OpenAPI 'querystring' parameters

    master

    In OpenAPI, in: "querystring" treats the entire query string as a single value. This is distinct from standard query parameters and must not be mixed with them.

    To implement this, use Tesla.OpenAPI.QueryString.

    • Use Tesla.OpenAPI.QueryString.form!(value) to format a value into a query string.
    • Use Tesla.OpenAPI.QueryString.raw!(value) if the OpenAPI media type has already produced the exact query string.

    Warning: Do not send Tesla.OpenAPI.QueryString through Tesla.Middleware.Query as a normal query map in :modern mode; that middleware expects values backed by Tesla.OpenAPI.QueryParams.

    defmodule MyApi.Operation.Search do
      alias MyApi.Client
      alias Tesla.OpenAPI.QueryString
    
      defstruct query_string: nil
    
      def handle_operation(%Client{} = client, %__MODULE__{} = operation, opts) do
        request_opts = [
          method: :get,
          url: "/search",
          query: QueryString.form!(operation.query_string),
          opts: opts
        ]
    
        Tesla.request(client.client, request_opts)
      end
    end
  10. Implement custom middleware using the Tesla.Middleware behaviour

    master

    In Tesla 1.x, you should no longer define local functions within your client module to act as middleware. Instead, extract the logic into a separate module that implements the Tesla.Middleware behaviour.

    defmodule ProperlyNamedMiddleware do
      @behaviour Tesla.Middleware
      def call(env, next, _opts) do
        # implementation
      end
    end
    
    defmodule MyClient do
      plug ProperlyNamedMiddleware
    end
  11. Implement the Multi-Client Pattern

    master

    The Multi-Client pattern involves passing the client instance as a parameter to your service functions. This allows you to create multiple clients with different configurations (e.g., different base URLs, tokens, or adapters) using the same service logic.

    Best for: Multi-tenant applications, interacting with multiple services, or library authors (to ensure flexibility for users).

    defmodule MyApp.ServiceName do
      def operation_name(client, body) do
        url = "/endpoint"
        # The client is passed as a parameter
        Tesla.post!(client, url, body)
      end
    
      def client(opts) do
        middleware = [
          {Tesla.Middleware.BaseUrl, opts[:base_url]},
          {Tesla.Middleware.BearerAuth, token: opts[:bearer_token]}
        ]
        Tesla.client(middleware, opts[:adapter])
      end
    end
    
    # Usage:
    client = MyApp.ServiceName.client(
      base_url: "https://api.service.com",
      bearer_token: "token_value",
      adapter: Tesla.Adapter.Hackney
    )
    {:ok, response} = MyApp.ServiceName.operation_name(client, %{key: "value"})