What is Rack middleware in Ruby? I couldn't find any good explanation for what they mean by "middleware".
相关问题
- Angular RxJS mergeMap types
- Question marks after images and js/css files in ra
- Using :remote => true with hover event
- Google Apps Script: testing doPost() with cURL
- How to instantiate Http service in main.ts manuall
相关文章
- C#使用http访问网络,有办法用指定网卡访问网络嘛?
- Ruby using wrong version of openssl
- Right way to deploy Rails + Puma + Postgres app to
- AWS S3 in rails - how to set the s3_signature_vers
- Difference between Thread#run and Thread#wakeup?
- how to call a active record named scope with a str
- How to add a JSON column in MySQL with Rails 5 Mig
- “No explicit conversion of Symbol into String” for
Rack middleware is a way to filter a request and response coming into your application. A middleware component sits between the client and the server, processing inbound requests and outbound responses, but it's more than interface that can be used to talk to web server. It’s used to group and order modules, which are usually Ruby classes, and specify dependency between them. Rack middleware module must only: – have constructor that takes next application in stack as parameter – respond to “call” method, that takes environment hash as a parameter. Returning value from this call is an array of: status code, environment hash and response body.
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?
Authorisation: "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.
What is Rack?
Rack provides a minimal interface between between webservers supporting Ruby and Ruby frameworks.
Using Rack you can write a Rack Application.
Rack will pass the Environment hash (a Hash, contained inside a HTTP request from a client, consisting of CGI-like headers) to your Rack Application which can use things contained in this hash to do whatever it wants.
What is a Rack Application?
To use Rack, you must provide an 'app' - an object that responds to the
#call
method with the Environment Hash as a parameter (typically defined asenv
).#call
must return an Array of exactly three values:each
).You can write a Rack Application that returns such an array - this will be sent back to your client, by Rack, inside a Response (this will actually be an instance of the Class
Rack::Response
[click to go to docs]).A Very Simple Rack Application:
gem install rack
config.ru
file - Rack knows to look for this.We will create a tiny Rack Application that returns a Response (an instance of
Rack::Response
) who's Response Body is an array that contains a String:"Hello, World!"
.We will fire up a local server using the command
rackup
.When visiting the relevant port in our browser we will see "Hello, World!" rendered in the viewport.
Fire up a local server with
rackup
and visit localhost:9292 and you should see 'Hello, World!' rendered.This is not a comprehensive explanation, but essentially what happens here is that the Client (the browser) sends a HTTP Request to Rack, via your local server, and Rack instantiates
MessageApp
and runscall
, passing in the Environment Hash as a parameter into the method (theenv
argument).Rack takes the return value (the array) and uses it to create an instance of
Rack::Response
and sends that back to the Client. The browser uses magic to print 'Hello, World!' to the screen.Incidentally, if you want to see what the environment hash looks like, just put
puts env
underneathdef call(env)
.Minimal as it is, what you have written here is a Rack application!
Making a Rack Application interact with the Incoming Environment hash
In our little Rack app, we can interact with the
env
hash (see here for more about the Environment hash).We will implement the ability for the user to input their own query string into the URL, hence, that string will be present in the HTTP request, encapsulated as a value in one of the key/value pairs of the Environment hash.
Our Rack app will access that query string from the Environment hash and send that back to the client (our browser, in this case) via the Body in the Response.
From the Rack docs on the Environment Hash: "QUERY_STRING: The portion of the request URL that follows the ?, if any. May be empty, but is always required!"
Now,
rackup
and visitlocalhost:9292?hello
(?hello
being the query string) and you should see 'hello' rendered in the viewport.Rack Middleware
We will:
MessageSetter
,env
,MessageSetter
will insert a'MESSAGE'
key into the env hash, its value being'Hello, World!'
ifenv['QUERY_STRING']
is empty;env['QUERY_STRING']
if not,@app.call(env)
-@app
being the next app in the 'Stack':MessageApp
.First, the 'long-hand' version:
From the Rack::Builder docs we see that
Rack::Builder
implements a small DSL to iteratively construct Rack applications. This basically means that you can build a 'Stack' consisting of one or more Middlewares and a 'bottom level' application to dispatch to. All requests going through to your bottom-level application will be first processed by your Middleware(s).#use
specifies middleware to use in a stack. It takes the middleware as an argument.Rack Middleware must:
call
method that takes the Environment hash as a parameter.In our case, the 'Middleware' is
MessageSetter
, the 'constructor' is MessageSetter'sinitialize
method, the 'next application' in the stack isMessageApp
.So here, because of what
Rack::Builder
does under the hood, theapp
argument ofMessageSetter
'sinitialize
method isMessageApp
.(get your head around the above before moving on)
Therefore, each piece of Middleware essentially 'passes down' the existing Environment hash to the next application in the chain - so you have the opportunity to mutate that environment hash within the Middleware before passing it on to the next application in the stack.
#run
takes an argument that is an object that responds to#call
and returns a Rack Response (an instance ofRack::Response
).Conclusions
Using
Rack::Builder
you can construct chains of Middlewares and any request to your application will be processed by each Middleware in turn before finally being processed by the final piece in the stack (in our case,MessageApp
). This is extremely useful because it separates-out different stages of processing requests. In terms of 'separation of concerns', it couldn't be much cleaner!You can construct a 'request pipeline' consisting of several Middlewares that deal with things such as:
(above bullet points from another answer on this thread)
You will often see this in professional Sinatra applications. Sinatra uses Rack! See here for the definition of what Sinatra IS!
As a final note, our
config.ru
can be written in a short-hand style, producing exactly the same functionality (and this is what you'll typically see):And to show more explicitly what
MessageApp
is doing, here is its 'long-hand' version that explicitly shows that#call
is creating a new instance ofRack::Response
, with the required three arguments.Useful links
First of all, Rack is exactly two things:
Rack - The Webserver Interface
The very basics of rack is a simple convention. Every rack compliant webserver will always call a call method on an object you give him and serve the result of that method. Rack specifies exactly how this call method has to look like, and what it has to return. That's rack.
Let's give it a simple try. I'll use WEBrick as rack compliant webserver, but any of them will do. Let's create a simple web application that returns a JSON string. For this we'll create a file called config.ru. The config.ru will automatically be called by the rack gem's command rackup which will simply run the contents of the config.ru in a rack-compliant webserver. So let's add the following to the config.ru file:
As the convention specifies our server has a method called call that accepts an environment hash and returns an array with the form [status, headers, body] for the webserver to serve. Let's try it out by simply calling rackup. A default rack compliant server, maybe WEBrick or Mongrel will start and immediately wait for requests to serve.
Let's test our new JSON server by either curling or visiting the url
http://localhost:9292/hello.json
and voila:It works. Great! That's the basis for every web framework, be it Rails or Sinatra. At some point they implement a call method, work through all the framework code, and finally return a response in the typical [status, headers, body] form.
In Ruby on Rails for example the rack requests hits the
ActionDispatch::Routing.Mapper
class which looks like this:So basically Rails checks, dependent on the env hash if any route matches. If so it passes the env hash on to the application to compute the response, otherwise it immediately responds with a 404. So any webserver that is is compliant with the rack interface convention, is able to serve a fully blown Rails application.
Middleware
Rack also supports the creation of middleware layers. They basically intercept a request, do something with it and pass it on. This is very useful for versatile tasks.
Let's say we want to add logging to our JSON server that also measures how long a request takes. We can simply create a middleware logger that does exactly this:
When it gets created, it saves itself a copy of the actual rack application. In our case that's an instance of our JSONServer. Rack automatically calls the call method on the middleware and expects back a
[status, headers, body]
array, just like our JSONServer returns.So in this middleware, the start point is taken, then the actual call to the JSONServer is made with
@app.call(env)
, then the logger outputs the logging entry and finally returns the response as[@status, @headers, @body]
.To make our little rackup.ru use this middleware, add a use RackLogger to it like this:
Restart the server and voila, it outputs a log on every request. Rack allows you to add multiple middlewares that are called in the order they are added. It's just a great way to add functionality without changing the core of the rack application.
Rack - The Gem
Although rack - first of all - is a convention it also is a gem that provides great functionality. One of them we already used for our JSON server, the rackup command. But there's more! The rack gem provides little applications for lots of use cases, like serving static files or even whole directories. Let's see how we serve a simple file, for example a very basic HTML file located at htmls/index.html:
We maybe want to serve this file from the website root, so let's add the following to our config.ru:
If we visit
http://localhost:9292
we see our html file perfectly rendered. That's was easy, right?Let's add a whole directory of javascript files by creating some javascript files under /javascripts and adding the following to the config.ru:
Restart the server and visit
http://localhost:9292/javascript
and you'll see a list of all javascript files you can include now straight from anywhere.I had a problem understanding Rack myself for a good amount of time. I only fully understood it after working on making this miniature Ruby web server myself. I've shared my learnings about Rack (in the form of a story) here on my blog: http://gauravchande.com/what-is-rack-in-ruby-rails
Feedback is more than welcome.
config.ru
minimal runnable exampleRun
rackup
and visitlocalhost:9292
. The output is:So it is clear that the
Middleware
wraps and calls the main app. Therefore it is able to pre-process the request, and post-process the response in any way.As explained at: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack , Rails uses Rack middlewares for a lot of it's functionality, and you can add you own too with
config.middleware.use
family methods.The advantage of implementing functionality in a middleware is that you can reuse it on any Rack framework, thus all major Ruby ones, and not just Rails.