Sprockets Dissected: Rack and Middleware

Share this article

Sprockets Dissected: Rack and Middleware
sprocketsmag

In a previous article I explain how asset tags are inserted into HTML by Rails and Sprockets. Inserting asset tags into the HTML is the first step towards serving assets. In this post, I continue the journey, answering the remaining questions:

  • What is /assets and it’s purpose?
  • What is ActionDispatch::Static middleware used for?

Key Takeaways

  • The /assets path in Rails applications using Sprockets is not just a prefix for assets, but a rack endpoint. Sprockets prepends every asset with /assets, causing each browser request for those assets to send calls to http(s)://your_domain/assets/asset_name.
  • Building a rack endpoint in a Rails application involves following strict guidelines, including that it must respond to the call method, and it must return an array with three pieces of information: the HTTP status code, the response headers, and the response itself.
  • Sprockets uses a rack endpoint at /assets to ensure that /assets takes priority over other rack endpoints. It does this by prepending /assets to the Rails routes, as rack matches every request in the first matching route definition.
  • ActionDispatch::Static is a middleware in the default Rails middleware stack that serves static assets, which are assets that don’t require any preprocessing before serving them, like images and fonts. In the development environment, all assets are handled by Sprockets, whereas in production, static assets in the public directory exist and are served by ActionDispatch::Static.

What is /assets?

By default, Rails applications using Sprockets prepend /assets on every asset’s path. /assets is not, however, only a prefix for assets; it is also a rack endpoint.

Basically, when generating the response, Sprockets prepends every asset with /assets, causing each browser requests for those assets to send calls to http(s)://your_domain/assets/asset_name. /assets/asset_name is the name of the asset we want browser to request.

Whenever a call is made to /assets/asset_name, the Rails application needs to respond by serving that asset. As I mentioned, /assets is a rack endpoint, so is it handled by our Rails routes? Every endpoint or path in a Rails application is defined in config/routes.rb, but when you view its content, you will not find any route for /assets. Yet, our assets are getting served. Let’s see how.

Building a rack endpoint in any existing Rails application is quite easy. There are some strict guidelines that every rack endpoint must follow and these are:

  • It must respond to the call method
  • It must return an array with three pieces of information: the HTTP status code, the response headers, and the response itself

For example, place the following code in our sample Rails application (created in first article) config/routes.rb file:

mount Proc.new {|env| [200, {'Content-Type' => 'text/html'}, ["SitePoint rocks!"]]} => "/sitepoint"

mount defines /sitepoint as a rack endpoint. Proc.new will return a Ruby block, which responds to the call method. The block returns an array containing status code, status headers, and the response.

Run your Rails application (rails server) and navigate to http://localhost:3000/sitepoint. You will see SitePoint rocks! displayed in browser. That was a quick and dirty introduction to rack endpoints in Rails. Let’s get back to Sprockets and see how Sprockets pulls this off.

Sprockets uses the following code to mount a rack endpoint at /assets which is defined here:

app.routes.prepend do
  mount app.assets => config.assets.prefix
end

app.assets is Rails.application.assets which is an instance of Sprockets::Environment, discussed in the previous article. config.assets.prefix is /assets. Sprockets is prepending /assets to the Rails routes because rack will match every requests in the first matching route definition. So, by prepending, Sprockets is making sure that /assets takes priority over other rack endpoints.

app.assets must respond to call in order to be processed as a rack endpoint. Looking at the code for Sprockets::Environment in sprockets/lib/sprockets/environment.rb, call is inherited from Sprockets::Base. Exploring sprockets/lib/sprockets/base.rb further, it is including some modules, one of which is Sprockets::Server. Code spelunking into Sprockets::Server and sprockets/lib/sprockets/server.rb, there is a call method defined. Therefore, app.assets is getting a call method from Sprockets::Server, which is called each time there is request to /assets. This call method returns the appropriate asset.

