• Stars
    star
    2,218
  • Rank 20,797 (Top 0.5 %)
  • Language
    Elixir
  • License
    MIT License
  • Created over 11 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Yet Another HTTP client for Elixir powered by hackney

HTTPoison logo

HTTPoison Build Status Hex pm hex.pm downloads

HTTP client for Elixir, based on HTTPotion (documentation).

Installation

First, add HTTPoison to your mix.exs dependencies:

def deps do
  [
    {:httpoison, "~> 2.0"}
  ]
end

and run $ mix deps.get. Add :httpoison to your applications list if your Elixir version is 1.3 or lower:

def application do
  [applications: [:httpoison]]
end

Upgrading to 2.x.x

The main change that caused a major version is that ssl option now merges with the default options where previously it would override the ssl options. The new option ssl_override was added to allow people to keep the previous behaviour but it's more explicit now.

defp default_ssl_options() do
    [
      {:versions, [:"tlsv1.2", :"tlsv1.3"]},
      {:verify, :verify_peer},
      {:cacertfile, :certifi.cacertfile()},
      {:depth, 10},
      {:customize_hostname_check,
       [
         match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
       ]}
    ]
  end

More context here: #466

Usage

iex> HTTPoison.start
iex> HTTPoison.get! "https://postman-echo.com/get"
%HTTPoison.Response{
  status_code: 200,
  body: "{\n  \"args\": {},\n  \"headers\": {\n    \"x-forwarded-proto\": \"https\",\n    \"x-forwarded-port\": \"443\",\n    \"host\": \"postman-echo.com\",\n    \"x-amzn-trace-id\": \"Root=1-644624fb-769bca0458e739dc07f6b630\",\n    \"user-agent\": \"hackney/1.18.1\"\n  },\n  \"url\": \"https://postman-echo.com/get\"\n}",
  headers: [ ... ]
}

iex> HTTPoison.get! "http://localhost:1"
** (HTTPoison.Error) :econnrefused
iex> HTTPoison.get "http://localhost:1"
{:error, %HTTPoison.Error{id: nil, reason: :econnrefused}}

iex> HTTPoison.post "https://postman-echo.com/post", "{\"body\": \"test\"}", [{"Content-Type", "application/json"}]
{:ok,
 %HTTPoison.Response{
   status_code: 200,
   body: "{\n  \"args\": {},\n  \"data\": {\n    \"body\": \"test\"\n  },\n  \"files\": {},\n  \"form\": {},\n  \"headers\": {\n    \"x-forwarded-proto\": \"https\",\n    \"x-forwarded-port\": \"443\",\n    \"host\": \"postman-echo.com\",\n    \"x-amzn-trace-id\": \"Root=1-6446255e-703101813ec2e395202ab494\",\n    \"content-length\": \"16\",\n    \"user-agent\": \"hackney/1.18.1\",\n    \"content-type\": \"application/json\"\n  },\n  \"json\": {\n    \"body\": \"test\"\n  },\n  \"url\": \"https://postman-echo.com/post\"\n}",
   headers: [ ... ]
 }}

You can also easily pattern match on the HTTPoison.Response struct:

