A close read of the ActionDispatch::Response class

A close read of the ActionDispatch::Response class
Photo by Kristina Tripkovic / Unsplash

This is a line-by-line investigation of the ActionDispatch::Response class, focused on understanding why each line is the way that it is – where (and when) did it come from, and what behaviors are any weird or complex lines enabling?

If you came here trying to solve a problem related to this class, and you still don't have the answer after you read through the article, e-mail me at nat at this website. I'd love to have a chance to help you out.

Much of the complexity in this class comes from handling the Live Streaming feature, so we're going to see Aaron Patterson's Github handle all over key parts of this code. Most of the information you're otherwise likely to ask it for otherwise comes out of the Rack::Response::Helpers module, so if you're trying to get a handle on what you can get and set out of a Response, start there. The other main thing this specific class does is handle header access, especially for the Content-Type header that impacts details of how it might get served.

Finally, since from the framework's perspective this is all implementation details for Controllers, reviewing the controller tests is a good way to get a handle on how it's supposed to behave.

I'll be referencing the version of the response.rb file committed on July 29 by Rafael França. I'll include line numbers and links, but if you're doing a deep read along with me, you'll want to get Github open in another browser and compare side-by-side.

Lines 1-7

The file starts the way all great Rails classes start: a generic magic comment that's present in almost all files, and a few require statements. We'll come back to these when we encounter the code that uses them. For now, we'll note that we've required an ActiveSupport utility class, a couple of other classes from the http section of ActionDispatch, and Ruby's standard library "monitor" class.

Lines 8-36

The module's comment tells us that if we're ever accessing a Response outside of a test, we're probably doing something wrong. "Controllers should use the methods defined in ActionController::Base instead."

Lines 37-60

Our first real bit of code is the Header class, nested inside of the Response class itself. It inherits from DelegateClass(Hash) and has four methods:

  • initialize
  • []=(k, v)
  • merge(other)
  • to_hash

The DelegateClass business is a "prefer composition over inheritance" thing that I'm not going to pretend to understand. It's apparently good practice to avoid subclassing Ruby core classes, because their methods are written in C and don't always reference other methods that you would expect them to reference. Many of the examples in the linked article that Steve Kablink describes as surprising don't surprise me, so I don't trust my understanding of why, exactly, or how delegating to universally the way DelegateClass appears to would solve the problems he's describing.

Still, DelegateClass(Hash) tells me that a Header is structured very much like a Hash, but with some extra behavior. The main extra behavior is in that []=(k, v) method. If we try to set a header after the response has already been sent, the call will fail with an exception that tells us the response has been sent. It also overrides the initialize method in order to associate a response with the header, and it redefines merge so that our @response stays associated with the new header hash. It also allows a call to split off just the headers from the response as a hash.

Lines 61-70

The main interesting thing in this block of accessors is that delegate assignment. While Header is its own separate object, we access the headers themselves directly by calling [] on Response.

Lines 81-88

Here's where that ActiveSupport require statement comes into play. cattr_accessor is defined there. cattr_accessor makes class attributes get-able and set-able just as attr_accessor does for instance attributes.

Lines 89-100

content_type at one point returned just the media type, but now returns the entire Content-Type header, including optional parameters, and can no longer be configured to return only the media type. Now, if you need just the media type, you call Response.media_type. This change was made to better match user expectations.

The sprinkle of deprecation warnings here is evidence of a series of bug fixes that unearthed implicit requirements.

Lines 101-109

Here we're pulling in a bunch of behavior that's defined in other files. We get a bunch of basic behavior from Rack::Response::Helpers – status code definitions and wrappers around header manipulation primitives like "set_cookie."

ActionDispatch::HTTP::FilterRedirect contributes the filtered_location method, and was added to allow filtering redirects from logs – things like private paths to s3 buckets.

ActionDispatch::Http::Cache::Response was extracted from the main Response class in 2010. It handles the Rails implementation of conditional HTTP Caching with ETags, a mechanism that lets a client quickly check whether a resource has changed since the last time it requested it from the server, before the server performs a full response for that resource, by comparing digests of that resource.

The MonitorMixin is a standard library Mixin that provides methods for negotiating mutexes. Response uses new_cond and synchronize, which we'll see a little further down, when we get to the actual response sending code.

Lines 110-160

Buffer allows the Response object to provide its body as a streamable object, and was added as part of the Live Streaming feature introduced in Rails 4.0.

The stream itself is made accessible a little further down.

    # The underlying body, as a streamable object.
    attr_reader :stream

I don't think there's any particular reason the next two methods separate the Buffer from attr_reader :stream

Lines 162-170

self.create and self.merge_default_headers exist to allow the objects created by ActionDispatch::Response#new to be more generic, in case a Controllers needs to create a Response without the default headers for some reason. In most cases, Controllers will call ActionDispatch::Response.create, and get the default headers included.

Note that default_headers are defined by config, so the standard values are defined over in ActionDispatch::Railtie. They're mainly security defaults to stop cross-site scripting.

Line 174-190

That super() call is a little bit tricky. ActionDispatch::Response's superclass is Object, but super() doesn't call the method with that name from the superclass, it calls the method with that name from the first class in the ancestor chain that has it. include MonitorMixin inserts MonitorMixin at the front of the ancestor chain, so that super() call is how Response calls mon_initialize and sets up a Monitor to manipulate later with synchronize.

