wojtekmach

wojtekmach

Hex Core Team

Hey everyone!

Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out there so why create a new one? Two things: great out of the box experience and extensibility.

Regarding out of the box experience, let’s first see it in action:

Mix.install([
  {:req, "~> 0.3.0"}
])

Req.get!("https://api.github.com/repos/elixir-lang/elixir").body["description"]
#=> "Elixir is a dynamic, functional language designed for building scalable and maintainable applications"

Req.get!("http://api.github.com").status
# 23:24:11.670 [debug] follow_redirects: redirecting to https://api.github.com/
#=> 200

Req.get!("https://httpbin.org/status/500,200").status
# 19:02:08.463 [error] retry: got response with status 500, will retry in 2000ms, 2 attempts left
# 19:02:10.710 [error] retry: got response with status 500, will retry in 4000ms, 1 attempt left
#=> 200

Req automatically decompress and decodes response body, follows redirects, retries in face of errors, and more. See “Features” section in the README for the whole list.

Regarding extensibility, virtually all of Req functionality is broken down into individual pieces - steps. Req works by running the request struct through these steps. You can easily reuse or rearrange built-in steps or write new ones. Steps are similar to Tesla Middleware although they are very different in implementation. Steps are just regular functions:

debug_url = fn request ->
  IO.inspect(URI.to_string(request.url))
  request
end

req =
  Req.new(base_url: "https://api.github.com")
  |> Req.Request.append_request_steps(debug_url: debug_url)

Req.get!(req, url: "/repos/wojtekmach/req").body["description"]
# Outputs: "https://api.github.com/repos/wojtekmach/req"
#=> "Req is a batteries-included HTTP client for Elixir."

See Req.Steps module for a list of all built-in steps.

After writing custom Req steps we can make them even easier to use by others by packaging them up into plugins. Here are some examples:

Mix.install([
  {:req, "~> 0.3.0"},
  {:req_easyhtml, github: "wojtekmach/req_easyhtml"},
  {:req_s3, github: "wojtekmach/req_s3"},
  {:req_hex, github: "wojtekmach/req_hex"}
])

req =
  (Req.new(http_errors: :raise)
  |> ReqEasyHTML.attach()
  |> ReqS3.attach()
  |> ReqHex.attach())

Req.get!(req, url: "https://elixir-lang.org").body[".entry-summary h5"]
#=>
# #EasyHTML[<h5>
#    Elixir is a dynamic, functional language for building scalable and maintainable applications.
#  </h5>]

Req.get!(req, url: "s3://ossci-datasets").body
#=>
# [
#   "mnist/",
#   "mnist/t10k-images-idx3-ubyte.gz",
#   "mnist/t10k-labels-idx1-ubyte.gz",
#   "mnist/train-images-idx3-ubyte.gz",
#   "mnist/train-labels-idx1-ubyte.gz"
# ]

Req.get!(req, url: "https://repo.hex.pm/tarballs/req-0.1.0.tar").body["metadata.config"]["links"]
#=> %{"GitHub" => "https://github.com/wojtekmach/req"}

Plugins are nothing more than a convention (there’s no plugin contract) and I’m still figuring out what makes and doesn’t make sense to be a plugin. See “Writing Plugins” section in Req.Request module documentation for a little bit more information about plugins.

If you’re new to Req, I hope this post serves as a good introduction. If you have heard about it before, you may want to check the latest v0.3 release.

Any feedback is appreciated. Happy hacking!

https://github.com/wojtekmach/req

Showing Posts 1 to 10

tj0

tj0

All praise sensible defaults! I got bitten once by one of the clients not verifying SSL. Now it’s the first thing I check. :rofl:

Req.get!("https://wrong.host.badssl.com/")
10:19:47.183 [info] TLS :client: In state :certify at ssl_handshake.erl:1990 generated CLIENT ALERT: Fatal - Handshake Failure
 - {:bad_cert, :hostname_check_failed}
10:19:47.184 [error] retry: got exception, will retry in 1000ms, 3 attempts left
10:19:47.184 [error] ** (Mint.TransportError) TLS client: In state certify at ssl_handshake.erl:1990 generated CLIENT ALERT: Fatal - Handshake Failure
 {bad_cert,hostname_check_failed}
sergio

sergio

Will swap out my Tesla usage with this. I love sensible defaults and Req seems less code to write. I hate writing code. :slight_smile:

