Tesla
Tesla is an HTTP client loosely based on Faraday. It embraces the concept of middleware when processing the request/response cycle.
HTTP Client example
Define module with use Tesla
and choose from a variety of middleware.
defmodule GitHub do
use Tesla
plug Tesla.Middleware.BaseUrl, "https://api.github.com"
plug Tesla.Middleware.Headers, %{"Authorization" => "token xyz"}
plug Tesla.Middleware.JSON
def user_repos(login) do
get("/user/" <> login <> "/repos")
end
end
Then use it like this:
response = GitHub.user_repos("teamon")
response.status # => 200
response.body # => [%{…}, …]
response.headers # => %{'content-type' => 'application/json'}
See below for documentation.
Installation
Add tesla
as dependency in mix.exs
defp deps do
[{:tesla, "~> 0.9.0"},
{:poison, ">= 1.0.0"}] # optional, required by JSON middleware
end
Also, unless using Elixir >= 1.4
, add :tesla
to the applications
list:
def application do
[applications: [:tesla, ...], ...]
end
Documentation
- Middleware
- Runtime middleware
- Adapters
- Streaming
- Multipart
- Testing
- Writing middleware
- Direct usage
- Cheatsheet
Middleware
Tesla is built around the concept of composable middlewares. This is very similar to how Plug Router works.
Basic
Tesla.Middleware.BaseUrl
- set base urlTesla.Middleware.Headers
- set request headersTesla.Middleware.Query
- set query parametersTesla.Middleware.Opts
- set request optionsTesla.Middleware.FollowRedirects
- follow 3xx redirectsTesla.Middleware.MethodOverride
- set X-Http-Method-OverrideTesla.Middleware.Logger
- log requests (method, url, status, time)Tesla.Middleware.DebugLogger
- log full requests & responses
Formats
Tesla.Middleware.FormUrlencoded
- urlencode POST body parameter, useful for POSTing a map/keyword listTesla.Middleware.JSON
- JSON request/response bodyTesla.Middleware.Compression
- gzip & deflateTesla.Middleware.DecodeRels
- decodeLink
header intoopts[:rels]
field in response
Auth
Tesla.Middleware.BasicAuth
- HTTP Basic AuthTesla.Middleware.DigestAuth
- Digest access authentication
Error handling
Tesla.Middleware.Timeout
- timeout request after X milliseconds despite of server responseTesla.Middleware.Retry
- retry few times in case of connection refusedTesla.Middleware.Fuse
- fuse circuit breaker integrationTesla.Middleware.Tuples
- return{:ok, env} | {:error, reason}
instead of raising exception
Runtime middleware
All HTTP functions (get
, post
, etc.) can take a dynamic client function as the first parameter.
This allow to use convenient syntax for modifying the behaviour in runtime.
Consider the following case: GitHub API can be accessed using OAuth token authorization.
We can’t use plug Tesla.Middleware.Headers, %{"Authorization" => "token here"}
since this would be compiled only once and there is no way to insert dynamic user token.
Instead, we can use Tesla.build_client
to create a dynamic middleware function:
defmodule GitHub do
# same as above with a slightly change to `user_repos/1`
def user_repos(client, login) do
# pass `client` argument to `get` function
get(client, "/user/" <> login <> "/repos")
end
def issues(client \\ %Tesla.Client{}) do
# default to empty client that will not include runtime token
get(client, "/issues")
end
# build dynamic client based on runtime arguments
def client(token) do
Tesla.build_client [
{Tesla.Middleware.Headers, %{"Authorization" => "token: " <> token }}
]
end
end
and then:
client = GitHub.client(user_token)
client |> GitHub.user_repos("teamon")
client |> GitHub.get("/me")
GitHub.issues()
client |> GitHub.issues()
The Tesla.build_client
function can take two arguments: pre
and post
middleware.
The first list (pre
) will be included before any other middleware. In case there is a need
to inject middleware at the end you can pass a second list (post
). It will be put just
before adapter. In fact, one can even dynamically override the adapter.
For example, a private (per user) cache could be implemented as:
def new(user) do
Tesla.build_client [], [
fn env, next ->
case my_private_cache.fetch(user, env) do
{:ok, env} -> env # return cached response
:error -> Tesla.run(env, next) # make real request
end
end
end
end
Adapters
Tesla supports multiple HTTP adapter that do the actual HTTP request processing.
Tesla.Adapter.Httpc
- the default, built-in erlang httpc adapterTesla.Adapter.Hackney
- hackney, “simple HTTP client in Erlang”Tesla.Adapter.Ibrowse
- ibrowse, “Erlang HTTP client”
When using ibrowse or hackney adapters remember to alter applications list in mix.exs
(for Elixir < 1.4)
def application do
[applications: [:tesla, :ibrowse, ...], ...] # or :hackney
end
and add it to the dependency list
defp deps do
[{:tesla, "~> 0.7.0"},
{:ibrowse, "~> 4.2"}, # or :hackney
{:poison, ">= 1.0.0"}] # for JSON middleware
end
Streaming
If adapter supports it, you can pass a Stream as body, e.g.:
defmodule ElasticSearch do
use Tesla
plug Tesla.Middleware.BaseUrl, "http://localhost:9200"
plug Tesla.Middleware.JSON
def index(records_stream) do
stream = records_stream |> Stream.map(fn record -> %{index: [some, data]})
post("/_bulk", stream)
end
end
Each piece of stream will be encoded as JSON and sent as a new line (conforming to JSON stream format)
Multipart
You can pass a Tesla.Multipart
struct as the body.
alias Tesla.Multipart
mp =
Multipart.new
|> Multipart.add_content_type_param("charset=utf-8")
|> Multipart.add_field("field1", "foo")
|> Multipart.add_field("field2", "bar", headers: [{:"Content-Id", 1}, {:"Content-Type", "text/plain"}])
|> Multipart.add_file("test/tesla/multipart_test_file.sh")
|> Multipart.add_file("test/tesla/multipart_test_file.sh", name: "foobar")
|> Multipart.add_file_content("sample file content", "sample.txt")
response = MyApiClient.post("http://httpbin.org/post", mp)
Testing
You can set the adapter to :mock
in tests.
# config/test.exs
config :tesla, adapter: :mock
Then, mock requests before using your client:
defmodule MyAppTest do
use ExUnit.Case
setup do
Tesla.Mock.mock fn
%{method: :get, url: "http://example.com/hello"} ->
%Tesla.Env{status: 200, body: "hello"}
%{method: :post, url: "http://example.com/world"} ->
%Tesla.Env{status: 200, body: "hi!"}
end
:ok
end
test "list things" do
assert %Tesla.Env{} = env = MyApp.get("/hello")
assert env.status == 200
assert env.body == "hello"
end
end
Writing middleware
A Tesla middleware is a module with call/3
function, that at some point calls Tesla.run(env, next)
to process
the rest of stack.
defmodule MyMiddleware do
@behaviour Tesla.Middleware
def call(env, next, options) do
env
|> do_something_with_request
|> Tesla.run(next)
|> do_something_with_response
end
end
The arguments are:
env
-Tesla.Env
instancenext
- middleware continuation stack; to be executed withTesla.run(env, next)
options
- arguments passed during middleware configuration (plug MyMiddleware, options
)
There is no distinction between request and response middleware, it’s all about executing Tesla.run/2
function at the correct time.
For example, a request logger middleware could be implemented like this:
defmodule Tesla.Middleware.RequestLogger do
@behaviour Tesla.Middleware
def call(env, next, _) do
IO.inspect env # print request env
Tesla.run(env, next)
end
end
and response logger middleware like this:
defmodule Tesla.Middleware.ResponseLogger do
@behaviour Tesla.Middleware
def call(env, next, _) do
res = Tesla.run(env, next)
IO.inspect res # print response env
res
end
end
See built-in middlewares for more examples.
Middleware should have documentation following this template:
elixir
defmodule Tesla.Middleware.SomeMiddleware do
@behaviour Tesla.Middleware
@moduledoc """
Short description what it does
Longer description, including e.g. additional dependencies.
### Example usage
```
defmodule MyClient do
use Tesla
plug Tesla.Middleware.SomeMiddleware, most: :common, options: "here"
end
```
### Options
- `:list` - all possible options
- `:with` - their default values
"""
end
Direct usage
You can also use Tesla directly, without creating a client module. This however won’t include any middleware.
# Example get request
response = Tesla.get("http://httpbin.org/ip")
response.status # => 200
response.body # => '{\n "origin": "87.205.72.203"\n}\n'
response.headers # => %{'Content-Type' => 'application/json' ...}
response = Tesla.get("http://httpbin.org/get", query: [a: 1, b: "foo"])
response.url # => "http://httpbin.org/get?a=1&b=foo"
# Example post request
response = Tesla.post("http://httpbin.org/post", "data", headers: %{"Content-Type" => "application/json"})
Cheatsheet
Making requests 101
# GET /path
get("/path")
# GET /path?a=hi&b[]=1&b[]=2&b[]=3
get("/path", query: [a: "hi", b: [1,2,3]])
# GET with dynamic client
get(client, "/path")
get(client, "/path", query: [page: 3])
# arguments are the same for GET, HEAD, OPTIONS & TRACE
head("/path")
options("/path")
trace("/path")
# POST, PUT, PATCH
post("/path", "some-body-i-used-to-know")
put("/path", "some-body-i-used-to-know", query: [a: "0"])
patch("/path", multipart)
Configuring HTTP functions visibility
# generate only get and post function
use Tesla, only: ~w(get post)a
# generate only delete fuction
use Tesla, only: [:delete]
# generate all functions except delete and options
use Tesla, except: [:delete, :options]
Disable docs for HTTP functions
use Tesla, docs: false
Decode only JSON response (do not encode request)
plug Tesla.Middleware.DecodeJson
Use other JSON library
# use JSX
plug Tesla.Middleware.JSON, engine: JSX, engine_opts: [strict: [:comments]]
# use custom functions
plug Tesla.Middleware.JSON, decode: &JSX.decode/1, encode: &JSX.encode/1
Custom middleware
defmodule Tesla.Middleware.MyCustomMiddleware do
def call(env, next, options) do
env
|> do_something_with_request
|> Tesla.run(next)
|> do_something_with_response
end
end
Contributing
- Fork it (https://github.com/teamon/tesla/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details
Copyright (c) 2015-2017 Tymon Tobolski