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.

10 thoughts on “rails exceptions in xml

  1. 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?

  2. 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.

  3. 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?

  4. 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.

  5. 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

  6. 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.

  7. 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

  8. 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

What do you think?