A close read of the ActionDispatch::Response class
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.
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.
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."
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.
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
.
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.
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.
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.
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
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.
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 create
d, 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.
A few header helpers.
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.
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.
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.
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.)
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.
These are basically all access helpers, to present clearer or more compatible names for writing and reading response parameters.
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.
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.
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.