HTTP Status Codes Are Not Enough
I spotted an article called Just learn Rails (Part 3) HTTP status codes. It started off good, and I liked that it was teaching people to avoid hardcoding their HTTP status codes in code, using :conflict
instead of 409
and the like.
That is a good message to send, which is why I wrote an article on exactly that last month.
The article also stresses that you must not return errors on 200
, which only the insanity wolf would do.
The problem with the article is when it gets to sarcastically pointing out error messages are an overly verbose waste of time, or needless novella. I pointed out the flaw in this logic using some jovially hyperbolic wording as I'm known to do, and in the end the chap suggested I didn't understand the article.
I understand the article perfectly, and it's a damaging message to send.
Instead, Ruby on Rails gives us very helpful symbols that can be used to accurately convey the appropriate response to end users.
To only return the correct HTTP status code, the books controller can be rewritten to:
class BooksController < ApplicationController
before_filter :authenticate_user
def show
if @user.nil?
return head :unauthorized
end
render json: Book.find(params[:id])
end
end
Fuck that! Am I unauthorized because I didn't provide a key or because the key was invalid? A developer can spend forever debugging
that alone.
Alternatively, if extremely verbose and tedious error messages are the cat's pajamas to you, it is possible to use these symbols in conjunction with response bodies:
class BooksController < ApplicationController
before_filter :authenticate_user
def show
if @user.nil?
response = { error: 'User is not logged in' }
status = :unauthorized
else
response = Book.find(params[:id])
status = :ok
end
render json: response, status: status
end
end
Error messages are not some over-the-top exercise, creating tedious reading and understanding for everyone involved. They are fundafuckingmental to having any sort of half useful API.
Brevity is underrated
This example has exposed the difference between a decent API and one that understands how HTTP should work. While the specific problem could be solved with CanCanCan, it is important to understand how and why those libraries work the way that they do.
Absolutely, a good API should definitely use status codes appropriately, but the suggestion throughout this article is that suplimenting that HTTP status messages are a pain, or uneccessary.
Bunk. Bunk I say!
Take a look at this:
{
"errors" : [{
"code" : 20002,
"title" : "There are no savings for this user.",
"status" : 400
}]
}
How the flying shit would I know from just a 400 that that user doesn't have any savings? Especially when that same endpoint could also return:
{
"errors" : [{
"code" : 20010,
"title" : "Invalid geopoints for simulated savings.",
"status" : 400
}]
}
The whole API does not rely on 400 errors over here, no, but even if you did find a more suitable status code I still would not know exactly what was happening.
If you make me work with an API that does not have errors, I'm not coming to your birthday party.
If one were to continue down the naive path, returning hashes or strings by default for all responses, things would become messy quickly. That response structure unjustly handcuffs the API clients to be unnecessarily tolerant of ad-hoc text responses. But, if the API conforms to HTTP standards, a client knows exactly what each response means. Nothing is left up to the imagination, and no bright new developer can accidentally change the error key to Error and ruin everyone's day.
Returning error messages is not in the HTTP specification, so best not to do it ever?
I'll concede that my jimmies do get a little rustled when I see people making their own random ad-hoc error formats, especially when error messages for APIs are a solved problem with some great solutions:
Hilariously that last one is an RFC, written largely the same chap responsible for most of the modern day HTTP specification, so, if we're into following specs with our APIs then we should probably be doing that.
Instead of capturing in text every single detail about why a request did not result in an expected response, use HTTP status codes and save the novella for another time.
I'll take vague hints, a brief description and a link to some docs, or any one of those at a pinch. None of that is novella. If you give me none of that, then you've released a useless pile of shit, and most developers will just use a different API by somebody else instead of integrating with your business.
Nice Errors
The specifics of how you throw these vary by language, framework, and personal preference. Here is an approach we use in Rails:
class ApplicationController < ActionController::Base
rescue_from 'User::MissingPaymentDetails' do |exception|
render_json_error(
:match_accepted_by_non_paying_passenger,
:precondition_failed,
details: exception.message
)
end
def render_json_error(code, status, options = {})
status = Rack::Utils::SYMBOL_TO_STATUS_CODE[status] if status.is_a? Symbol
error = {
code: I18n.t("error_codes.#{code}.code"),
title: I18n.t("error_codes.#{code}.message"),
status: status
}.merge(options)
render json: { errors: [error] }, status: status
end
end
Then in config/locales/errors.en.yaml
:
en:
error_codes:
# ...
match_accepted_by_non_paying_passenger:
code: 19005
message: It appears we're missing your payment details.
Using this approach, you can catch global exceptions at a very high level for really generic things like this, and handle very specific exceptions in controllers. Hitting the method yourself works too of course. We even have some logic that turns validation errors into standard errors in this way.
All of this allows for business logic to be translated into meaningful errors, all with useful computer and human readable error codes, with a convenient :match_accepted_by_non_paying_passenger
that corresponds to the full title and the error code. Then you're also providing the HTTP status via a symbol using :precondition_failed
instead of just shoving a 412
in the controller.
Comments ()