How do I... Fetch data from an HTTP endpoint?
When writing bots you will probably need to talk to a server over HTTP sooner or later. Elixir/Erlang has several options to do so. Here is an example using a common library set up.
Making calls with HTTPoison and Jason
HTTPoison is a popular elixir HTTP library, and Jason is the standard
library for encoding and decoding JSON. You can add both to your bot project as
dependencies by adding them to the deps
section of your mix.ex
file:
defp deps do
[
{:credo, "~> 1.1", only: [:dev, :test]},
{:behavior_tree, "~> 0.3.0"},
{:bot_army, path: "vendor/bot_army/"},
{:httpoison, "~> 1.6"},
{:jason, "~> 1.1"}
# other deps...
]
end
Requests are straightforward, but read the HTTPoison documentation for full options.
A GET
looks like:
resp = HTTPoison.get!(url)
A POST
is similar, but includes an encoded body and headers:
headers = [{"Content-Type", "text/plain"}, {"charset", "utf-8"}]
body = %{field1: "value 1"} |> Jason.encode!()
resp = HTTPoison.post!(url, body, headers)
Handling errors
Since requests can fail, you should handle errors. In Elixir you could use a case
statement, but when you have a "chain" of operations that could error, with
statements are very useful. Each line indicates the operation and corresponding
expected result. Each result can be used on the lines beneath it, and in the body. If
any result does not match the expected result, it will jump to the else
section,
where you can pattern match the actual value to respond appropriately.
In this example we "chain" together making a request and decoding the result. Note
how we use the non-throwing version of the functions (ie. get
instead of get!
).
Finally we pattern match on the error types that the libraries provide:
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- HTTPoison.get(url),
{:ok, decoded} <- Jason.decode(body) do
Logger.info(inspect(decoded, pretty: true))
:succeed
else
{:ok, %HTTPoison.Response{status_code: 404}} ->
Logger.error("Not found :(")
:fail
{:error, %HTTPoison.Error{reason: reason}} ->
Logger.error(inspect(reason))
:fail
{:error, %Jason.DecodeError{} = err} ->
Logger.error(inspect(err))
:fail
end
Making an API wrapper
You may find it useful to create a separate file for the API that you are making requests against, that encapsulates the details of each request. Not only is this easier to call in your actions, but you could potentially "swap out" API wrappers based on a runtime config, which can be a powerful technique.
HTTPoison has an option to help you create an API wrapper if you are interested.