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
Does Concurrency Happen Even When Only One Thread Is in a Thread Pool
Ruby on Rails 4 - What Authentication Gem to Use
Actiondispatch::Http::Uploadedfile.Content_Type Not Being Initialized in Rspec Test
Error Installing Gem: Couldn't Reserve Space for Cygwin's Heap, Win32 Error 487
Differences Between Literals and Constructors? ([] VS Array.New and {} VS Hash.New)
Read Contents of a Local File into a Variable in Rails
Ruby - Array.Join Versus String Concatenation (Efficiency)
How to Color Unit Tests with Lib Minitest or Test:Unit
Machinist VS Factorygirl - Pros and Cons
Get Jekyll Configuration Inside Plugin
Marking an Unused Block Variable
How to Fix Undefined Method 'Split' for Nil:Nilclass Error
Differencebetween 'Size' and 'Length' Methods