How to Set a Cookie with a (Ruby) Rack Middleware Component

How do I set a cookie with a (ruby) rack middleware component?

If you want to use the Response class, you need to instantiate it from the results of calling the middleware layer further down the stack.
Also, you don't need instance variables for a middleware like this and probably don't want to use them that way(@status,etc would stay around in the middleware instance after the request is served)

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

def call(env)
status, headers, body = @app.call(env)
# confusingly, response takes its args in a different order
# than rack requires them to be passed on
# I know it's because most likely you'll modify the body,
# and the defaults are fine for the others. But, it still bothers me.

response = Rack::Response.new body, status, headers

response.set_cookie("foo", {:value => "bar", :path => "/", :expires => Time.now+24*60*60})
response.finish # finish writes out the response in the expected format.
end
end

If you know what you are doing you could directly modify the cookie header, if you don't want to instantiate a new object.

How to run a simple Ruby Rack app that consists of only middleware?

A Rack app (including middleware) is basically just an object that responds to call, accepting a hash describing the request and returning an array describing the response.

When using a rackup file, Rack uses the DSL described in Rack::Builder. The run method just sets the “base” app:

def run(app)
@run = app
end

The use method stores the middleware classes you want to include in your app, and then when constructing the final app does something like this for each one (in reverse order of how they appear in the config.ru file):

@run = MiddleWare.new(@run, other_args)

It’s a little more complex than this, but this is the general idea. A new instance of the middleware class is created, with the existing app passed as the first argument of the constructor, and the resulting object becomes the new app. This is the (not well documented) “interface” to Rack middleware: the first argument to its initializer is the app it is wrapping, the rest are any other arguments passed to use.

The DSL expects there will always be either a run or map statement, you will get an error if you omit both.

If your middleware is written in such a way that it can handle having no arguments passed to its initializer and it will behave like a full app, then you may be able to use it directly as the app in config.ru:

run SomeMiddleware.new

This is what Sinatra does to allow it to be used as middleware. It stores the app in the initializer if it is given, and then when a request arrives that doesn’t match any route it uses the presence of @app to decide whether to behave as middleware and pass the request on, or to behave as the final app and treat it as a not found error.

If your middleware doesn’t have this flexibility then you will need to provide an app for it to wrap, as you have in your example. In this case it may also be useful to have the app for error handling, in the event that the middleware doesn’t correctly handle a request and tries to pass it on to the wrapped app.

If you want to avoid using separate use and run statements in your config.ru, you could just use run, and pass a simple app directly to your middleware:

run SomeMiddleware.new(->(e){
[500, {'Content-type' => 'text/plain'}, ["Error: SomeMiddleware didn't handle request"]]
}

Note how this follows the interface described above for middleware: the first argument to the initializer is the app to wrap.

How do I set/get session vars in a Rack app?

session is a method that is part of some web frameworks, for example Sinatra and Rails both have session methods. Plain rack applications don’t have a session method, unless you add one yourself.

The session hash is stored in the rack env hash under the key rack.session, so you can access it like this (assuming you’ve named the rack environment to your app env):

env['rack.session'][:msg]="Hello Rack"

Alternatively, you could use Rack’s built in request object, like this:

request = Rack::Request.new(env)
request.session[:msg]="Hello Rack"

How to fix Ruby Rack SessionId error Error: undefined class/module Rack::Session::SessionId when decoding the rack.session cookie

Thanks to Louis Nyffenegger (@snyff) who helped me with this
https://twitter.com/snyff/status/1407054131697758211?s=20

This error can be solved by creating a method stub

require 'pp'
require 'base64'
require 'uri'
require 'rack'

class Rack::Session::SessionId
end

c = gets
cookie, signature = c.split("--")
decoded = Base64.decode64(URI.decode(cookie))
begin
object = Marshal.load(decoded)
pp object
rescue ArgumentError => e
puts "Error: " + e.to_s
end

What is Rack middleware?

Rack as Design

Rack middleware is more than "a way to filter a request and response" - it's an implementation of the pipeline design pattern for web servers using Rack.

It very cleanly separates out the different stages of processing a request - separation of concerns being a key goal of all well designed software products.

For example with Rack I can have separate stages of the pipeline doing:

  • Authentication: when the request arrives, are the users logon details correct? How do I validate this OAuth, HTTP Basic Authentication, name/password?

  • Authorization: "is the user authorised to perform this particular task?", i.e. role-based security.

  • Caching: have I processed this request already, can I return a cached result?

  • Decoration: how can I enhance the request to make downstream processing better?

  • Performance & Usage Monitoring: what stats can I get from the request and response?

  • Execution: actually handle the request and provide a response.

Being able to separate the different stages (and optionally include them) is a great help in developing well structured applications.

Community

There's also a great eco-system developing around Rack Middleware - you should be able to find pre-built rack components to do all of the steps above and more. See the Rack GitHub wiki for a list of middleware.

What's Middleware?

Middleware is a dreadful term which refers to any software component/library which assists with but is not directly involved in the execution of some task. Very common examples are logging, authentication and the other common, horizontal processing components. These tend to be the things that everyone needs across multiple applications but not too many people are interested (or should be) in building themselves.

More Information

  • The comment about it being a way to filter requests probably comes from the RailsCast episode 151: Rack Middleware screen cast.

  • Rack middleware evolved out of Rack and there is a great intro at Introduction to Rack middleware.

  • There's an intro to middleware on Wikipedia here.

Rack middleware trapping stack trace

I finally found the solution to this. It turns out that the last line in what is considered my application is in the middleware. I was running the rest of the code in a local rails engine located in a components directory. All we needed to do was create a new silencer for BacktraceCleaner. Notice components dir is now included.

# config/initializers/backtrace_silencers.rb
Rails.backtrace_cleaner.remove_silencers!
Rails.backtrace_cleaner.add_silencer { |line| line !~ /^\/(app|config|lib|test|components)/}

If you are interested here is an issue I posted on the rails project about how to replicate this in detail. https://github.com/rails/rails/issues/22265

Alter Rails params hash from Rack middleware

The docs for Rack::Request#params say:

Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.

When you use the line

request.params['portal_id'] = portal_id

you add the new parameter to the hash created for that instance of Rack::Request, but the env that is passed on to rails isn’t modified. To make the new value available further down the Rack stack use update_param as the docs suggest:

request.update_param('portal_id', portal_id)

NewRelic::Rack middleware not exposing Sinatra::Base settings

I was able to work around this by updating my middleware

    def initialize app, &block
@app = app
@block = block
end

def call env
@block.call(env)
do_something(env[:my_middleware_option])
...
@app.call(env)
end

And initializing my middleware with

    use MyMiddleware do |env|
env[:my_middleware_option] = "any data type"
end


Related Topics



Leave a reply



Submit