Faraday: Rety Middleware won't run after parser

Created on 23 Apr 2021  ·  4Comments  ·  Source: lostisland/faraday

Basic Info

  • Faraday Version: 1.4.1
  • Ruby Version: 2.7.2

Issue description

_NB: Please let me know if this belongs in faraday_middleware instead. As it involves two middlewares, I didn't know where to put it._

When creating a connection, I expect (as is documented) the middleware will run in the order it is declared. However, when a request fails and I load the :json parsing middleware followed by the :retry middleware, env.body is unparsed in the scope of the :retry middleware's :retry_if block:

I need to run the :retry_if block on the JSON-decoded message because the JSON document can be very large if the request is successful, and the alternative is to parse it a second time in the retry_if, which seems totally unnecessary.

Steps to reproduce

@_client ||= Faraday.new(url: ROOT_URL) do |f|
      f.options[:timeout] = 10
      f.headers['Content-Type'] = 'application/json'
      f.use :instrumentation, name: INSTRUMENTATION_EVENT_NAME
      f.response :json, content_type: /\bjson$/
      f.request :retry, FARADAY_RETRY_OPTIONS
end

Note: If I print f.instance_variable_get :@handlers, inside the block, I see the middleware in the order I declared it.

When a request fails, and I evaluate these expressions in the :retry middleware's :retry_if block, I get the following output:

(byebug) environment.response.to_hash.without(:url)
{:status=>405, :body=>"{\"message\": \"The method is not allowed for the requested URL.\"}\n", :response_headers=>{"date"=>"Fri, 23 Apr 2021 18:17:16 GMT", "content-type"=>"application/json", "content-length"=>"64", "connection"=>"keep-alive", "server"=>"gunicorn", "allow"=>"HEAD, OPTIONS, GET"}}

(byebug) environment.response.body
"{\"message\": \"The method is not allowed for the requested URL.\"}\n"
(byebug) environment.response.body.class
String

Basically, the parsing middleware just refuses to run first.

The JSON is well-formed and the response's Content-Type header is correctly set, so I'm stumped.

My only guess is it's something to do with #response middleware always runs before #request middleware, but in this case retry being #request middleware is something of a misnomer because it implicitly needs to be evaluated after the response is returned.

Most helpful comment

Hi @mvastola, what you're experiencing is the expected behaviour!
I know it's hard to get your head around it at first, but given the nature of the middleware stack being similar to that of a rack stack, middleware are traversed in reversed order for responses.
In our website we have a picture that illustrates this to make it easier to understand.

Hence you already did the right thing 🙌! By moving the :json middleware after the :retry one, you allow the response to be processed by that first, and the :retry middleware will find the response body parsed.

I'm closing this now but feel free to follow-up if you have any further questions

All 4 comments

Update: I literally flipped the order of the two middlewares in the connection config block (while thinking "this can't possibly work") and now it works perfectly. Leaving this though because I have no idea what's going on and it still seems like a bug (or at least should be better-documented).

Hi @mvastola, what you're experiencing is the expected behaviour!
I know it's hard to get your head around it at first, but given the nature of the middleware stack being similar to that of a rack stack, middleware are traversed in reversed order for responses.
In our website we have a picture that illustrates this to make it easier to understand.

Hence you already did the right thing 🙌! By moving the :json middleware after the :retry one, you allow the response to be processed by that first, and the :retry middleware will find the response body parsed.

I'm closing this now but feel free to follow-up if you have any further questions

Thanks for the quick, kind, and thorough explaination. It's not that hard to understand, but it's a bit unintuitive here. I wonder if there's an effective way to make it less so.

Docs

I had actually seen the documentation you linked, and it made me think I knew what I needed to do. (I saw the picture, and the stuff about "innermost" middleware, but I didn't really process that it meant reversing the order.)

The image shown is informative with regard to the internals of middleware, but it might be helpful to compliment it with a visual indication of how the order in the config block affects the run order.

Configuration

Beyond that, there are a bunch of details of this implementation that make this unintuitive:

  • There are three different functions in faraday to add middleware, and they are not interchangable: each piece of middleware seems to only work with one of them. This makes it seem as if there are three different Middleware stacks (or orderings) independent of each other.

  • What these three different names/groups of Middleware do is very unclear, however, since most Middleware does _not_ run at both ends of the request->response pathway, a user would be apt to assume "request" tasks always happen before "response" related things.

  • The rack keyword to add middleware, use, applies (as best I can tell) in Faraday only to that sort of middleware -- middleware that wraps both the request and the response.

Anyway I'm happy to try to think up some ideas if you're open to tweaking the API for this issue. Otherwise at least I get this now. I'm only saying this because I can easily see other users suffering the same confusion here.

Thanks @mvastola, this is invaluable feedback 🙏!
And to be fair, this is not the first time I hear about this things.

When it comes to documentation, I agree we're a long way off, so any contribution to improve that is very much welcome (there's actually a very recent discussion thread about that!).

The points on configuration are also very valid, let me just say that the request, response and use methods confusion have been raised multiple times, to the point that I'm actually considering to simply drop them in favour of just having use, leaving then to the middleware to document what/when they'll be manipulating the request/response.
So I'd suggest not to push any PR to change that API just yet, but we're very much open to hear the community on this topic via discussions

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aleksb86 picture aleksb86  ·  3Comments

subvertallchris picture subvertallchris  ·  5Comments

ryanbyon picture ryanbyon  ·  3Comments

jeffb-stell picture jeffb-stell  ·  5Comments

luizkowalski picture luizkowalski  ·  3Comments