Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does @app.call(env) really do?

I really love to learn how the things work under de hood, specially when it comes to technology. Currently, I'm studying ruby more deeply and trying to use it only with rack in order to understand how rack based frameworks work.

At this moment, rack middlewares are getting me crazy. Why? Although middlewares are very simple, I'm a little confused about the @app.call(env). For the sake of clarity, consider the following code:

class MyCustomMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    @app.call(env) if env['REQUEST_METHOD'] != 'POST'

    body = env['rack.input'].clone
    body = JSON.parse(body.gets || {}, symbolize_names: true)
    body[:some_message] = "Peace, Love and Hope"

    env.update('rack.input', StringIO.new(JSON.dump(body)))

    @app.call(env)
  env
end

All I want to do is change the request body if (and only if) the request method is POST. If the request method is any other type than "POST", I want to pass the request to the next middleware (it works this way in Rack, right?). The problem, is that all the code is being executed, no matter if the request method is POST or not.

Maybe it can be a misunderstanding in regard to rack middlewares, as I'm used to work with Express.js. In Express, you have a stack of middlewares in which the requests pass through, and, each middleware calls the next() method in order to "release" the request. I was thinking that @app.call(env) would be similar to the Express' next() method... But looks like not, as the request is not being released when I call it and all the code is being executed.

Can somebody explain me what this method really does and point me where is my error?

like image 553
Pedro Vinícius Avatar asked Sep 26 '17 21:09

Pedro Vinícius


1 Answers

@app.call doesn't terminate the execution of your handler - it just calls the next middleware in the chain. It is expected that each middleware will either call the next in the chain and return its return value, or terminate the chain by returning an array of [status_code, body, headers]. Each middleware is expected to pass the array of [status_code, body, headers] back up the chain, by returning that value out of its #call method. Recall that in Ruby, the return value of the last statement of each method is implicitly returned to its caller.

As written, you're going to invoke the remaining middleware in the stack, then discard its result, and then continue on with your handler, run the code, invoke the remaining middleware stack again, and then finally return that result back upstream.

Just explicitly return if you want to bail out of the handler:

def call(env)
  return @app.call(env) if env['REQUEST_METHOD'] != 'POST'

  body = env['rack.input'].clone
  body = JSON.parse(body.gets || {}, symbolize_names: true)
  body[:some_message] = "Peace, Love and Hope"

  env.update('rack.input', StringIO.new(JSON.dump(body)))

  @app.call(env)
end

It may be more clear to just run your mutators conditionally, then always @app.call to terminate the handler:

def call(env)
  mutate!(env) if env['REQUEST_METHOD'] == "POST"
  @app.call(env)
end

def mutate!(env)
  body = env['rack.input'].clone
  body = JSON.parse(body.gets || {}, symbolize_names: true)
  body[:some_message] = "Peace, Love and Hope"
  env.update('rack.input', StringIO.new(JSON.dump(body)))
end

Since @app.call is the last statement in #call here, its return value is returned to your middleware's caller.

like image 162
Chris Heald Avatar answered Oct 22 '22 18:10

Chris Heald