Req — A batteries-included HTTP client for Elixir

Hey everyone, some Req updates since v0.4.0 below.

New step: checksum

Sets expected response body checksum.

iex> resp = Req.get!("https://httpbin.org/json", checksum: "sha1:9274ffd9cf273d4a008750f44540c4c5d4c8227c")
iex> resp.status
200

iex> Req.get!("https://httpbin.org/json", checksum: "sha1:bad")
** (Req.ChecksumMismatchError) checksum mismatch
expected: sha1:bad
actual:   sha1:9274ffd9cf273d4a008750f44540c4c5d4c8227c

New step put_aws_sigv4

Signs request with AWS Signature Version 4.

iex> req =
...>   Req.new(
...>     base_url: "https://s3.amazonaws.com",
...>     aws_sigv4: [
...>       access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
...>       secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
...>       service: :s3
...>     ]
...>   )
iex>
iex> %{status: 200} = Req.put!(req, "/bucket1/key1", body: "Hello, World!")
iex> resp = Req.get!(req, "/bucket1/key1").body
"Hello, World!"

Request body streaming also works (though content-length header must be explicitly set):

iex> path = "a.txt"
iex> File.write!(path, String.duplicate("a", 100_000))
iex> size = File.stat!(path).size
iex> chunk_size = 10 * 1024
iex> stream = File.stream!(path, chunk_size)
iex> %{status: 200} = Req.put!(req, url: "/key1", headers: [content_length: size], body: stream)
iex> byte_size(Req.get!(req, "/bucket1/key1").body)
100_000

Req.Test

Functions for creating test stubs.

Stubs provide canned answers to calls made during the test, usually not responding at all to
anything outside what’s programmed in for the test.

“Mocks Aren’t Stubs” by Martin Fowler

Req has built-in support for stubs via :plug, :adapter, and (indirectly) :base_url
options. This module enhances these capabilities by providing:

Example

Imagine we’re building an app that displays weather for a given location using an HTTP weather
service:

defmodule MyApp.Weather do
  def get_rating(location) do
    case get_temperature(location) do
      {:ok, %{status: 200, body: %{"celsius" => celsius}}} ->
        cond do
          celsius < 18.0 -> {:ok, :too_cold}
          celsius < 30.0 -> {:ok, :nice}
          true -> {:ok, :too_hot}
        end

      _ ->
        :error
    end
  end

  def get_temperature(location) do
    [
      base_url: "https://weather-service"
    ]
    |> Keyword.merge(Application.get_env(:myapp, :weather_req_options, []))
    |> Req.request(options)
  end
end

We configure it for production:

# config/runtime.exs
config :myapp, weather_req_options: [
  auth: {:bearer, System.fetch_env!("MYAPP_WEATHER_API_KEY")}
]

And tests:

# config/test.exs
config :myapp, weather_req_options: [
  plug: {Req.Test, MyApp.Weather}
]

And now we can easily stub out values in concurrent tests:

use ExUnit.Case, async: true

test "nice weather" do
  Req.Test.stub(MyApp.Weather, fn conn ->
    Req.Test.json(conn, %{"celsius" => 25.0})
  end)

  assert MyApp.Weather.get_rating("Krakow, Poland") == {:ok, :nice}
end

Full changelog below:

v0.4.10 (2024-02-19)

  • run_finch: Default to connect_options: [protocols: [:http1, :http2]].

  • run_finch: Change version requirement to ~> 0.17, that is all versions up to 1.0.

  • put_aws_sigv4: Support streaming request body.

  • auth: Always update authorization header.

  • decode_body: Gracefully handle multiple content-type values.

  • Req.Request.new/1: Use URI.parse for now.

v0.4.9 (2024-02-14)

  • retry: Raise on invalid return from :retry_delay function

  • run_finch: Update to Finch 0.17

  • run_finch: Deprecate connect_options: [protocol: ...] in favour of
    connect_options: [protocols: ...]] which defaults to [:http1, :http2], that is,
    make request using HTTP/1 but if negotiated switch to HTTP/2 over the HTTP/1 connection.

  • New step: put_aws_sigv4 - signs request with AWS Signature Version 4.

v0.4.8 (2023-12-11)

  • put_plug: Fix response streaming. Previously we were relying on unreleased
    Plug features (which may never get released). Now, Plug adapter will emit the
    entire response body as one chunk. Thus,
    plug: plug, into: fn ... -> {:halt, acc} end is not yet supported as it
    requires Plug changes that are still being discussed. On the flip side,
    we should have much more stable Plug integration regardless of this small
    limitation.

v0.4.7 (2023-12-11)

  • put_plug: Don’t crash if plug is not installed and :plug is not used

v0.4.6 (2023-12-11)

  • New step: checksum
  • put_plug: Fix response streaming when plug uses send_resp or send_file
  • retry: Retry on :closed

v0.4.5 (2023-10-27)

  • decompress_body: Remove content-length header

  • auth: Deprecate auth: {user, pass} in favour of auth: {:basic, "user:pass"}

  • Req.Request: Allow steps to be {mod, fun, args}

v0.4.4 (2023-10-05)

  • compressed: Check for optional depenedencies brotli and ezstd only at compile-time.
    (backported from v0.3.12.)

  • decode_body: Check for optional depenedency nimble_csv at compile-time.
    (backported from v0.3.12.)

  • run_finch: Add :finch_private option

v0.4.3 (2023-09-13)

v0.4.2 (2023-09-04)

  • put_plug: Handle response streaming on Plug 1.15+.

  • Don’t warn on mixed-case header names

v0.4.1 (2023-09-01)

  • Fix Req.Request Inspect regression
26 Likes