-
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
Acceptheader.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 endWith a corresponding route:
resources :posts, only: :showFetching
/posts/1in your browser will try to render an HTML response by default. You can specify the requestformatby using an explicit file extension in the URL:/posts/1.htmlwill return the HTML version and/posts/1.jsonwill return JSON.But what if you can’t read the
formatout 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/htmlorAccept: application/jsonin 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/htmlrequest header, you might try setting a defaultformatin your route, e.g.resources :posts, only: :show, id: /.+/, defaults: { format: :json }This means
/posts/1now returns JSON by default but requesting/posts/1withAccept: text/htmlreturns the JSON version even though we asked for HTML.This is because Rails’ notion of a request’s
formattakes precedence over anything specified in theAcceptheader (see also a discussion between James Higgs and Simon Coffey about this issue). By setting a defaultformatof:json, this will always be used instead of anything else in the request.There is another approach: don’t use a default
formatbut 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 endWhen we now request
/posts/1with the defaultAccept: */*header (as set by curl and httpie), we’ll get JSON by default instead of HTML. SpecifyingAccept: text/htmlwill now return the HTML version as expected.However, what if someone doesn’t specify an
Acceptheader 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 anXMLHttpRequestin 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 defaultformatif noAcceptheader 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? endNow we’ll serve JSON when someone requests
/posts/1with the defaultAccept: */*header,Accept: application/jsonand when there is noAcceptheader 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.selectorIO#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 endAs 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