My personal bin to put stuff around, it will definitely change… or not? who knows
request.format
is not your friend
How many times you wanted to have your endpoint exclusively receive requests in JSON format?
I bet probably wrote:
head :unprocessable_entity unless request.format.json?
and thought: My job here is done!. Well not so fast…
What you probably meant is:
If the request is not
application/json
, then reject it with422 Unprocessable Entity
.
Like any human being who wants to use HTTP codes appropriately. So you do the rational thing: put it to work
# I'm exaggerating my curl profficiency just to prove a point
curl -sSL -w "%{http_code}" -o/dev/null localhost:3000/your-controller/path?format=json
=> 200 # expected, params[:format] == :json
curl -sSL -w "%{http_code}" -o/dev/null -H 'Content-Type: application/json' localhost:3000/your-controller/path
=> 422 # WTF?
What the actual? Isn’t that a JSON??? Well…
It all boils down to that tricky request.format
bit.
# actionpack/lib/action_dispatch/http/mime_negotiation.rb
# [...]
def format(_view_path = nil)
formats.first || Mime::NullType.instance
end
def formats
fetch_header("action_dispatch.request.formats") do |k|
v = if params_readable?
Array(Mime[parameters[:format]]) # Writer's Note: this means query-string or request body has: format=json
elsif use_accept_header && valid_accept_header # Writer's Note: this means: Has "Accept: application/json" ?
accepts.dup
elsif extension_format = format_from_path_extension
[extension_format] # Writer's Note: this means "request uri ends_with '.json'?"
elsif xhr?
[Mime[:js]]
else
[Mime[:html]]
end
v.select! do |format|
format.symbol || format.ref == "*/*"
end
set_header k, v
end
end
# [...]
TL;DR: what you told rails was:
If the request does not [either of the following]
- have
format=json
in either query string or parseable request body.- have an
Accept
header that is recognized as json:application/json
,application/jsonrequest
,application/problem+json
- have a path ending
.json
extension Reject it.
And that’s not quite equivalent… You see, from MDN Docs on HTTP Accept
Header:
The Accept request HTTP header indicates which content types, expressed as MIME types, the client is able to understand. […]
The Accept
header is used strictly from the client perspective to say: “Please gimme a %content-type%, that’s what I can read”.
Side note: It’s fun to me too that to check the format the body _is parsed 😂._
In any case what you probably meant to write was:
head :unprocessable_entity unless request.content_mime_type.json?
Which really says: “Reject the request if the client did not provide a json”
This _“new friend” is defined a couple of lines above our mischievous pal ActionDispatch::Request#format
def content_mime_type
fetch_header("action_dispatch.request.content_type") do |k|
v = if get_header("CONTENT_TYPE") =~ /^([^,;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
set_header k, v
rescue ::Mime::Type::InvalidMimeType => e
raise InvalidType, e.message
end
end
This one instead tries to lookup the value of the Content-Type
header, which according to MDN Docs on HTTP Content-Type
:
The Content-Type representation header is used to indicate the original media type of the resource (prior to any content encoding applied for sending). […] In requests, (such as POST or PUT), the client tells the server what type of data is actually sent.
Lesson learned: request.format
is for Response format negotiation (What to
send to the client), DO NOT ATTEMPT TO USE IT TO DETERMINE IF A CLIENT SENT YOU
A JSON, IT WILL FAIL MISERABLY AND YOU’LL JUST PUT A .json
AT THE END AND RUIN YOUR SHINY BEAUTIFUL SMART ENDPOINT