Tesla
Tesla is an HTTP client loosely based on Faraday. It embraces the concept of middleware when processing the request/response cycle.
Note that this README refers to the
master
branch of Tesla, not the latest released version on Hex. See the documentation for the documentation of the version you’re using.
0.x
to 1.0
Migration Guide
defp deps do
[{:tesla, "1.0.0-beta.1"}]
end
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:
{:ok, 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, "~> 1.0.0"},
{:jason, ">= 1.0.0"}] # optional, required by JSON middleware
end
Documentation
- Middleware
- Runtime middleware
- Adapters
- Streaming
- Multipart
- Testing
- Writing middleware
- Direct usage
- Cheatsheet
- Changelog
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.KeepRequest
- keep request body & headers
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 integration
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} -> {:ok, 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 it to the dependencies list in mix.exs
defp deps do
[{:tesla, "~> 1.0.0"},
{:jason, ">= 1.0.0"}, # optional, required by JSON middleware
{:hackney, "~> 1.10"}] # or :ibrowse
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")
{:ok, response} = MyApiClient.post("http://httpbin.org/post", mp)
Testing
You can set the adapter to Tesla.Mock
in tests.
# config/test.exs
# Use mock adapter for all clients
config :tesla, adapter: Tesla.Mock
# or only for one
config :tesla, MyApi, adapter: Tesla.Mock
Then, mock requests before using your client:
defmodule MyAppTest do
use ExUnit.Case
import Tesla.Mock
setup do
mock fn
%{method: :get, url: "http://example.com/hello"} ->
%Tesla.Env{status: 200, body: "hello"}
%{method: :post, url: "http://example.com/world"} ->
json(%{"my" => "data"})
end
:ok
end
test "list things" do
assert {:ok, %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
{:ok, 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" ...}]
{:ok, 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
{:ok, 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-2018 Tymon Tobolski