Packages
tesla
1.18.1
1.20.0
1.18.3
1.18.2
1.18.1
1.18.0
1.17.0
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.3
1.12.2
1.12.1
1.12.0
1.11.2
1.11.1
1.11.0
1.10.3
1.10.2
1.10.1
1.10.0
1.9.0
1.8.1
retired
1.8.0
1.7.0
1.6.1
1.6.0
1.5.1
1.5.0
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.0
1.0.0
1.0.0-beta.1
0.10.0
0.9.0
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.2.2
0.2.1
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
HTTP client library, with support for middleware and multiple adapters.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/tesla.ex
defmodule Tesla do
@moduledoc """
A HTTP toolkit for building API clients using middlewares.
## Building API client
Use `Tesla.client/2` to build a client with the given middleware and adapter.
### Examples
```elixir
defmodule ExampleApi do
def client do
Tesla.client([
{Tesla.Middleware.BaseUrl, "http://api.example.com"},
Tesla.Middleware.JSON
])
end
def fetch_data(client) do
Tesla.get(client, "/data")
end
end
```
Now you can use `ExampleApi.client/0` to make requests to the API.
```elixir
client = ExampleApi.client()
ExampleApi.fetch_data(client)
```
## Direct usage
It is also possible to do request directly with `Tesla` module.
```elixir
Tesla.get("https://example.com")
```
## Default adapter
By default `Tesla` is using `Tesla.Adapter.Httpc`, because `:httpc` is
included in Erlang/OTP and does not require installation of any additional
dependency. It can be changed globally with config:
```elixir
config :tesla, :adapter, Tesla.Adapter.Mint
```
## Assigns and Private
`Tesla.Env` has two map fields for storing additional data: `:assigns` and `:private`.
`:assigns` is a place for user data. It can be used to carry application-specific metadata
through the middleware pipeline.
`:private` is a map reserved for libraries and middleware to use. The keys must be atoms.
Prefix the keys with the name of your project to avoid any future conflicts. The `tesla_`
prefix is reserved for Tesla.
"""
use Tesla.Builder
alias Tesla.Env
require Tesla.Adapter.Httpc
@default_adapter Tesla.Adapter.Httpc
defmacro __using__(opts \\ []) do
quote do
use Tesla.Builder, unquote(opts)
end
end
@doc false
def execute(module, client, options) do
{env, stack} = prepare(module, client, options)
run(env, stack)
end
@doc false
def execute!(module, client, options) do
{env, stack} = prepare(module, client, options)
case run(env, stack) do
{:ok, env} -> env
{:error, error} -> raise Tesla.Error, env: env, stack: stack, reason: error
end
end
defp prepare(module, %{pre: pre, post: post} = client, options) do
adapter = effective_adapter(module, client)
env = struct(Env, options ++ [__module__: module, __client__: %{client | adapter: adapter}])
stack = pre ++ module.__middleware__() ++ post ++ [adapter]
{env, stack}
end
@doc false
def effective_adapter(module, client \\ %Tesla.Client{}) do
with nil <- client.adapter,
nil <- adapter_per_module_from_config(module),
nil <- adapter_per_module(module),
nil <- adapter_from_config() do
adapter_default()
end
end
defp adapter_per_module_from_config(module) do
case Application.get_env(:tesla, module, [])[:adapter] do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_per_module(module) do
module.__adapter__()
end
defp adapter_from_config do
case Application.get_env(:tesla, :adapter) do
nil -> nil
{adapter, opts} -> {adapter, :call, [opts]}
adapter -> {adapter, :call, [[]]}
end
end
defp adapter_default do
{@default_adapter, :call, [[]]}
end
def run_default_adapter(env, opts \\ []) do
apply(@default_adapter, :call, [env, opts])
end
@spec run(Env.t(), Env.stack()) :: Env.result()
# NOTE: keep this empty stack case is useful for reusing/testing middlewares
# (just pass [] as next)
def run(env, []), do: {:ok, env}
# last item in stack is adapter - skip passing rest of stack
def run(env, [{:fn, f}]), do: apply(f, [env])
def run(env, [{m, f, a}]), do: apply(m, f, [env | a])
# for all other elements pass (env, next, opts)
def run(env, [{:fn, f} | rest]), do: apply(f, [env, rest])
def run(env, [{m, f, a} | rest]), do: apply(m, f, [env, rest | a])
@doc """
Adds given key/value pair to `:opts` field in `Tesla.Env`.
Useful when there's a need to store additional middleware data in `Tesla.Env`
## Examples
iex> %Tesla.Env{opts: []} |> Tesla.put_opt(:option, "value")
%Tesla.Env{opts: [option: "value"]}
"""
@spec put_opt(Tesla.Env.t(), atom, any) :: Tesla.Env.t()
def put_opt(env, key, value) do
Map.update!(env, :opts, &Keyword.put(&1, key, value))
end
@doc """
Merges the given assigns into `Tesla.Env`.
## Examples
iex> env = %Tesla.Env{}
iex> env = Tesla.put_assigns(env, %{user_id: 123, role: :admin})
iex> env.assigns
%{user_id: 123, role: :admin}
iex> env = %Tesla.Env{assigns: %{user_id: 123}}
iex> env = Tesla.put_assigns(env, %{role: :admin})
iex> env.assigns
%{user_id: 123, role: :admin}
"""
@spec put_assigns(Tesla.Env.t(), Tesla.Env.assigns()) :: Tesla.Env.t()
def put_assigns(%Tesla.Env{} = env, assigns) when is_map(assigns) do
%{env | assigns: Map.merge(env.assigns, assigns)}
end
@doc """
Assigns key/value to `Tesla.Env`.
## Examples
iex> env = %Tesla.Env{}
iex> env = Tesla.put_assign(env, :user_id, 123)
iex> env.assigns
%{user_id: 123}
"""
@spec put_assign(Tesla.Env.t(), atom, any) :: Tesla.Env.t()
def put_assign(%Tesla.Env{} = env, key, value) when is_atom(key) do
%{env | assigns: Map.put(env.assigns, key, value)}
end
@doc """
Assigns key/value to `Tesla.Env` unless the key is already set.
## Examples
iex> env = %Tesla.Env{}
iex> env = Tesla.put_assign_new(env, :user_id, 123)
iex> env = Tesla.put_assign_new(env, :user_id, 456)
iex> env.assigns
%{user_id: 123}
"""
@spec put_assign_new(Tesla.Env.t(), atom, any) :: Tesla.Env.t()
def put_assign_new(%Tesla.Env{} = env, key, value) when is_atom(key) do
%{env | assigns: Map.put_new(env.assigns, key, value)}
end
@doc """
Updates assign `key` in `Tesla.Env` with the given function.
Raises if the `key` is not set.
See also `update_assign/4`.
## Examples
iex> env = %Tesla.Env{assigns: %{counter: 1}}
iex> env = Tesla.update_assign!(env, :counter, &(&1 + 1))
iex> env.assigns
%{counter: 2}
"""
@spec update_assign!(Tesla.Env.t(), atom, (any -> any)) :: Tesla.Env.t()
def update_assign!(%Tesla.Env{} = env, key, fun) when is_atom(key) and is_function(fun, 1) do
%{env | assigns: Map.update!(env.assigns, key, fun)}
end
@doc """
Updates assign `key` in `Tesla.Env` with the given function or `default`.
If `key` is present in assigns then the existing value is passed to `fun` and its
result is used as the updated value of `key`. If `key` is not present in assigns,
`default` is inserted as the value of `key`. The `default` value will not be passed
through the update function.
See also `update_assign!/3`.
## Examples
iex> env = %Tesla.Env{assigns: %{counter: 10}}
iex> env = Tesla.update_assign(env, :counter, 1, &(&1 + 1))
iex> env = Tesla.update_assign(env, :other, 1, &(&1 + 1))
iex> env.assigns
%{counter: 11, other: 1}
"""
@spec update_assign(Tesla.Env.t(), atom, any, (any -> any)) :: Tesla.Env.t()
def update_assign(%Tesla.Env{} = env, key, default, fun)
when is_atom(key) and is_function(fun, 1) do
%{env | assigns: Map.update(env.assigns, key, default, fun)}
end
@doc """
Returns value of header specified by `key` from `:headers` field in `Tesla.Env`.
## Examples
# non existing header
iex> env = %Tesla.Env{headers: [{"server", "Cowboy"}]}
iex> Tesla.get_header(env, "some-key")
nil
# existing header
iex> env = %Tesla.Env{headers: [{"server", "Cowboy"}]}
iex> Tesla.get_header(env, "server")
"Cowboy"
# first of multiple headers with the same name
iex> env = %Tesla.Env{headers: [{"cookie", "chocolate"}, {"cookie", "biscuits"}]}
iex> Tesla.get_header(env, "cookie")
"chocolate"
"""
@spec get_header(Env.t(), binary) :: binary | nil
def get_header(%Env{headers: headers}, key) do
case List.keyfind(headers, key, 0) do
{_, value} -> value
_ -> nil
end
end
@spec get_headers(Env.t(), binary) :: [binary]
def get_headers(%Env{headers: headers}, key) when is_binary(key) do
for {k, v} <- headers, k == key, do: v
end
@spec put_header(Env.t(), binary, binary) :: Env.t()
def put_header(%Env{} = env, key, value) when is_binary(key) and is_binary(value) do
headers = List.keystore(env.headers, key, 0, {key, value})
%{env | headers: headers}
end
@spec put_headers(Env.t(), [{binary, binary}]) :: Env.t()
def put_headers(%Env{} = env, list) when is_list(list) do
%{env | headers: env.headers ++ list}
end
@spec delete_header(Env.t(), binary) :: Env.t()
def delete_header(%Env{} = env, key) when is_binary(key) do
headers = for {k, v} <- env.headers, k != key, do: {k, v}
%{env | headers: headers}
end
@spec put_body(Env.t(), Env.body()) :: Env.t()
def put_body(%Env{} = env, body), do: %{env | body: body}
@doc """
Dynamically build client from list of middlewares and/or adapter.
```
# add dynamic middleware
client = Tesla.client([{Tesla.Middleware.Headers, [{"authorization", token}]}])
Tesla.get(client, "/path")
# configure adapter in runtime
client = Tesla.client([], Tesla.Adapter.Hackney)
client = Tesla.client([], {Tesla.Adapter.Hackney, pool: :my_pool})
Tesla.get(client, "/path")
# complete module example
defmodule MyApi do
@middleware [
{Tesla.Middleware.BaseUrl, "https://example.com"},
Tesla.Middleware.JSON,
Tesla.Middleware.Logger
]
@adapter Tesla.Adapter.Hackney
def new(opts) do
# do any middleware manipulation you need
middleware = [
{Tesla.Middleware.BasicAuth, username: opts[:username], password: opts[:password]}
] ++ @middleware
# allow configuring adapter in runtime
adapter = opts[:adapter] || @adapter
# use Tesla.client/2 to put it all together
Tesla.client(middleware, adapter)
end
def get_something(client, id) do
# pass client directly to Tesla.get/2
Tesla.get(client, "/something/\#{id}")
# ...
end
end
client = MyApi.new(username: "admin", password: "secret")
MyApi.get_something(client, 42)
```
"""
@doc since: "1.2.0"
@spec client([Tesla.Client.middleware()], Tesla.Client.adapter()) :: Tesla.Client.t()
def client(middleware, adapter \\ nil), do: Tesla.Builder.client(middleware, [], adapter)
@deprecated "Use client/1 or client/2 instead"
def build_client(pre, post \\ []), do: Tesla.Builder.client(pre, post)
@deprecated "Use client/1 or client/2 instead"
def build_adapter(fun), do: Tesla.Builder.client([], [], fun)
@type query_encoding_fun :: (Tesla.Env.query() -> binary)
@type encoding_strategy :: :rfc3986 | :www_form | query_encoding_fun
@doc """
Builds URL with the given URL and query params.
Useful when you need to create a URL with dynamic query params from a Keyword
list
Allows to specify the `encoding` strategy as either `:www_form`,
`:rfc3986`, or a custom function that receives the query params and returns
the encoded query string.
- `url` - the base URL to which the query params will be appended.
- `query` - a list of key-value pairs to be encoded as query params.
- `encoding` - the encoding strategy to use. Defaults to `:www_form`
## Examples
iex> Tesla.build_url("https://api.example.com", [user: 3, page: 2])
"https://api.example.com?user=3&page=2"
URL that already contains query params:
iex> url = "https://api.example.com?user=3"
iex> Tesla.build_url(url, [page: 2, status: true])
"https://api.example.com?user=3&page=2&status=true"
Default encoding `:www_form`:
iex> Tesla.build_url("https://api.example.com", [user_name: "John Smith"])
"https://api.example.com?user_name=John+Smith"
Specified encoding strategy `:rfc3986`:
iex> Tesla.build_url("https://api.example.com", [user_name: "John Smith"], :rfc3986)
"https://api.example.com?user_name=John%20Smith"
Custom encoding function:
Tesla.get(
"http://example.com",
query: [username: "John Smith"],
opts: [query_encoding: &Plug.Conn.Query.encode/1]
)
Tesla.build_url(
"https://api.example.com",
[filters: [pagination: [page: 2]]],
&Plug.Conn.Query.encode/1
)
#=> "https://api.example.com?filters[pagination][page]=2"
This keeps query params structured inside Tesla while letting applications
delegate serialization to encoders such as `Plug.Conn.Query.encode/1`.
When query params are passed as maps, the encoded parameter order is
unspecified. Pass an ordered list of pairs if the exact query string order
matters.
Pass `t:Tesla.OpenAPI.QueryString.t/0` as the query when the entire query string is
already serialized as one value:
iex> query = Tesla.OpenAPI.QueryString.raw!("foo=a+%2B+b&bar=true")
iex> Tesla.build_url("https://api.example.com", query)
"https://api.example.com?foo=a+%2B+b&bar=true"
"""
@spec build_url(Tesla.Env.url(), Tesla.Env.query(), encoding_strategy) :: binary
def build_url(url, query, encoding \\ :www_form)
def build_url(url, %Tesla.OpenAPI.QueryString{} = query_string, _encoding) do
Tesla.OpenAPI.QueryString.append_to_url(query_string, url)
end
def build_url(url, [], _encoding), do: url
def build_url(url, query, _encoding) when map_size(query) == 0, do: url
def build_url(url, query, encoding) do
join = if String.contains?(url, "?"), do: "&", else: "?"
url <> join <> encode_query(query, encoding)
end
@doc """
Builds a URL from the given `t:Tesla.Env.t/0` struct.
Combines the `url` and `query` fields, and allows specifying the `encoding`
strategy before calling `build_url/3`.
"""
@spec build_url(Tesla.Env.t()) :: String.t()
def build_url(%Tesla.Env{} = env) do
query_encoding = Keyword.get(env.opts, :query_encoding, :www_form)
Tesla.build_url(env.url, env.query, query_encoding)
end
@spec encode_query(Tesla.Env.query(), encoding_strategy) :: binary
def encode_query(query, encoding \\ :www_form)
def encode_query(%Tesla.OpenAPI.QueryString{} = query_string, _encoding) do
Tesla.OpenAPI.QueryString.to_query(query_string)
end
def encode_query(query, fun) when is_function(fun, 1), do: fun.(query)
def encode_query(query, encoding) do
query
|> Enum.flat_map(&encode_pair/1)
|> URI.encode_query(encoding)
end
@doc false
def encode_pair({key, value}) when is_map(value) and not is_struct(value),
do: encode_nested_pairs(key, value)
def encode_pair({key, value}) when is_list(value) do
if list_of_tuples?(value) do
encode_nested_pairs(key, value)
else
Enum.map(value, fn e -> {"#{key}[]", e} end)
end
end
@doc false
def encode_pair({key, value}), do: [{key, value}]
defp encode_nested_pairs(parent_key, value) do
Enum.flat_map(value, &encode_nested_pair(parent_key, &1))
end
defp encode_nested_pair(parent_key, {key, value}) do
encode_pair({nested_key(parent_key, key), value})
end
defp nested_key(parent_key, key), do: "#{parent_key}[#{key}]"
defp list_of_tuples?([{k, _} | rest]) when is_atom(k) or is_binary(k), do: list_of_tuples?(rest)
defp list_of_tuples?([]), do: true
defp list_of_tuples?(_other), do: false
end