Dealing with HTTP errors in a Flex with Rails application.

The Flash Player is restricted in the way it deals with HTTP errors. This is mainly due to provide cross browser consistency and I believe is due to the restrictions the browser imposes on the Flash Player plugin. In fact when your Flex application performs HTTP requests using the HTTPService class, the request is passed by the Flash Player to the browser and in case of an Rails error (500, 404, …) the response is somehow crippled on the way back.

Problem

So let’s consider that the Flex application requests to update a Person but the validation fails. In the update method of our Rails controller the HTTP Status is set to :unprocessable_entity. This corresponds the HTTP error code 422.

  # PUT /people/1
  # PUT /people/1.xml
  def update
    @person = Person.find(params[:id])

    respond_to do |format|
      if @person.update_attributes(params[:person])
        flash[:notice] = 'Person was successfully updated.'
        format.html { redirect_to(@person) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @person.errors, :status => :unprocessable_entity }
      end
    end
  end

Now in our Flex application by default we cannot identify that the error 422 occured, more annoyingly we cannot retrieve the Rails error messages. All we get back is the following:

[FaultEvent fault=[RPC Fault faultString="HTTP request error" faultCode="Server.Error.Request" faultDetail="Error: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 text="Error #2032: Stream Error. URL: http://localhost:3000/people/8.xml?_method=put"]. URL: http://localhost:3000/people/8.xml?_method=put"] messageId="65EBBA92-5911-68D2-1710-18A687C28455" type="fault" bubbles=false cancelable=true eventPhase=2]

Solution

I haven’t found a way to have the status code and error message appear in the Flex application without having to change the Rails application. But fortunately, I was able to deal with that by using an after_filter at the application controller level, hence having only one place in the application to take care of all controllers and requests. The trick is to “hide” the HTTP Status error code in Rails and as the Flex application deals with XML responses, simply check in Flex if the response starts with <errors>. This can then also be dealt with in the Flex application in one place of the application. In a Cairngorm application I had the Delegate transform these “errors” responses to Faults.

Here is an example of the change to the Rails ApplicationController.

class ApplicationController < ActionController::Base
  
  after_filter :flex_error_handling
  
  def flex_error_handling
    response.headers['Status'] = interpret_status(200) if response.headers['Status'] == interpret_status(422)
  end
  
  def rescue_action_in_public(exception)
    render_exception(exception)
  end
  def rescue_action_locally(exception)
    render_exception(exception)
  end

  rescue_from ActiveRecord::RecordNotFound, :with => :render_exception
  def render_exception(exception)
    render :text => "<errors><error>#{exception}</error></errors>", :status => 200 
  end
  
end

Posted by Daniel Wanja Wed, 20 Feb 2008 15:56:23 GMT


Comments

  1. royston 36 minutes later:

    http://code.google.com/p/as3httpclientlib/
    You’ll get back status codes just fine.

  2. Alex MacCaw about 1 hour later:

    Yes, that’s the way I’ve done it. I have a parameter called ‘always_return_200’ and check the response for errors. Or you could use a custom socket lib (like royston suggests). You could clean up the rails code though. There’s a new method called rescue_from in edge. Also if the errors are on a model (which I presume they are) you can call model.errors.to_xml. e.g.

    rescue_from ActiveRecord::RecordInvalid do |exception| respond_to do |wants| wants.xml { render :xml => exception.record.errors } end end
  3. Matthijs Langenberg about 4 hours later:

    It’s good to know that the HTTP status and body are available when dealing with an Adobe AIR application.

  4. Matthijs Langenberg about 4 hours later:

    It’s good to know that the HTTP status and body are available when dealing with an Adobe AIR application.

  5. Chris 14 days later:

    but what’s make the responses of XMLHTTPRequest take such a long time (in most cases nearly 7 seconds), when the statuscode is set to less known status like 424, 415 or in your case 422? setting it to 400, 500 or other well known status-codes the response returns much faster. i checked it and found out that ther rendering itself doesn’t cost that time. i really don’t know. you can try and set your statuscode to 422. what is happening in the meantime? and how to prevent it? watching firebug: the reponse contains “Loading…”. ???
    i hope there is somebody out there who knows an answer.

    best regards
    chris

  6. Chris 15 days later:

    sorry, for my question. the reason was the application-server i used: lighttpd. with webrick (and i suppose the apache too) it works as expected.

  7. Walter about 1 month later:

    Very helpful. Thanks for the post.

    One tweak that I did… I wanted my application to give the normal responses if the client was something other than Flex. So I modified to the following:

    
      def flex_error_handling
        valid_status_codes = [201, 422].collect{|code| interpret_status(code)}
        if request.headers.has_key?('HTTP_X_FLASH_VERSION') && valid_status_codes.include?(response.headers['Status'])
          response.headers['Status'] = interpret_status(200)
        end
      end
    
    
  8. tejaswini 3 months later:

    <?xml version=“1.0” encoding=“utf-8”?>

    <!--<mx:TextArea id="a" x="634" y="101" height="228"/> <mx:Button x="526" y="213" label="Button" click="test()"/>-->

    <![CDATA[
    import mx.validators.ValidationResult;
    import flash.net.sendToURL;
    import mx.controls.Alert;
    import mx.utils.ObjectUtil;
    import mx.controls.Text;
    import mx.collections.ArrayCollection;
    import mx.controls.ComboBox;
    import mx.rpc.xml.SimpleXMLEncoder;
    import mx.controls.DataGrid;
    var xml1:XML;
    //import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    public var arr_list1:Array=new Array();
    // var arr_list2:ArrayCollection=new ArrayCollection();
    public var arr_list2:ArrayCollection=new ArrayCollection();
    //public var arr_email:ArrayCollection= new ArrayCollection();
    // var arr_list3:Array=new Array();
    //var xml:XML = objectToXML(arr_list2.source);

    private function init():void{
    ws.ROLEMASTER.send();
    ws.email.send();
    // ws.addemail.send();
    }
    private function remove():void{
    arr_list2.removeItemAt(dg2.selectedIndex);

    }

    private function dome():void
    {
    arr_list2.addItem(txt.text);
    dg2.dataProvider=arr_list2;
    }

    private function handleResult():void { Alert.show(ws.addemail.lastResult.toString()); //Alert.show(ws.addemail.ResultEvent.toLocaleString()); }

    private function onResult(event:ResultEvent):void {

    if (event.result.Tables != null) { // panel.removeAllChildren() for each (var table:Object in event.result.Tables) { displayROLEMASTERTable(table); // displayemailTable(table); } for each (var table1:Object in event.result.Tables) { displayemailTable(table1); } } } private function displayROLEMASTERTable(tbl:Object):void { for(var i:int=0;i<tbl.Rows.length;i++) { arr_list1.push(tbl.Rows[i].ROLENAME); } comb.dataProvider=arr_list1; }

    /*/private function nextresult(event:ResultEvent):void{
    if (event.result.Tables!=null)
    for each (var table:Object in event.result.Tables)
    {
    displayemailTable(table);

    }

    } */
    private function displayemailTable(tbl1:Object):void{

    for(var j:int=0;j<tbl1.Rows.length;j++)
    {
    arr_list2.addItem({Email:tbl1.Rows[j].emailid});

    //arr_list3.push({Email:tbl1.Rows[j].a});
    }
    dg2.dataProvider=arr_list2;

    }

    public function objectToXML(obj:Object):XML { var qName:QName= new QName(“root”); var xmlDocument:XMLDocument= new XMLDocument(); var simpleXMLEncoder:SimpleXMLEncoder= new SimpleXMLEncoder(xmlDocument); var xmlNode:XMLNode= simpleXMLEncoder.encodeValue(obj,qName,xmlDocument); var xml:XML = new XML); return xml; }

    private function store():void{

    // var xml2:XML; xml1=objectToXML(arr_list2.source); // xml2=xml1.toXMLString(); ws2.addemail.send(xml1.toXMLString(),‘A’); //myWS.Map.send(xml2,combo.selectedItem.RoleID); // ws.addemail.send(xml1.dg2.selectedItem.emailid); }

    ]]>

    <mx:operation name=“addemail” resultFormat=“object” result=“handleResult()”

  9. John Burgoon 9 months later:

    Thank you for this! It doesn’t prevent the error but at least it stops the cryptic “Error #2032” and allows me to confirm that this is the problem.

  10. Scott about 1 year later:

    When you get errors couldn’t you just change the Unprocessable Entity to a 200. That way it sends back the errors in xml format and you can deal with that in client side flex code.

  11. Daniel Wanja over 1 year later:

    Hey Scott, yea that’s what we do in the flex_error_handling after filter.