stefanchrobot

stefanchrobot

In my app I’m making requests to user-defined URLs, so errors are expected and I guess my feedback is mostly around that. Have a look at this:

iex> Req.request(url: "https://bad.domain")
22:23:59.915 [error] retry: got exception, will retry in 1000ms, 3 attempts left
22:23:59.915 [error] ** (Mint.TransportError) non-existing domain
22:24:00.932 [error] retry: got exception, will retry in 2000ms, 2 attempts left
22:24:00.933 [error] ** (Mint.TransportError) non-existing domain
22:24:02.945 [error] retry: got exception, will retry in 4000ms, 1 attempt left
22:24:02.945 [error] ** (Mint.TransportError) non-existing domain
{:error, %Mint.TransportError{reason: :nxdomain}}
  • Would be nice to have Req.get (not only the ! versions),
  • Would be nice to be able to abstract away the adapter: here I need to know the adapter and the possible errors as defined by the adapter; changing the adapter requires changing the error handling code,
  • I think the default retries should be more aggressive: something like 2 attempts with just a few milliseconds of delay,
  • I don’t think that retrying non-existing domain error is a sensible default.

And two general comments:

  • As a step “consumer” it would be nice to be able to ignore the difference between request, response and error steps,
  • It’s unclear how request options relate to step options. Personally I’d find it clearer if Req.request(foo: [bar: 1]) meant that [bar: 1] are the options for the :foo step.
wojtekmach

wojtekmach OP

Hex Core Team

Thanks for feedback!

  • Would be nice to have Req.get (not only the ! versions),

I might ultimately do that but no plans at the moment. I think the ! version is what most people would use most of the time, i.e. crash on transient errors (after N retries) or some serious misconfiguration (of eg ssl options), at which point it’s not like at the call site we can do much in that situation anyway so might as well crash. I’ll definitely keep this in mind though.

  • Would be nice to be able to abstract away the adapter: here I need to know the adapter and the possible errors as defined by the adapter; changing the adapter requires changing the error handling code,

Do you have specific kinds of errors you want to differentiate between?

  • I think the default retries should be more aggressive: something like 2 attempts with just a few milliseconds of delay,

On the flip side if we’re erroring because service is under load, retrying again in a few milliseconds won’t help and in fact will make things worse. I went with the backoff values (1s, 2s, 4s, 8s, …) that curl has. It is very easy to provide your own retry strategy, fwiw.

  • I don’t think that retrying non-existing domain error is a sensible default.

Agreed. Looking at it again, retrying on transport errors (which, to your point, are adapter specific) needs rethinking. curl handles some of 4xx/5xx by default plus opts-in to handling ECONNREFUSED with --retry-connrefused and that seems sensible enough. I will look into that.

  • As a step “consumer” it would be nice to be able to ignore the difference between request, response and error steps,

Not sure what you mean by this, can you elaborate?

  • It’s unclear how request options relate to step options. Personally I’d find it clearer if Req.request(foo: [bar: 1]) meant that [bar: 1] are the options for the :foo step.

They don’t relate, it is arbitrary. I actually started with having options be named after steps and nesting but found it a bit too verbose at times, e.g.: follow_redirects: [max_redirects: 10]. Besides this, there are options which are affecting multiple steps, for example raw: true would disable both decompress_body and decode_body. Overall I’m pretty happy with the “flatter” options but totally understand the concern. Curious what others think.

stefanchrobot

stefanchrobot

Not sure really, I just don’t like the fact that it’s an opaque term. That’s an issue that I have with a lot of HTTP clients and I’d like to see an improvement in this area. Can I log the error? It might contain sensitive information. I could pattern match, but then what happens when I switch adapters? Is it always going to be a map with a :reason key?

Makes sense. From my experience the retries usually address random network errors, so short retries are better. But I guess there’s no universal answer to this.

Would be nice nice to be able to just say append_steps instead of append_*_steps since I guess most of the time the step already implies where it should be.

Unless I’d learn the steps and all the config options more or less by heart, the fact that they are arbitrary makes it confusing. I don’t like when things are arbitrary or too implicit. I think there are ways to address the verbosity - the step name implies what the settings do, so you could accept all of the following:

  • follow_redirects: [max: 10],
  • follow_redirects: true,
  • follow_redirects: 10.