find_asset, defined at sprockets/lib/sprockets/server.rb:42, returns an instance of Sprockets::BundledAsset, provided it is JavaScript or CSS. If it is an image, a Sprockets::StaticAsset is returned. When the browser calls /assets/application.js, then find_asset will return an instance of Sprockets::ProcessedAsset.

At line:62 there is call to ok_response. ok_response is defined on line:187 and returns an array per the rack specification, by placing the asset into the response. This array is returned from ok_response, which is returned from call.

We have seen that an asset instance is presented as the response for /assets/application.js. But is this sufficient to send back to the client? The answer is “it depends”.

According to rack, the response being sent from a rack endpoint must respond to each. Both Sprockets::BundledAsset and Sprockets::StaticAsset inherit from the Sprockets::Asset class. sprockets/lib/sprockets/asset.rb:82 defines each, which means our asset instance responds to each and is rack compatible.

In the each method, to_s is passed as argument to the block. At line:55 there is a method to_s which returns the source of the asset. Each asset instance has its source in a source instance variable which is used to get source code for the corresponding asset. We are doing same thing in the to_s method, fetching the source code and returning it to the appropriate method call. The source code of the requested asset is then transferred to the HTTP client by our Rails application.

This is how Sprockets serves our assets. The above procedure is the same for every type of asset call, JavaScript, stylesheet, image, font, etc.

What is ActionDispatch::Static?

ActionDispatch::Static is a middleware in the default Rails middleware stack. Its primarily purpose is to serve static assets. Static assets are assets which don’t require any preprocessing before serving them, like images and fonts. JavaScript and stylesheet files are not considered to be static assets because there is some preprocessing involved before serving them up. When we execute rake assets:precompile, however, those assets become static assets because they are preprocessed and saved as files so they can be served just like images. Let’s see how this middleware works.

ActionDispatch::Static middleware is configured and added to the middleware stack here:

middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control

ActionDispatch::Static is defined in actionpack/lib/action_dispatch/middleware/static.rb:47. There are three parameters in its constructor: app, path, and cache_control. Providing the app parameter is the responsibility of rack. Rails is passing paths["public"].first for the second argument. paths["public"].first is the absolute path of the public directory in the Rails application. If we have enabled config.serve_static_assets in the relevant environment file under config/environments, then ActionDispatch::Static is the first middleware in the list when rake middleware is executed. It will serve as the first execution point for our Rails application.

Looking at actionpack/lib/action_dispatch/middleware/static.rb:53 there is a call method, which is used by rack. In this method, at line:54, there is a case statement on the HTTP method being used. If the request’s HTTP method is GET or HEAD, this middleware is evaluating the request path to see if it is a static asset that needs to be served. line:50 of the constructor for ActionDispatch::Static shows the following:

@file_handler = FileHandler.new(path, cache_control)

The FileHandler class is defined in same file, just above ActionDispatch::Static. At line:57, there is a call to match? on this @file_handler instance. At line:12, you can see the definition of match?. This method will evaluate the provided path to see whether it is a static asset. If this method returns true, ActionDispatch::Static will serve the asset and control will not be passed to other middlewares in the stack. If match? returns false, control will be passed to subsequent middleware.

line:15 of match? concatenates the provided path with @root. @root is ithe absolute path of hte public directory of our Rails application.

For example, a request for /assets/application.js is concatenated with the public directory path to become /Users/imran/work/rails_asset_serve/public/assets/application.js (your’s will be different). The code on line:19 tries to determine whether this file exists. If it does, then this file will be handled by ActionDispatch::Static, otherwise it will be handled by Sprockets.

In the development environment there are no assets in the public directory, so they are all handled by Sprockets. In production, however, static assets in the public directory exist because we usually compile assets by using rake assets:precompile. Here, match? returns true and assets are served by ActionDispatch::Static.

On line:59, a call is made to call on the @file_handler instance. call is defined on line:26 which is, in turn, calling call on @file_server, an instance of Rack::File.

