rails exceptions in xml

September 13, 2009

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.


Comments

Austin Putman September 13, 2009

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?

Sarah September 13, 2009

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.

Erik Ostrom September 13, 2009

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?

Sarah September 13, 2009

Good point. I think empty content with a status code is a reasonable solution.

Wolfram Arnold September 24, 2009

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.

Josh Gum November 25, 2009

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

Sarah November 25, 2009

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.

Josh Gum November 25, 2009

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

Josh Gum November 25, 2009

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

Sarah November 26, 2009

That's just a regular error I think, not thrown as an exception...