Skip to content
/ tesla Public
forked from elixir-tesla/tesla

The flexible HTTP client library for Elixir, with support for middleware and multiple adapters.

License

Notifications You must be signed in to change notification settings

hodak/tesla

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tesla

Build Status Hex.pm Hex.pm Hex.pm codecov Inline docs

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.


defp deps do
  [{:tesla, "~> 1.2.1"}]
end

Documentation for 0.x branch


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.2.0"},
    {:hackney, "~> 1.14.0"}, # optional, but recommended adapter
    {:jason, ">= 1.0.0"} # optional, required by JSON middleware
  ] 
end

Configure default adapter in config/config.exs (optional).

# config/config.exs

config :tesla, adapter: Tesla.Adapter.Hackney

The default adapter is erlang's built-in httpc, but it is not recommended to use it in production environment as it does not validate SSL certificates among other issues.

Documentation

Middleware

Tesla is built around the concept of composable middlewares. This is very similar to how Plug Router works.

Basic

Formats

Auth

Error handling

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.client to create a dynamic middleware function:

defmodule GitHub do
  # notice there is no `use Tesla`

  def user_repos(client, login) do
    # pass `client` argument to `get` function
    Tesla.get(client, "/user/" <> login <> "/repos")
  end

  def issues(client) do
    Tesla.get(client, "/issues")
  end

  # build dynamic client based on runtime arguments
  def client(token) do
    middleware = [
      {Tesla.Middleware.BaseUrl, "https://api.github.com"},
      Tesla.Middleware.JSON,
      {Tesla.Middleware.Headers, [{"authorization", "token: " <> token }]}
    ]

    Tesla.client(middleware)
  end
end

and then:

client = GitHub.client(user_token)
client |> GitHub.user_repos("teamon")
client |> GitHub.get("/me")

Adapters

Tesla supports multiple HTTP adapter that do the actual HTTP request processing.

When using ibrowse or hackney adapters remember to it to the dependencies list in mix.exs

defp deps do
  [{:tesla, "~> 1.2.0"},
   {:jason, ">= 1.0.0"}, # optional, required by JSON middleware
   {:hackney, "~> 1.10"}] # or :ibrowse
end

Adapter options

In case there is a need to pass specific adapter options you can do it in one of three ways:

Using adapter macro:

defmodule GitHub do
  use Tesla

  adapter :hackney, recv_timeout: 30_000, ssl: [certfile: "certs/client.crt"]
end

Using Tesla.client/2:

def new(...) do
  middleware = [...]
  adapter = {Tesla.Adapter.Hackney, [recv_timeout: 30_000]}
  Tesla.client(middleware, adapter)
end

Passing directly to get/post/etc.

MyClient.get("/", opts: [adapter: [recv_timeout: 30_000]])
Tesla.get(client, "/", opts: [adapter: [recv_timeout: 30_000]])

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]} end)
    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 instance
  • next - middleware continuation stack; to be executed with Tesla.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:

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

  1. Fork it (https://github.com/teamon/tesla/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. 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

About

The flexible HTTP client library for Elixir, with support for middleware and multiple adapters.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Elixir 100.0%