Rack::File is defined in rack/lib/rack/file.rb. In the call method on line:30, dup is called which creates a shallow copy of the current instance. _call (defined on line:36) is called on that newly created copy, performing different checks and then calling serving on line:53.

In serving, on line:73, the array for the rack response is created, where self is presented as the response object. At the end of serving, this response array is returned.

I asked previously if self is enough to send a response to client? The answer is yes. Because self responds to each which is defined on line:104. In each, a file is opened for reading and read in 8KB chunks to the passed in block, which is transferred to the browser by our Rails application.

This is how ActionDispatch::Static serves static assets.

Conclusion

Rails is an incredible codebase written by the brightest minds in the Ruby community. By reading the source code, you not only improve your skills but you also get a chance to learn new things that you might not find elsewhere. For example, I discovered that, by using ActionDispatch::Static, we can serve portions of our assets using Range headers, which I was not aware of before.

I hope these articles helped you learn more about how Rails and Sprockets serve assets. Please let me know what you think!

Frequently Asked Questions (FAQs) about Rack Middleware and Sprockets

What is the primary function of Rack Middleware in Ruby on Rails?

Rack Middleware in Ruby on Rails is a component that sits between the web server and the web framework. It’s responsible for processing HTTP requests and responses. It can perform a variety of functions such as logging, setting up sessions, managing cookies, and handling errors. Middleware components are reusable and can be used in different applications or in different parts of the same application.

How does Sprockets work in conjunction with Rack Middleware?

Sprockets is a Ruby library that aids in the generation of asset files. It works with Rack Middleware to serve these assets in a Rails application. When a request for an asset is made, Sprockets compiles the asset if necessary and then the request is passed to Rack, which then serves the asset to the client.

How can I add a new middleware to my Rails application?

You can add a new middleware to your Rails application by using the config.middleware.use command in the application configuration file. You need to specify the class of the middleware and any arguments that should be passed to it. Once added, the middleware will be included in the processing of every request.

Can I remove or disable a middleware in Rails?

Yes, you can remove or disable a middleware in Rails. This can be done by using the config.middleware.delete command in the application configuration file. You need to specify the class of the middleware that you want to remove.

What is the order of execution of middleware in Rails?

The order of execution of middleware in Rails is determined by the order in which they are defined in the application configuration file. Middleware defined earlier in the file are executed before those defined later. However, the response is processed in the reverse order.

How can I debug middleware in Rails?

You can debug middleware in Rails by using the rake middleware command. This will display a list of all the middleware in your application, in the order they are executed. You can also add logging statements in your middleware code to track its execution.

How does Sprockets handle asset compilation?

Sprockets handles asset compilation by concatenating all the required files into a single file. It also minifies the assets by removing unnecessary characters, and can compile assets from a higher-level language like CoffeeScript or Sass to a lower-level language like JavaScript or CSS.

Can I use Sprockets with other Ruby frameworks?

Yes, Sprockets can be used with other Ruby frameworks. While it is commonly used with Rails, it can also be used with Sinatra or any other Rack-based framework.

How can I customize the behavior of Sprockets?

You can customize the behavior of Sprockets by creating a custom initializer. This allows you to set various configuration options, such as the paths that Sprockets should search for assets, the cache directory, and the compression level for assets.

What are some common issues that can occur with Rack Middleware and Sprockets?

Some common issues that can occur with Rack Middleware and Sprockets include conflicts between middleware, issues with the order of middleware execution, problems with asset compilation or serving, and performance issues. These can often be resolved by debugging the middleware stack, adjusting the middleware order, or tweaking the Sprockets configuration.

Imran LatifImran Latif
View Author

Imran Latif is a Web Developer and Ruby on Rails and JavaScript enthusiast from Pakistan. He is a passionate programmer and always keeps on learning new tools and technologies. During his free time he watches tech videos and reads tech articles to increase his knowledge.

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week