-
I’ve been working on an API written using Ruby on Rails and was attempting to understand how Rails handles content types when sending the HTTP
Accept
header.Given a controller action like so:
def show @post = Post.find(params[:id]) respond_to do |format| format.html format.json { render json: @post } end end
With a corresponding route:
resources :posts, only: :show
Fetching
/posts/1
in your browser will try to render an HTML response by default. You can specify the requestformat
by using an explicit file extension in the URL:/posts/1.html
will return the HTML version and/posts/1.json
will return JSON.But what if you can’t read the
format
out of the URL, e.g. if you accept dots in the post ID?resources :posts, only: :show, id: /.+/
Another way to request a content type is to send
Accept: text/html
orAccept: application/json
in your request which will return the HTML and JSON versions respectively.If you want the JSON representation to be the default and for the HTML version to only be returned if someone explicitly requests it using an
Accept: text/html
request header, you might try setting a defaultformat
in your route, e.g.resources :posts, only: :show, id: /.+/, defaults: { format: :json }
This means
/posts/1
now returns JSON by default but requesting/posts/1
withAccept: text/html
returns the JSON version even though we asked for HTML.This is because Rails’ notion of a request’s
format
takes precedence over anything specified in theAccept
header (see also a discussion between James Higgs and Simon Coffey about this issue). By setting a defaultformat
of:json
, this will always be used instead of anything else in the request.There is another approach: don’t use a default
format
but instead rely on a little documented feature of Rails’respond_to
: put the default response first.If we change our controller to the following:
def show @post = Post.find(params[:id]) respond_to do |format| format.json { render json: @post } format.html end end
When we now request
/posts/1
with the defaultAccept: */*
header (as set by curl and httpie), we’ll get JSON by default instead of HTML. SpecifyingAccept: text/html
will now return the HTML version as expected.However, what if someone doesn’t specify an
Accept
header at all (e.g. an HTTP client on an embedded device)? In that case, Rails will use a default content type of HTML (unless it is anXMLHttpRequest
in which case it will use JavaScript) which isn’t what we expect.To work around this, we can use Rails’
request.format=
API to explicitly set our defaultformat
if noAccept
header is present:before_action :set_default_format_to_json def show @post = Post.find(params[:id]) respond_to do |format| format.json { render json: @post } format.html end end private def set_default_format_to_json request.format = :json if request.headers["Accept"].blank? end
Now we’ll serve JSON when someone requests
/posts/1
with the defaultAccept: */*
header,Accept: application/json
and when there is noAccept
header at all. -
We needed to improve detection of clients disconnecting from a streaming endpoint using Rack’s Hijack API. As this is a difficult problem, I turned to Jesse Storimer’s “Working With TCP Sockets”.
When a client disconnects cleanly, their socket becomes
eof?
which means it becomes ready to read (e.g. withIO.select
orIO#wait_readable
). Following Action Cable’s event loop’s lead, we implemented a system using New I/O for Ruby:# Create a single selector. selector = NIO::Selector.new # Register each rack.hijack socket for reading. monitor = selector.register(io, :r) # Associate a connection helper object with the monitor # to handle cleaning up after client disconnect. monitor.value = connection # Run an infinite loop in a separate thread, checking # for when the rack.hijack socket becomes readable which, # in our case, *must* mean the client has disconnected. # # As we're using the blocking form of select, in reality we # use selector.wakeup to periodically interrupt the thread # (e.g. when registering and deregistering sockets). Thread.new do loop do selector.select do |monitor connection = monitor.value connection.close end end end
As this won’t catch all client disconnects, we also attempt to write a keepalive message to the socket every 3 seconds and, if anything goes wrong, clean things up.
-
The announcement of the Apple Studio Display and Jason Snell’s declaration of “I am not interested in having a stand on my desk” led me to order Fully’s Jarvis Monitor Arm but no new display (yet).
-
After a three week hiatus due to a combination of illness and hosting guests, I pulled on my running tights and managed to run along the river this morning.
-
When particularly ill, I sat on the sofa and watched several action films, embracing the likelihood they would be terrible guilty pleasures: Wonder Woman 1984, Nobody and The King’s Man. Following that bit of escapism, I watched all of Invincible which was genuinely good (if you can stomach incredible violence).
-
I baked bread for the first time in a while.
Weeknotes #90
By Paul Mucur,
on