prepare_cache_control! sets up the initial cache_control Hash, presumably by looking up default values in a config. This is one of the places where my "close read" gets a little bit fuzzy. When ActionDispatch::Response.create is called in isolation, cache_control is empty, but in the context of a proper Rails response it contains default max_age, must_revalidate, and private values. This is controlled at least in part by Rack::Etag middleware, but I'm not sure whether that happens before or after the Response is created, so I can't say for sure what this code typically does in practice.

yield self if block_given? is a line of code that dates back to 2007, in the Rack::Response code that ActionDispatch::Response once descended from, in yon olden days.

It's here to enable "Pretty Object Initialization." I'll be honest – I don't entirely understand what coders mean by "pretty" most of the time, but here it means that instead of writing something like...

res = Response.new
res.status = 300
res.body = [ some_body ]

... you can wrap all that after-the-initialization tweaking in a block.

res = Response.new do |r|
  r.status = 300
  r.body = [ some_body ]
end

So the "pretty" part means "it's clear that the next few lines are still part of the initialization sequence.

Note though that create doesn't have the equivalent code, and that's how you should expect Response objects to get created most of the time. This might be because can call any object this way with the tap method, so there's no need to add a similar line to your own initializers even if you want to be able to use blocks for initialization like that.

Lines 191-195

A few header helpers.

Lines 196-232

Here's most of where we're using that MonitorMixin capability. This is more code to enable tenderlove's live stream capability, so if you're not mixing ActionController::Live into your controller, you're probably not using it.

@cv here is a "condition variable." A "condition variable" is a mutex that can temporarily release its hold on a resource, and then re-capture it when a condition is met. synchronize ensures that only one block operates on a particular resource at a time. Together, this allows a Live controller to "return the response code and headers back up the Rack stack, and still process the body in parallel with sending data to the client." commit! and await_commit are used in ActionController::Live, sent! and sending! are used in Response's Header class, and await_sent appears to be entirely unused.

Lines 238-254

The Rack::Utils.status_code call inside status converts status codes passed into it to integers, and ensures that status codes passed as symbols are in fact valid HTTP status codes. (It throws an error if they aren't.)

Most of the code in content_type= exists to ensure that if the new content_type passed in is missing or invalid, the header still gets set to a sensible value – either the existing value, or a default.

Lines 255-259

super in this case calls Rack::Response::Helpers#content_type to get the value of the content type header. I'm not sure why the presence call is necessary – removing it doesn't change what's returned when a Response doesn't yet have a Content-Type header.

Lines 260-264

If there's a content type parameter, and it's parseable into a mime/media type and charset by the CONTENT_TYPE_PARSER regex, parsed_content_type_header creates a struct to store mime_type and charset. If both of those things aren't true, it creates a NullContentTypeHeader. So media_type either returns the mime_type, or nil. (Media and mime type are, for our purposes here, the same. Mime type is the older term.)

Lines 271-291

This handles the other part of the Content-Type header– setting the charset. The main thing that's weird here is sending_file=(v) – it's used in just one place, in the DataStreaming module, but it used to be an instance variable and used to make decisions about whether to set charset in a bunch of places. That's since been refactored away, leaving just the vestigial public use.

Lines 292-324

These are basically all access helpers, to present clearer or more compatible names for writing and reading response parameters.

Lines 325-393

More code for handling streaming bodies – specifically, streaming bodies from files, and making sure that body still presents a consistent interface.

If the body is already a FileBody, body= just sets it as the @stream. Otherwise it sets up a Live::Buffer containing whatever's been passed into it, and makes sure it's had an opportunity to do so before any other thread tries to work on @stream with synchronize.

alias_method :redirect_url, :location is a test helper that somehow ended up in the middle of what's otherwise unrelated code.

Lines 394-403

rack_response and the RackBody it creates are defined down in private methods.

Once again, this is a method that appears to mainly be used in rails/metal.rb. ( prepare! also shows up in controller test setup). The to_a name appears to be an old Rack convention.

Lines 404-420

Finally, a method for returning the contents of everyone's favorite header as a key-value map, rather than a semi-colon delimitated string. The main thing that's interesting here is that the string splitting happens every time, there's no memoization of the cookies map it produces, so keep that in mind if you ever somehow find yourself writing performance-intensive cookie processing code.

We've mostly covered the private methods in the methods that call them. At the very end of the file you'll see the line that runs whatever lazy-load hooks your plugins or libraries have attached to Response.


Jobs

Roivent is hiring SREs. They have a bunch of ex-Pivots on the team.

It's a bit silly I didn't notice this until now, but Wildbit manages a job board called People First Jobs. It notes which criteria for inclusion a given job meets, with an emphasis on things like remote work, sensible hours, and flexible schedules.

Media

I'm reading The Body Keeps the Score, a book about trauma that is a strong contender for "best book I've ever read." Top five, easily. Lays out the physiology of the human stress response(s), what happens when they're thwarted or overwhelmed, and how they can be treated. I've gotten a bunch of good ideas, including a better understanding of both why pairing was both healing and damaging for me, and why it seems to have a dramatic impact on some people and very little on others.

I especially recommend it if you've ever thought you ought to be in therapy but haven't liked or gotten value out of any therapist you've talked to. (A category I belonged to until a few years ago.) The book gets reasonable technical about therapy and has some information in it about the way that therapists are trained, so you'll get a clearer idea of what your options are and what to ask about.