### Production-ready middleware pipeline example Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/2.middleware.md An example of a middleware pipeline combining built-in and custom middleware. It includes KeepRequest, BaseUrl, Headers, BearerAuth, JSON, Logger, Telemetry, and PathParams, with commented-out optional middleware like Compression and OpenTelemetry. ```elixir defmodule MyApp.ServiceName do defp middleware do base_url = "..." token = "..." [ # Preserve the original request, should be the first middleware in the # pipeline to preserve the original request. Tesla.Middleware.KeepRequest, # Set the base URL {Tesla.Middleware.BaseUrl, base_url}, # Add headers {Tesla.Middleware.Headers, [{"user-agent", "MyApp/1.0"}]}, # Add authorization {Tesla.Middleware.BearerAuth, [token: token]}, # Process the body (encoding, compression) # Use string keys for untrusted input Tesla.Middleware.JSON, # Compress the request and response # Tesla.Middleware.Compression, # Optional OpenTelemetry middleware. Be careful adding `Tesla.Middleware.PathParams` # before this middleware since you want to have low cardinality attribute # values, therefore, you want the URL template. # Tesla.Middleware.OpenTelemetry # Keep the telemetry and logging as close as possible to the actual request # being made. # Log requests and responses Tesla.Middleware.Logger, # Telemetry of the Request Tesla.Middleware.Telemetry, # Replaces the Path Params, you may want keep it at the end when telemetry # packages prefer to work with the URL template. Tesla.Middleware.PathParams, ] end end ``` -------------------------------- ### Make a simple GET request with Tesla.get! Source: https://github.com/elixir-tesla/tesla/blob/master/README.md Executes a GET request to https://httpbin.org/get and returns the HTTP status code. This uses the global default adapter without any middleware. ```elixir iex> Tesla.get!("https://httpbin.org/get").status # => 200 ``` -------------------------------- ### Create a Tesla client with middleware Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/0.client.md Creates a Tesla client with a list of middleware modules. The client is then used to make a GET request. ```elixir client = Tesla.client([Tesla.Middleware.PathParams, Tesla.Middleware.Logger]) ``` ```elixir Tesla.get(client, "/users/123") ``` -------------------------------- ### Usage example with Tesla client Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Autopaginate-using-"Link"-header Shows how to configure a Tesla client to use the Autopaginate middleware. The middleware must be plugged after Tesla.Middleware.DecodeRels to ensure the 'rels' key is available. ```elixir defmodule MyApi do use Tesla plug Tesla.Middleware.DecodeRels plug Autopaginate end ``` -------------------------------- ### Set Up Mox Expectations in Test Module Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/test-using-mox.md Example test module that sets up Mox with ExUnit, defines expectations for a Tesla HTTP request, and verifies the request was made. Uses Tesla.Test helpers like expect_tesla_call, assert_received_tesla_call, assert_tesla_env, and assert_tesla_empty_mailbox. ```elixir defmodule MyApp.FeatureTest do use ExUnit.Case, async: true import Mox import Tesla.Test setup :set_mox_from_context setup :verify_on_exit! test "example test" do #--------- Given - Stubs and Preconditions # Expect a single HTTP request to be made and return a JSON response expect_tesla_call( times: 1, returns: json(%Tesla.Env{status: 200}, %{id: 1}) ) #--------- When - Run the code under test # Make the HTTP request using Tesla # Mimic a use case where we create a user assert :ok = create_user!(%{username: "johndoe"}) #--------- Then - Assert postconditions # Verify that the HTTP request was made and matches the expected parameters assert_received_tesla_call(env, []) assert env.status == 200 assert env.method == :post assert env.url == "https://acme.com/users" # ... # Or you can verify the entire `t:Tesla.Env.t/0` using something like this: assert_tesla_env(env, %Tesla.Env{ method: :post, url: "https://acme.com/users", body: %{username: "johndoe"}, status: 200, }) # Verify that the mailbox is empty, indicating no additional requests were # made and all messages have been processed assert_tesla_empty_mailbox() end defp create_user!(body) do # ... Tesla.post!("https://acme.com/users", body) # ... :ok end end ``` -------------------------------- ### Tesla.Middleware.Prometheus module with setup and call functions Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Prometheus-integration Defines the Prometheus middleware for Tesla. Requires the prometheus dependency and calls setup() before application start to declare the histogram metric. The call function measures request duration and observes it with host and status labels. ```elixir defmodule Tesla.Middleware.Prometheus do @moduledoc """ Prometheus metrics Usage: defmodule MyApi do use Tesla plug Tesla.Middleware.Prometheus end # lib/my_app/application.ex defmodule MyApp.Application do def start(_type, _args) do Tesla.Middleware.Prometheus.setup() # ... end end Metrics: - tesla_request_duration_seconds{host,status} """ @buckets [ 0.001, 0.002, 0.004, 0.008, 0.016, 0.032, 0.064, 0.128, 0.256, 0.512, 1.024, 2.048, 4.096 ] def setup do :prometheus_histogram.declare( name: :tesla_request_duration_seconds, help: "Time spent executing the request", labels: [:host, :status], buckets: @buckets ) end def call(env, next, _opts) do start = System.monotonic_time() response = Tesla.run(env, next) stop = System.monotonic_time() labels = [ URI.parse(env.url).host, status(response) ] :prometheus_histogram.observe(:tesla_request_duration_seconds, labels, stop - start) response end defp status({:ok, %{status: status}}), do: status defp status(_error), do: "error" end ``` -------------------------------- ### Configure default adapter in config/config.exs Source: https://github.com/elixir-tesla/tesla/blob/master/README.md Sets the default adapter to Tesla.Adapter.Mint in the configuration file. Requires installing the mint package as well. ```elixir # config/config.exs # Make sure to install `mint` package as well, recommended config :tesla, adapter: Tesla.Adapter.Mint ``` -------------------------------- ### Get header with Tesla.get_header Source: https://github.com/elixir-tesla/tesla/wiki/0.x-to-1.0-Migration-Guide Headers are now a list. Use Tesla.get_header to get a single header value, and Tesla.get_headers to get all values. ```diff - env.headers["cookie"] + Tesla.get_header(env, "cookie") # => "secret" + Tesla.get_headers(env, "cookie") # => ["secret", "token", "and more"] - case env.headers do - %{"server" => server} -> ... - _ -> ... - end + case Tesla.get_header(env, "server") do + nil -> ... + server -> + end ``` -------------------------------- ### Mock Tesla requests and assert request URL Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Testing-sent-requests Use Tesla.Mock.mock in a test setup to capture the request environment and send it to the test process. The mock returns an empty Tesla.Env for GET requests, and the test asserts the request URL using assert_receive. ```ex defmodule ApiTest do use ExUnit.Case setup do Tesla.Mock.mock fn %{method: :get} = env -> send self(), {:this_is_request, env} # send request env to self %Tesla.Env{} end :ok end test "issue some request here" do Api.some_call() assert_receive {:this_is_request, %{url: "/some"}} end end ``` -------------------------------- ### Usage example of the URL builder middleware in a Tesla client Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-URL-templates Shows how to plug the custom middleware into a Tesla client and use it with a URL template like "/user/:id". The get_user function passes parameters via opts: [params: [id: user_id]]. ```elixir defmodule MyAPI do use Tesla plug Tesla.Middleware.BaseUrl, "http://example.com" plug MyAPI.Middleware.UrlBuilder def get_user(user_id) do get("/user/:id", opts: [params: [id: user_id]]) end end ``` -------------------------------- ### Use Tesla client to make a GET request Source: https://github.com/elixir-tesla/tesla/blob/master/README.md Uses the previously created client to make a GET request to /json and returns the response body. The JSON middleware parses the response. ```elixir iex> Tesla.get!(client, "/json").body # => %{"slideshow" => ...} ``` -------------------------------- ### Request logger middleware example Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/2.middleware.md A middleware that logs requests and responses using Elixir's Logger. It logs the request before calling the next middleware, then logs the response or error after. ```elixir defmodule MyApp.Tesla.Middleware.Logger do require Logger @behaviour Tesla.Middleware def call(env, next, _) do Logger.info("Request: #{inspect(env)}") case Tesla.run(env, next) do {:ok, env} -> Logger.info("Response: #{inspect(env)}") {:ok, env} {:error, reason} -> Logger.error("Error: #{inspect(reason)}") {:error, reason} end end end ``` -------------------------------- ### Wrap adapter options in :adapter key Source: https://github.com/elixir-tesla/tesla/wiki/0.x-to-1.0-Migration-Guide Adapter options must be wrapped in the :adapter key. ```diff - MyClient.get("/", opts: [recv_timeout: 30_000]) + MyClient.get("/", opts: [adapter: [recv_timeout: 30_000]]) ``` -------------------------------- ### Singleton client pattern Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/0.client.md Encapsulates client creation in a module to provide a single shared client instance. ```elixir defmodule MyApp.ServiceName do defp client do middleware = [ {Tesla.Middleware.BaseUrl, "https://api.service.com"}, {Tesla.Middleware.BearerAuth, token: bearer_token()}, # Additional middleware... ] Tesla.client(middleware, adapter()) end defp adapter do Keyword.get(config(), :adapter) end defp bearer_token do Keyword.fetch!(config(), :bearer_token) end defp config do Application.get_env(:my_app, __MODULE__, []) end end ``` ```elixir defmodule MyApp.ServiceName do def operation_name(body) do url = "/endpoint" # The client() function is called internally response = Tesla.post!(client(), url, body) # Process the response... end defp client do # Client construction as shown earlier end end ``` ```elixir {:ok, response} = MyApp.ServiceName.operation_name(%{key: "value"}) ``` -------------------------------- ### Multi-client pattern Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/0.client.md Creates clients with different configurations by passing options to the client function. ```elixir defmodule MyApp.ServiceName do def operation_name(client, body) do url = "/endpoint" # The client is passed as a parameter response = Tesla.post!(client, url, body) # Process the response... end def client(opts) do middleware = [ {Tesla.Middleware.BaseUrl, opts[:base_url]}, {Tesla.Middleware.BearerAuth, token: opts[:bearer_token]}, # Additional middleware... ] Tesla.client(middleware, opts[:adapter]) end end ``` ```elixir client = MyApp.ServiceName.client( base_url: "https://api.service.com", bearer_token: "token_value", adapter: Tesla.Adapter.Hackney # Additional options... ) {:ok, response} = MyApp.ServiceName.operation_name(client, %{key: "value"}) ``` -------------------------------- ### Configure Tesla Tape adapter in test config Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Record-and-reply-requests-(tape,-vcr) Sets the Tesla adapter to Tesla.Adapter.Tape for the MyClient client in the test environment, enabling recording and replaying of HTTP responses. ```elixir # config/test.exs config :tesla, MyClient, adapter: Tesla.Adapter.Tape ``` -------------------------------- ### Use full module names for middleware and adapters Source: https://github.com/elixir-tesla/tesla/wiki/0.x-to-1.0-Migration-Guide Aliases for middleware and adapters are dropped. Use full module names like Tesla.Middleware.JSON and Tesla.Adapter.Hackney. ```diff # middleware - plug :json + plug Tesla.Middleware.JSON # adapter - adapter :hackney + adapter Tesla.Adapter.Hackney # config - config :tesla, adapter: :mock + config :tesla, adapter: Tesla.Mock ``` -------------------------------- ### Tesla.Middleware.Cache - Generic caching middleware Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Simple-file-based-cache Defines a Tesla middleware that caches GET responses. It uses a key derived from the method and URL, and delegates to provided get/set functions. For non-GET requests, it simply runs the request. ```elixir defmodule Tesla.Middleware.Cache do # requires tesla 0.x def call(env, run, [get: get, set: set]) do if env.method == :get do key = cache_key(env) case get.(key) do {:ok, value} -> value {:error, _} -> res = run.(env) set.(key, res) res end else run.(env) end end defp cache_key(env) do :crypto.hash(:sha256, [ to_string(env.method), env.url ]) |> Base.encode16 end end ``` -------------------------------- ### Implement Tesla.Adapter.Tape module Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Record-and-reply-requests-(tape,-vcr) Full implementation of the Tesla.Adapter.Tape behaviour. It reads cached responses from disk, and on cache miss calls the underlying adapter (default Tesla.Adapter.Httpc), saves the response to a file, and returns it. The cache key is built from the HTTP method, URL (with query), and MD5 hash of the body. ```elixir defmodule Tesla.Adapter.Tape do @behaviour Tesla.Adapter def call(env, opts) do dir = opts[:dir] || "test/tapes" adapter = opts[:adapter] || Tesla.Adapter.Httpc file = Path.join(dir, key(env)) case read(file) do {:ok, {:ok, status, headers, body}} -> {:ok, %{env | status: status, headers: headers, body: body}} {:ok, {:error, reason}} -> {:error, reason} {:error, :enoent} -> response = adapter.call(env, []) File.mkdir_p(dir) write(file, response) response end end defp key(env) do [ key_method(env.method), key_url(env.url, env.query), key_body(env.body) ] |> Enum.filter(& &1) |> Enum.join("_") end defp key_method(method), do: String.upcase(to_string(method)) defp key_url(url, query), do: String.replace(Tesla.build_url(url, query), ~r/[^a-z0-9]+/i, "_") defp key_body(nil), do: nil defp key_body(""), do: nil defp key_body(body), do: Base.encode16(:crypto.hash(:md5, body)) defp read(file) do case File.read(file) do {:ok, data} -> {:ok, :erlang.binary_to_term(data)} {:error, :enoent} -> {:error, :enoent} end end defp write(file, response) do data = case response do {:ok, %{status: status, headers: headers, body: body}} -> {:ok, status, headers, body} {:error, reason} -> {:error, reason} end File.write(file, :erlang.term_to_binary(data)) end end ``` -------------------------------- ### Use DecodeJsonAs middleware in Tesla client Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Decode-JSON-as-struct Shows how to use the custom `DecodeJsonAs` middleware in a Tesla client. It plugs the middleware after setting a base URL and passes the `decode_as` option to the `get` request to specify the struct to decode into. ```elixir defmodule MyAPI do use Tesla plug Tesla.Middleware.BaseUrl, "http://example.com" plug MyAPI.Middleware.DecodeJsonAs def get_user(user_id) do get("/user/" <> user_id, opts: [decode_as: %User{}]) end end ``` -------------------------------- ### Set header with Tesla.put_header Source: https://github.com/elixir-tesla/tesla/wiki/0.x-to-1.0-Migration-Guide Headers are now a list. Use Tesla.put_header to set a header. ```diff - env - |> Map.update!(&Map.put(&1.headers, "name", "value")) + env + |> Tesla.put_header("name", "value") ``` -------------------------------- ### Define static adapter module and configure Tesla client Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Static-response-adapter Defines a custom Tesla adapter that always returns a 200 OK response, and configures the Tesla client to use this adapter in the test environment. The adapter is set via the `:adapter` option in the client's configuration. ```elixir # lib/myapp/adapters/static.ex defmodule MyApp.Adapters.Static do def call(_env, :ok), do: {:ok, %Tesla.Env{status: 200}} end # config/test.exs config :tesla, MyApp.MyStatsClient, adapter: {MyApp.Adapters.Static, :ok} ``` -------------------------------- ### Create a Tesla client with middleware Source: https://github.com/elixir-tesla/tesla/blob/master/README.md Creates a Tesla client with a base URL and JSON middleware. This client can be reused across requests to compose middleware. ```elixir iex> client = Tesla.client([ ...> {Tesla.Middleware.BaseUrl, "https://httpbin.org/"}, ...> Tesla.Middleware.JSON, ...> ]) ``` -------------------------------- ### OpenAPI operation with path, query, header, and cookie parameters Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md OpenAPI operation definition showing path, query, header, and cookie parameters. The path parameters use matrix style with explode true for the array, the query parameter color uses pipeDelimited style, and filter uses deepObject style. All parameters are required. ```yaml paths: /items/{id}{coords}: get: operationId: getItem parameters: - name: id in: path required: true schema: type: integer - name: coords in: path required: true style: matrix explode: true schema: type: array items: type: string - name: color in: query required: true style: pipeDelimited schema: type: array items: type: string - name: filter in: query required: true style: deepObject schema: type: object - name: X-Request-ID in: header required: true schema: type: string - name: session_id in: cookie required: true schema: type: string responses: "200": description: Item response content: application/json: schema: $ref: "#/components/schemas/GetItemResponse" ``` -------------------------------- ### Build the API module with a generated function delegating to the operation module Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Defines MyApi with a send_get_item/3 function that delegates to GetItem.handle_operation/3. The function takes a client, an operation, and optional options, and returns the operation result. ```elixir defmodule MyApi do alias MyApi.Client alias MyApi.Operation.GetItem @spec send_get_item(Client.t(), GetItem.t(), keyword()) :: GetItem.result() def send_get_item(%Client{} = client, %GetItem{} = operation, opts \\ []) do GetItem.handle_operation(client, operation, opts) end end ``` -------------------------------- ### Send the operation by building operation values and calling the API module Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Constructs a GetItem operation with path, query, headers, and cookies, then sends it through MyApi.send_get_item/3. The operation includes additional query parameters, color and filter values, and a request ID and session ID. ```elixir alias MyApi.Operation.GetItem alias MyApi.Operation.GetItem.{Cookie, Header, Path, Query} operation = GetItem.new(%{ path: %Path{id: 42, coords: ["blue", "black"]}, query: %Query{ :"$additional" => %{"debug" => true}, color: ["blue", "black"], filter: [role: "admin"] }, headers: %Header{request_id: "req-123"}, cookies: %Cookie{session_id: "abc123"} }) MyApi.send_get_item(client, operation, []) ``` -------------------------------- ### Configure Tesla to Use Mock Adapter in config/test.exs Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/test-using-mox.md Configures Tesla to use the mock adapter globally in the test environment. If not using global adapter configuration, ensure client modules are configured to use MyApp.MockAdapter during tests. ```elixir # config/test.exs config :tesla, adapter: MyApp.MockAdapter ``` -------------------------------- ### Build the header module for Tesla.OpenAPI.HeaderParam Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Defines a module for header parameters, exposing Tesla.OpenAPI.HeaderParam metadata and converting the request struct into a value map for Tesla.OpenAPI.HeaderParams. Supports the OpenAPI header style :simple. ```elixir defmodule MyApi.Operation.GetItem.Header do alias Tesla.OpenAPI.{HeaderParam, HeaderParams} @type t :: %__MODULE__{ request_id: String.t() } 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 ``` -------------------------------- ### Build the client stack with Tesla.Middleware.PathParams and Query in :modern mode Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Defines a MyApi.Client struct and a new/1 function that builds a Tesla client with middleware for base URL, path params, query params, and JSON. Uses :modern mode for PathParams and Query to support Tesla.OpenAPI parameters passed through Tesla.Env.private/0. Requires :base_url and :adapter options. ```elixir defmodule MyApi.Client do @type t :: %__MODULE__{ client: Tesla.Client.t() } defstruct [:client] def new(opts) do middleware = [ {Tesla.Middleware.BaseUrl, Keyword.fetch!(opts, :base_url)}, {Tesla.Middleware.PathParams, mode: :modern}, {Tesla.Middleware.Query, mode: :modern}, Tesla.Middleware.JSON ] adapter = Keyword.fetch!(opts, :adapter) %__MODULE__{client: Tesla.client(middleware, adapter)} end end ``` -------------------------------- ### Define Mock Adapter with Mox.defmock Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/test-using-mox.md Defines a mock adapter module that implements the Tesla.Adapter behaviour, allowing interception of HTTP requests during tests. Place this in test/support/mocks.ex. ```elixir # test/support/mocks.ex Mox.defmock(MyApp.MockAdapter, for: Tesla.Adapter) ``` -------------------------------- ### Set adapter when creating a client Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/3.adapter.md Shows how to pass an adapter tuple with options to Tesla.client/2. The Hackney adapter is used with a receive timeout of 30 seconds. ```elixir defmodule MyService do def client(...) do middleware = [...] adapter = {Tesla.Adapter.Hackney, [recv_timeout: 30_000]} Tesla.client(middleware, adapter) end end ``` -------------------------------- ### Define response wrapper module with Tesla.OpenAPI.Response Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Use this module to wrap Tesla.Env with status, headers, and typed body returned by each operation. The module uses Tesla.OpenAPI.Response to provide the response handling. ```elixir defmodule MyApi.Response do use Tesla.OpenAPI.Response end ``` -------------------------------- ### Pass options to middleware Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/0.client.md Registers middleware as a tuple of module and options to pass configuration. ```elixir client = Tesla.client( [{Tesla.Middleware.BaseUrl, "https://api.example.com"}] ) ``` -------------------------------- ### Custom adapter implementation with Tesla.Adapter behaviour Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/3.adapter.md Defines a custom adapter module that implements the Tesla.Adapter behaviour. The call/2 function builds a request using Req, sends it, and returns a Tesla.Env struct with the response status, headers, and body. ```elixir defmodule Tesla.Adapter.Req do @behaviour Tesla.Adapter @impl Tesla.Adapter def call(env, _opts) do req = Req.new( url: Tesla.build_url(env), method: env.method, headers: env.headers, body: env.body ) case Req.request(req) do {:ok, %Req.Response{} = resp} -> {:ok, %Tesla.Env{env | status: resp.status, headers: resp.headers, body: resp.body}} {:error, reason} -> {:error, reason} end end end ``` -------------------------------- ### Build operation module for GetItem with nested parameter modules Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Assembles nested modules (Cookie, Header, Path, Query) into the generated operation. The module builds static request metadata and handles responses for statuses 200, 401, and 404. ```elixir defmodule MyApi.Operation.GetItem do alias MyApi.Client alias MyApi.Operation.GetItem.{Cookie, Header, Path, Query} alias MyApi.Response alias Tesla.OpenAPI alias Tesla.OpenAPI.{CookieParams, HeaderParams, PathParams, PathTemplate, QueryParams} defstruct path: nil, query: nil, headers: nil, cookies: nil @type t :: %__MODULE__{ path: Path.t() | nil, query: Query.t() | nil, headers: Header.t() | nil, cookies: Cookie.t() | nil } @type resp_body_200() :: MyApi.Schemas.GetItemResponse.t() @type resp_header_200() :: Response.headers() @type resp_200() :: Response.t(resp_body_200(), resp_header_200()) @type resp_401() :: Response.t(nil, Response.headers()) @type resp_404() :: Response.t(nil, Response.headers()) @type result() :: {:ok, resp_200() | resp_401() | resp_404()} | {:error, term()} @operation_path Path.path_template().path @header_params Header.header_params() @cookie_params Cookie.cookie_params() @private OpenAPI.merge_private([ PathTemplate.put_private(Path.path_template()), PathParams.put_private(Path.path_params()), QueryParams.put_private(Query.query_params()) ]) def new(attrs) when is_map(attrs) do %__MODULE__{ path: Map.fetch!(attrs, :path), query: Map.get(attrs, :query), headers: Map.get(attrs, :headers), cookies: Map.get(attrs, :cookies) } end @doc false def handle_operation(%Client{} = client, %__MODULE__{} = operation, opts) do headers = HeaderParams.to_headers(@header_params, Header.to_header_params(operation.headers)) ++ CookieParams.to_headers(@cookie_params, Cookie.to_cookie_params(operation.cookies)) request_opts = [ method: :get, url: @operation_path, query: Query.to_query(operation.query), headers: headers, opts: Keyword.put(opts, :path_params, Path.to_path_params(operation.path)), private: @private ] case Tesla.request(client.client, request_opts) do {:ok, %Tesla.Env{status: 200} = env} -> {:ok, Response.new(env, MyApi.Schemas.GetItemResponse.new(env.body))} {:ok, %Tesla.Env{status: status} = env} when status in [401, 404] -> {:ok, Response.new(env, nil)} {:ok, env} -> {:ok, Response.new(env, env.body)} {:error, reason} -> {:error, reason} end end end ``` -------------------------------- ### MyApp.TeslaSQLSandboxMiddleware - Elixir Tesla middleware for SQL.Sandbox Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Concurrent,-transactional-tests-with-Phoenix.Ecto.SQL.Sandbox Use this middleware in your Tesla client to enable asynchronous, transactional tests with Ecto's SQL.Sandbox. It extracts sandbox metadata from the current process and passes it via the user-agent header. The middleware is disabled unless the :sql_sandbox config is set, allowing reuse in production. ```elixir defmodule MyApp.TeslaSQLSandboxMiddleware do @moduledoc false @behaviour Tesla.Middleware if Application.compile_env!(:my_app, :sql_sandbox) do @impl Tesla.Middleware def call(env, next, _opts) do user_agent = Tesla.get_header(env, "user-agent") encoded_metadata = MyApp.Repo |> Phoenix.Ecto.SQL.Sandbox.metadata_for(self()) |> Phoenix.Ecto.SQL.Sandbox.encode_metadata() env |> Tesla.put_header("user-agent", "#{user_agent}/#{encoded_metadata}") |> Tesla.run(next) end else @impl Tesla.Middleware def call(env, next, _opts) do Tesla.run(env, next) end end end ``` -------------------------------- ### Build the path module with Tesla.OpenAPI.PathParams Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Defines a module for path parameters, including a struct for request values and static metadata via PathTemplate and PathParams. The operation module stores the result of Path.path_params() and passes the request map to Tesla.Middleware.PathParams in :modern mode. ```elixir defmodule MyApi.Operation.GetItem.Path do alias Tesla.OpenAPI.{PathParam, PathParams, PathTemplate} @type t :: %__MODULE__{ id: integer(), coords: [String.t()] } defstruct [:id, :coords] @path_template PathTemplate.new!("/items/{id}{coords}") @path_params PathParams.new!([ PathParam.new!("id"), PathParam.new!("coords", style: :matrix, explode: true) ]) def path_template, do: @path_template def path_params, do: @path_params def to_path_params(%__MODULE__{} = path) do %{ "id" => path.id, "coords" => path.coords } end end ``` -------------------------------- ### Pass adapter to client Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/0.client.md Overrides the default adapter by passing an adapter module or a tuple with options. ```elixir client = Tesla.client([], Tesla.Adapter.Mint) ``` ```elixir client = Tesla.client([], {Tesla.Adapter.Mint, pool: :my_pool}) ``` -------------------------------- ### Autopaginate middleware module Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Autopaginate-using-"Link"-header Defines a Tesla middleware that automatically fetches all pages when the response contains a 'next' link. It fetches pages in parallel if the next page is 2 and a last page exists, otherwise it fetches sequentially. Requires the 'rels' key in the environment, which is provided by Tesla.Middleware.DecodeRels. ```elixir defmodule AutoPaginate do # requires tesla 0.x def call(env, run, []) do env = run.(env) if env.status == 200 && Map.has_key?(env, :rels) do next_page = page(env.rels["next"]) last_page = page(env.rels["last"]) cond do next_page == 2 && last_page -> body = fetch_parallel(env, next_page, last_page) %{env | body: (env.body ++ body)} !last_page && next_page -> body = fetch_next(env).body %{env | body: (env.body ++ body)} true -> env end else env end end defp fetch_parallel(env, next_page, last_page) do (next_page..last_page) |> Gitalyzer.Utils.parmap(fn e -> fetch_page(env, e) end) |> Enum.reduce([], fn(e,a) -> a ++ e.body end) end defp page(rel) do if rel do Regex.run(~r/\A.+page=(\d+)\z/, rel, capture: :all_but_first) |> List.first |> String.to_integer else nil end end defp with_page(url, page) do url |> String.replace("2", to_string(page)) end defp fetch_page(env, page) do env._client |> env._module.get(with_page(env.rels["next"], page)) end defp fetch_next(env) do env._client |> env._module.get(env.rels["next"]) end end ``` -------------------------------- ### Tesla.Middleware.Appsignal module definition Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Appsignal-instrumentation Defines a Tesla middleware that instruments each HTTP request with Appsignal. It imports the instrument helper, builds a span name from the HTTP method and URL, and wraps Tesla.run in an instrument block. ```elixir defmodule Tesla.Middleware.Appsignal do import Appsignal.Instrumentation.Helpers, only: [instrument: 3] def call(env, next, _opts) do verb = env.method |> to_string |> String.upcase instrument "net.http", "#{verb} #{env.url}", fn -> Tesla.run(env, next) end end end ``` -------------------------------- ### Handle ok/error tuples from HTTP functions Source: https://github.com/elixir-tesla/tesla/wiki/0.x-to-1.0-Migration-Guide In tesla 1.0, HTTP functions return {:ok, Tesla.Env} or {:error, reason}. Use case to handle different responses. ```elixir case MyApi.get("/") do {:ok, %Tesla.Env{status: 200}} -> # ok response {:ok, %Tesla.Env{status: 500}} -> # server error {:error, reason} -> # connection & other errors end ``` -------------------------------- ### Tesla.Middleware.Cache.File - File-based cache implementation Source: https://github.com/elixir-tesla/tesla/wiki/Cookbook:-Simple-file-based-cache Implements a file-based cache for Tesla using the generic Tesla.Middleware.Cache. It stores responses as Erlang terms in files under a specified directory, with logging for cache hits and misses. ```elixir defmodule Tesla.Middleware.Cache.File do require Logger def call(env, run, [dir: dir]) do Tesla.Middleware.Cache.call(env, run, get: fn (key) -> get(dir, key) end, set: fn (key, val) -> set(dir, key, val) end ) end def get(dir, key) do case File.read(path(dir, key)) do {:ok, data} -> Logger.debug "cache/get #{key} fresh" {:ok, decode(data)} err -> Logger.debug "cache/get #{key} miss" err end end def set(dir, key, value) do Logger.debug "cache/set #{key}" File.mkdir_p(dir) File.write!(path(dir, key), encode(value)) end defp decode(data) do :erlang.binary_to_term(data) end defp encode(env) do :erlang.term_to_binary(env) end defp path(dir, key) do "#{dir}/#{key}" end end ``` -------------------------------- ### Add Tesla dependency in mix.exs Source: https://github.com/elixir-tesla/tesla/blob/master/README.md Adds the Tesla package and optional dependencies for JSON and Mint adapter to your Elixir project's mix.exs file. The JSON middleware requires Jason, and the Mint adapter is recommended for production use. ```elixir defp deps do [ {:tesla, "~> 1.21.3"}, # optional, required by JSON middleware {:jason, "~> 1.4"}, # optional, required by Mint adapter, recommended {:mint, "~> 1.0"} ] end ``` -------------------------------- ### Pass adapter options directly in request Source: https://github.com/elixir-tesla/tesla/blob/master/guides/explanations/3.adapter.md Shows how to pass adapter-specific options directly in a request call using the :adapter key in the opts list. Here, the Hackney adapter receives a receive timeout of 30 seconds. ```elixir Tesla.get(client, "/", opts: [adapter: [recv_timeout: 30_000]]) ``` -------------------------------- ### Build the query module with Tesla.OpenAPI.QueryParams Source: https://github.com/elixir-tesla/tesla/blob/master/guides/howtos/openapi-parameters.md Defines a module for query parameters, including a struct for request values and static metadata via QueryParams. Supports OpenAPI query styles :form, :space_delimited, :pipe_delimited, and :deep_object. The operation module stores the result of Query.query_params() and passes the request map to Tesla.Middleware.Query in :modern mode. ```elixir defmodule MyApi.Operation.GetItem.Query do alias Tesla.OpenAPI.{QueryParam, QueryParams} @type t :: %__MODULE__{ :"$additional" => map() | nil, color: [String.t()], filter: keyword() } 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 ```