As for options that affects multiple steps, I’m not sure that’s the optimal design. Can I decompress the body without decoding it?

hubertlepicki

hubertlepicki

This is very nice. How hard would it be, in your opinion, to add support for the streaming request body and also streaming responses? I have a pretty specific use case where I generate files I need to send out on the fly, and I don’t want to store them at all, but they can be fairly large (> 1GB) so currently, I am generating Elixir Stream and feed it as request body to Finch (added that part to the lib) Finch — Finch v0.23.0 and when I receive the files on the other end I also turn them into Elixir stream and consume line by line. Unfortunately HTTP is all I have to talk to between both apps, but so far it’s been working great with Finch.

wojtekmach

wojtekmach OP

Hex Core Team

What do you mean by opaque term? I believe it is pretty concrete. For Req by default it’s Finch.Error, Mint.HTTPError, or Mint.TransportError. You’re totally right that if switching adapters you’d get different errors so if you have error handling code, it would need to be updated. But what I want to figure out is what kind of error handling do you actually have. Because in my experience if I get any of these errors I cannot do anything with them anyway, I cannot recover from them, so the sensible thing is just to crash.

Perhaps I should make a bit less emphasis on being able to switch adapters because honestly I don’t see the point. Finch is great. :slight_smile: To me switching adapters is only useful in tests.

Would be nice nice to be able to just say append_steps instead of append_*_steps since I guess most of the time the step already implies where it should be.

Gotcha, sorry, that is not possible. We have three buckets, request, response or error steps, so
when we add something we need to know what type of a thing it is, we cannot infer it.

Unless I’d learn the steps and all the config options more or less by heart, the fact that they are arbitrary makes it confusing.

I’m not sure if it addresses your concern but fwiw all the available options for built-in steps
are documented in a single place: Req.request/1 options.

Can I decompress the body without decoding it?

Yes, you can set decode_body: false.

wojtekmach

wojtekmach OP

Hex Core Team

Thanks! Streaming request body is trivial as finch already does it (thank you for adding it!) but streaming response is pretty tricky. See Response streaming · Issue #82 · wojtekmach/req · GitHub for some discussion. It’s definitely on my mind and unless we have it, I won’t consider Req complete, but there’s no concrete plans at the moment, unfortunately.

stefanchrobot

stefanchrobot

You know the errors because you’re the author. As a user, I can only see this:

@spec request(Req.Request.t() | keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()}

Not as opaque as hackney:

request(URL::url() | binary() | list()) -> {ok, integer(), list(), client_ref()} | {ok, integer(), list()} | {error, term()}

but still not very useful. Ideally this would be {:error, Req.Error.t()} with a well-defined set of possible values.

In my use case those errors are expected (e.g. mistyped URL), so I don’t want to crash.

Supporting only one adapter is perfectly fine for me - I was pretty happy with HTTPoison at some point. Support for replacing the adapter for tests is very important though.

wojtekmach

wojtekmach OP

Hex Core Team

I’m skeptical about adding a Req.Error because I think it would be inferior to e.g. Mint.TransportError (it has a nice Exception.message/1 callback implementation) and I’m not sure I can reliably keep them in sync. I’m skeptical about adding strict error contract, one that would be useful for control flow, because I don’t think errors should be used for control flow. (I’m kind of doing that in retry step and maybe that’s my mistake.) I don’t have an answer for it but I’ll keep at it. Thanks for bringing this up!

Where Next? Top

Trending in Announcing Top

mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New
quatermain
Hello, I’m sharing my plugin here in forum after some time so it has time to mature and proof yourself. I use Claude Code daily on a pr...
New
gtcode
Aloha gang, There’s a new class of AI model out from TypeSafe. Jev is their flagship model. You can learn about it here: ...
New
LostKobrakai
I’ve recently created a small library phoenix_vite integrating the vite build tooling with phoenix. It provides an igniter.installer t...
New
danschultzer
I needed something like premailer for rails, and after searching, I decided to make an easy-to-use library to handle inline styles and co...
New
kraleppa
If you’ve ever had to debug a BEAM node you know it can be a painful process. Parsing raw data, or dealing with clutter often gets in the...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
AstonJ
AI tools and services seem to be the new JS framework - with new ones popping up every 5 minutes - hence it might be worth posting one of...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews