We ran into an issue last week where our XML APIs were returning HTML under certain error conditions, rather than the expected XML. Our solution was to add the following code to the ApplicationController:
rescue_from Exception do |exception| respond_to do |format| format.xml { render :xml => "<error>Internal Server Error #{exception.message}</error>", :status => 500 } format.html { render :html => {:file => 'public/500.html'}, :status => 500 } format.json { render :json => {:error => "Internal Server Error #{exception.message}"}.to_json, :status => 500 } end end
We might have also declared a rescue_action, and I’m not sure of the benefits of one over the other, except that perhaps we needed to implement a general form of rescue_from since we had another more specific form already declared.
It seemed to me that this should be the default behavior in rails, so I decided to dig into it a little more and see what I could discover. I started by making a little test app to reproduce the exception. The particular case from last week was a database limit that wasn’t being caught in the app with a length validation. When I tried to re-create the error in MySql, I noticed that no exception is thrown since MySql will just truncate the data (although perhaps that is only because I am not running MySql in strict mode). In PostgreSQL, the database layer will throw an exception.
Test app setup:
rails -d postgresql test_postgresql cd test_postgresql/ script/generate scaffold person first:string last:string present:boolean
Edit the migration to create a database limit:
class CreatePeople < ActiveRecord::Migration def self.up create_table :people do |t| t.string :first, :limit => 40 t.string :last, :limit => 40 t.boolean :present t.timestamps end end def self.down drop_table :people end end
Create the postgres user. Note double-quotes around user, single quotes around password. It has to be that way. Go figure.
$ sudo su postgres -c psql postgres=# create user "test_postgresql" with superuser password 'password'; CREATE ROLE postgres=# q
Finally create the database, run migration, and start the server:
rake db:create:all rake db:migrate ./script/server
If you point your browser at http://localhost:3000/people and try to create a person with more that 40 characters in the first name, you will see the following error:
ActiveRecord::StatementInvalid in PeopleController#create PGError: ERROR: value too long for type character varying(40)
That is all well and good; however, if you do the same in XML, you will get the same error in HTML.
$ curl -X POST -d "<person><first>This is a first name that is too long for the database limit</first></person>" -H "Content-Type: application/xml" http://localhost:3000/people.xml <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Action Controller: Exception caught</title> <style> body { background-color: #fff; color: #333; } body, p, ol, ul, td { font-family: verdana, arial, helvetica, sans-serif; font-size: 13px; line-height: 18px; }
That seems like a bug to me. Perhaps this should be a lighthouse ticket rather than a blog post.. still not confident in identifying bugs in Rails, so I figured I’d post here first.
Don’t know if its a bug per se. It’s odd as the database is doing validation in addition to the validations done by the model. I would say that if the 40 character limit is important, you could put an ActiveRecord validation in to prevent this situation. Then you will get the same error across all databases.
It surprises me that the controller logic doesn’t handle this case. Usually the database update is tested as a conditional:
if @person.save
respond_to # happy path block
…
else
respond_to # error message logic block
….
end
In your case, the save is throwing an error, like save!. The AR::Postgres adapter could maybe rescue that error and return false, but how can it represent the nature of the error in a way that is accessible to the controller?
Yeah, I consider it a bug in my code that there wasn’t a validation on the model; however, I wanted to be sure if *any* code throws an exception that it is returned to the client as XML (because the client isn’t built to expect HTML as a response to an XML API nor should it be).
Certainly the exception is not caught and perhaps it should be by ActiveRecord, which could then populate @person.errors with the message from the database adapter.
It seems reasonable that exceptions should always be returned in the format the client requests, but I’m not sure I’d want Rails to enforce this. Since XML is sort of a meta-format, it’s not enough to just have Rails return “something in XML”; it has to return something that conforms to the client’s expectations about how exceptions are rendered in XML. That’s application-specific.
(Or at least, uh, “specific-kind-of-XML-specific”. Like, if you’re using SOAP, that has its own exception protocol.)
Maybe when there’s an unhandled exception Rails should just return an HTTP error code with no content? Unless the application sets up an exception handler?
Good point. I think empty content with a status code is a reasonable solution.
I like Erik Ostrom’s suggestion, but I would refine it to say that if the accept header calls for HTML Rails behaves as today, as many web sites rely on this Rails default to return an HTML error message.
Where Rails fails today is that is replies in HTML even if the accept header specifies something else (e.g. XML or JSON). In that case, the default behavior would probably be best to just return a status code and leave it to the application developer to add textual error messages.
Try;
format.xml { render :xml => {:message => “Internal Server Error #{exception.message}”}.to_xml(:root => “error”), :status => 500}
format.json { render :json => {:error => { :message => “Internal Server Error #{exception.message}”}}, :status => 500
Ok, maybe that’s a little more rails-y, but I’m not sure that it is more readable than just plunking the wrapper xml node into a string and inserting the exception.message with string interpolation as we did.
Sure.. What’s strange is your respond block was rendering HTML..? I’ve got the same thing setup and mine return json and xml.
curl -H ‘Accepts: text/xml’ -i http://127.0.0.1:3000/offers/999.xml
HTTP/1.1 422
Connection: close
Date: Wed, 25 Nov 2009 20:04:35 GMT
Content-Type: application/xml; charset=utf-8
Cache-Control: no-cache
Content-Length: 68
Internal Server Error Couldn’t find Offer with ID=999
Sorry, was actually :
HTTP/1.1 422
Connection: close
Date: Wed, 25 Nov 2009 20:16:40 GMT
Content-Type: application/xml; charset=utf-8
Cache-Control: no-cache
Content-Length: 141
record_not_found
Couldn’t find Offer with ID=999
That’s just a regular error I think, not thrown as an exception…