case HTTPoison.get(url) do
  {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
    IO.puts body
  {:ok, %HTTPoison.Response{status_code: 404}} ->
    IO.puts "Not found :("
  {:error, %HTTPoison.Error{reason: reason}} ->
    IO.inspect reason
end

Here is the list of all possible error reasons.

Options

There are a number of supported options(not to be confused with the HTTP options method), documented here, that can be added to your request. The example below shows the use of the :ssl and :recv_timeout options for a post request to an api that requires a bearer token. The :ssl option allows you to set options accepted by the Erlang SSL module, and :recv_timeout sets a timeout on receiving a response, the default is 5000ms.

token = "some_token_from_another_request"
url = "https://example.com/api/endpoint_that_needs_a_bearer_token"
headers = ["Authorization": "Bearer #{token}", "Accept": "Application/json; Charset=utf-8"]
options = [ssl: [{:versions, [:'tlsv1.2']}], recv_timeout: 500]
{:ok, response} = HTTPoison.get(url, headers, options)

And the example below shows the use of the :ssl options for a post request to an api that requires a client certification.

url = "https://example.org/api/endpoint_that_needs_client_cert"
options = [ssl: [certfile: "certs/client.crt"]]
{:ok, response} = HTTPoison.post(url, [], options)

Wrapping HTTPoison.Base

You can also use the HTTPoison.Base module in your modules in order to make cool API clients or something. The following example wraps HTTPoison.Base in order to build a client for the GitHub API (Poison is used for JSON decoding):

defmodule GitHub do
  use HTTPoison.Base

  @expected_fields ~w(
    login id avatar_url gravatar_id url html_url followers_url
    following_url gists_url starred_url subscriptions_url
    organizations_url repos_url events_url received_events_url type
    site_admin name company blog location email hireable bio
    public_repos public_gists followers following created_at updated_at
  )

  def process_request_url(url) do
    "https://api.github.com" <> url
  end

  def process_response_body(body) do
    body
    |> Poison.decode!
    |> Map.take(@expected_fields)
    |> Enum.map(fn({k, v}) -> {String.to_atom(k), v} end)
  end
end
iex> GitHub.start
iex> GitHub.get!("/users/myfreeweb").body[:public_repos]
37

It's possible to extend the functions listed below:

def process_request_body(body), do: body

def process_request_headers(headers) when is_map(headers) do
  Enum.into(headers, [])
end

def process_request_headers(headers), do: headers

def process_request_options(options), do: options

def process_request_url(url), do: url

def process_response_body(body), do: body

def process_response_chunk(chunk), do: chunk

def process_response_headers(headers), do: headers

def process_response_status_code(status_code), do: status_code

Async requests

HTTPoison now comes with async requests!

iex> HTTPoison.get! "https://github.com/", %{}, stream_to: self
%HTTPoison.AsyncResponse{id: #Reference<0.0.0.1654>}
iex> flush
%HTTPoison.AsyncStatus{code: 200, id: #Reference<0.0.0.1654>}
%HTTPoison.AsyncHeaders{headers: %{"Connection" => "keep-alive", ...}, id: #Reference<0.0.0.1654>}
%HTTPoison.AsyncChunk{chunk: "<!DOCTYPE html>...", id: #Reference<0.0.0.1654>}
%HTTPoison.AsyncEnd{id: #Reference<0.0.0.1654>}
:ok

Warning: this option can flood a receiver in messages.

If a server may send very large messages the async: :once option should be used. This will send only a single chunk at a time the receiver can call HTTPoison.stream_next/1 to indicate ability to process more chunks.

Cookies

HTTPoison allows you to send cookies:

iex> HTTPoison.get!("https://postman-echo.com/cookies", %{}, hackney: [cookie: ["session=a933ec1dd923b874e691; logged_in=true"]])
%HTTPoison.Response{
  status_code: 200,
  body: "{\n  \"cookies\": {\n    \"session\": \"a933ec1dd923b874e691\",\n    \"logged_in\": \"true\"\n  }\n}",
  headers: [ ... ]
}

You can also receive cookies from the server by reading the "set-cookie" headers in the response:

iex(1)> response = HTTPoison.get!("https://postman-echo.com/cookies/set?foo=1")
iex(2)> cookies = Enum.filter(response.headers, fn
...(2)> {key, _} -> String.match?(key, ~r/\Aset-cookie\z/i)
...(2)> end)
[ {"set-cookie", "foo=1; Path=/"}, ...]

You can see more usage examples in the test files (located in the test/) directory.

Connection Pools

Normally hackney opens and closes connections on demand, but it also creates a default pool of connections which are reused for requests to the same host. If the connection and host support keepalive, the connection is kept open until explicitly closed.

To use the default pool, you can just declare it as an option:

HTTPoison.get("httpbin.org/get", [], hackney: [pool: :default])

It is possible to use different pools for different purposes when a more fine grained allocation of resources is necessary.

Simple pool declaration

The easiest way is to just pass the name of the pool, and hackney will create it if it doesn't exist. Pools are independent from each other (they won't compete for connections) and are created with the default configuration.

HTTPoison.get("httpbin.org/get", [], hackney: [pool: :first_pool])
HTTPoison.get("httpbin.org/get", [], hackney: [pool: :second_pool])

Explicit pool creation

If you want to use different configuration options you can create a pool manually when your app starts with :hackney_pool.start_pool/2.

:ok = :hackney_pool.start_pool(:first_pool, [timeout: 15000, max_connections: 100])

From the already linked hackney's readme:

timeout is the time we keep the connection alive in the pool, max_connections is the number of connections maintained in the pool. Each connection in a pool is monitored and closed connections are removed automatically.

Disabling pool

If you don't want to use a pool for a single http request, you can do it by passing an option:

HTTPoison.get("httpbin.org/get", [], hackney: [pool: false])

If you want to disable the usage of the pool for every request you can do it by adding this to your environment configuration:

config :hackney, use_default_pool: false

You can find a little explanation here hackney's readme.

Pools as supervised processes

A third option is to add the pool as part of your supervision tree:

children = [
  :hackney_pool.child_spec(:first_pool, [timeout: 15000, max_connections: 100])
]

Add that to the application supervisor and first_pool will be available to be used by HTTPoison/hackney.

Multipart

Request

HTTPoison supports making multipart requests. E.g. with a local file:

HTTPoison.post("https://myurl.php", {:multipart, [{:file, "test.txt", {"form-data", [{"name", "mytest"}, {"filename", "test.txt"}]}, []}]})

Sometimes you may already have the file contents in memory and want to upload it elsewhere. A common example is fetching the file from a service like S3 and uploading it somewhere else. There is no need to persist the file locally, you can do the below:

binary_file_content = "Something you fetched and now have it in memory"
token = "some_token_from_another_request"
headers = [{"Authorization", "Bearer #{token}"}, {"Content-Type", "multipart/form-data"}]
options = [ssl: [{:versions, [:'tlsv1.2']}], recv_timeout: 500]

HTTPoison.request(
  :post,
  "https://myurl.com",
  {:multipart,
   [{"file", binary_file_content, {"form-data", [name: "file", filename: "a_file_name.txt"]}, []}]},
  headers,
  options
)

Further examples of multipart requests can be found in the issues (e.g.: here and here).

For more complex queries regarding multipart requests, you should follow the hackney docs for the multipart API.

Response

HTTPoison supports parsing multipart responses. E.g.:

iex(1)> response = %HTTPoison.Response{
...(1)>   body: "--123\r\nContent-type: application/json\r\n\r\n{\"1\": \"first\"}\r\n--123\r\nContent-type: application/json\r\n\r\n{\"2\": \"second\"}\r\n--123--\r\n",
...(1)>   headers: [{"Content-Type", "multipart/mixed;boundary=123"}],
...(1)>   request_url: "http://localhost",
...(1)>   status_code: 200
...(1)> }
%HTTPoison.Response{
  body: "--123\r\nContent-type: application/json\r\n\r\n{\"1\": \"first\"}\r\n--123\r\nContent-type: application/json\r\n\r\n{\"2\": \"second\"}\r\n--123--\r\n",
  headers: [{"Content-Type", "multipart/mixed;boundary=123"}],
  request_url: "http://localhost",
  status_code: 200
}

iex(2)> HTTPoison.Handlers.Multipart.decode_body(response)
[
  {[{"Content-Type", "application/json"}], "{\"1\": \"first\"}"},
  {[{"Content-Type", "application/json"}], "{\"2\": \"second\"}"}
]

For more complex queries regarding multipart response parsing, you should follow the hackney docs for the hackney_multipart API.

Logging

If you're running on top of hackney (which you probably are) there's a handy way to get detailed request logging:

# Add :runtime_tools to :extra_applications in mix.exs
def application do
  [extra_applications: [:logger, :runtime_tools]]
end
iex(1)> :hackney_trace.enable(:max, :io)

Just throw this in your code before your HTTPoison call and you'll get low-level log output.

License

Copyright Β© 2013-present Eduardo Gurgel <[email protected]>

This work is free. You can redistribute it and/or modify it under the
terms of the MIT License. See the LICENSE file for more details.

More Repositories

1

poxa

Pusher server implementation compatible with Pusher client libraries.
Elixir
1,035
star
2

verk

A job processing system that just verks! πŸ§›β€
Elixir
721
star
3

tentacat

Simple Elixir wrapper for the GitHub API
Elixir
443
star
4

mimic

A mocking library for Elixir
Elixir
386
star
5

solid

Liquid template engine in Elixir
Elixir
201
star
6

CMNavBarNotificationView

[unmaintained] An in-app notification view above the navigation bar "totally" based on MPNotificationView
Objective-C
124
star
7

httparrot

HTTP Request & Response Server. An incomplete clone of http://httpbin.org
Elixir
85
star
8

verk_web

A dashboard for the job processing system that just verks! πŸ§›β€
JavaScript
81
star
9

bertex

Elixir BERT encoder/decoder
Elixir
30
star
10

poxa-erlang

Open Pusher server implementation compatible with Pusher libraries.
Erlang
26
star
11

signaturex

Simple key/secret based authentication for APIs
Elixir
26
star
12

pusher

Elixir library to access the Pusher REST API.
Elixir
26
star
13

riex

[OLD] Riak & Elixir in a pool
Elixir
23
star
14

tldr_elixir_client

Elixir command line client for tldr
Elixir
15
star
15

wth

WTH is a command line tool much like old WTF that searchs on Urban Dictionary for a term definition.
Elixir
15
star
16

httpehaviour

Yet yet another HTTP client. This time using Behaviour
Elixir
14
star
17

watcher

GenEvent Watcher
Elixir
13
star
18

bertrpcex

A pool of BERT-RPC clients built on top of poolboy
Elixir
12
star
19

CMSignals

Qt Signals and Slots clone to Objective-C.
Objective-C
12
star
20

pusher_client

A websocket client to work with Pusher
Elixir
10
star
21

verk-stats

Verk Stats πŸ§›β€πŸ“Š
Elixir
4
star
22

bertrpc

A pool of BERT-RPC clients build on top of poolboy.
Erlang
3
star
23

defpool

Elixir macro to use pooler on a clean way
Elixir
2
star
24

compiz-plugins-ideas

Some ideas for new compiz plugins
C
1
star