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:
- =(k, v)
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.
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
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.
MonitorMixin is a standard library Mixin that provides methods for negotiating mutexes.
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
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.
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
mon_initialize and sets up a Monitor to manipulate later with
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
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
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.
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."
await_commit are used in
sending! are used in
Header class, and
await_sent appears to be entirely unused.
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
parsed_content_type_header creates a struct to store
charset. If both of those things aren't true, it creates a
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.
body is already a
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
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.
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
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.
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.