Search code examples
actionscript-3flashairflash-builder

Error #2044: Unhandled IOErrorEvent:. text=Error #2038: File I/O Error


I have a standard AIR script compiled on Flash or Flash Builder 4.5 designed to upload a file to a php server-side code. The code starts by writing the file to the users desktop, before completing the request.

import flash.net.*;
import flash.filesystem.*

//create the file
var file:File = File.desktopDirectory;
file = file.resolvePath("test.txt");
var fs:FileStream = new FileStream();
fs.open(file, FileMode.WRITE);
//write "test" to txt file
fs.writeUTFBytes("test");
fs.close();

//start request
var req:URLRequest = new URLRequest();
req.method = "POST";
req.url = //php file
var vars:URLVariables = new URLVariables();
vars.a = 1;
vars.b = 2;
vars.c = 3
req.data = vars;
file.upload(req, "txt_file");

Now, when I upload to a php file on my own Apache test server on localhost (XAMPP), the file upload goes through without any errors. However, when I try to upload to my actual server, I get an I/O error:

Error #2044: Unhandled IOErrorEvent:. text=Error #2038: File I/O Error.

I have checked my max_file_size set at 2MB so that is not the issue. The URL is also correct. Looking at other sources (e.g. http://www.judahfrangipane.com/blog/2007/01/01/error-2044-unhandled-ioerrorevent-texterror-2038-file-io-error/), some suggest the issue is with Apache ModSecurity, turned off by setting .htaccess for the directory to:

SecFilterEngine Off
SecFilterScanPOST Off

This does not work. In fact, it makes the rest of my code designed to obtain data from the same server obsolete. I also know its not a cross-domain issue. I have also tried setting the php code to return HTTP 200. Still does not work. Some have suggested that these errors are a bit random and the actual file upload has taken place regardless of the error. So the problem could be fixed by just catching the error and ignoring it (http://dev.nuclearrooster.com/2008/04/05/uploading-with-a-filereference-from-flex-3-on-os-x/). This is not an option for me as I need to track the upload progress.

What is interesting is that this seems to be a Mac OSX issue as I have the exact script running on a Windows compiler without errors although the only stipulation is that they were made with older version of AIR. So I am not sure whether that is the case here.

I have been banging my head against the wall for 3 days. Please help me......

UPDATE

Just found that my server where the file is uploading to is returning a HTTP 301 response on the file upload request, something which again only happens on OSX on AIR and not on Windows or form data submitted via Firefox.


Solution

  • OK. This definitely seems to be a Mac OSX issue. For some reason, the HTTP headers created by File.upload() method in OSX cause the external server to crash with a 301 Move Permanently. This DOES NOT happen in Windows. Of course, anything other than a HTTP 2xx response will result in an I/O Error as far as the AIR compiler concerned. Therefore, in order to get around the file upload problem, we have to manually create the HTTP headers responsible for the upload per se. I would suggest not using File.upload() (or FileReference.upload() for that matter) until this issue is resolved.

    In order to do this, the file that we are uploading must be converted into a ByteArray. Therefore I would only use File/FileReference for capturing the file. For the actual upload, the file must be converted to ByteArray.

    In my example above, it's very simple:

    import flash.net.*
    import flash.utils.ByteArray;
    
    var xml:XML = new XML(<items><item>one</item><item>two</item></items>);
    var data:ByteArray = new ByteArray();
    data.writeUTF(xml.toXMLString());
    

    Now as far creating the manual HTTP headers, credit goes to Jonathan Marston (http://marstonstudio.com/2007/10/19/how-to-take-a-snapshot-of-a-flash-movie-and-automatically-upload-the-jpg-to-a-server-in-three-easy-steps/) and his beautiful UploadPostHelper class:

    package {
    
        import flash.events.*;
        import flash.net.*;
        import flash.utils.ByteArray;
        import flash.utils.Endian;
    
        /**
         * Take a fileName, byteArray, and parameters object as input and return ByteArray post data suitable for a UrlRequest as output
         *
         * @see http://marstonstudio.com/?p=36
         * @see http://www.w3.org/TR/html4/interact/forms.html
         * @see http://www.jooce.com/blog/?p=143
         * @see http://www.jooce.com/blog/wp%2Dcontent/uploads/2007/06/uploadFile.txt
         * @see http://blog.je2050.de/2006/05/01/save-bytearray-to-file-with-php/
         *
         * @author Jonathan Marston
         * @version 2007.08.19
         *
         * This work is licensed under a Creative Commons Attribution NonCommercial ShareAlike 3.0 License.
         * @see http://creativecommons.org/licenses/by-nc-sa/3.0/
         *
         */
        public class UploadPostHelper {
    
            /**
             * Boundary used to break up different parts of the http POST body
             */
            private static var _boundary:String = "";
    
            /**
             * Get the boundary for the post.
             * Must be passed as part of the contentType of the UrlRequest
             */
            public static function getBoundary():String {
    
                if(_boundary.length == 0) {
                    for (var i:int = 0; i < 0x20; i++ ) {
                        _boundary += String.fromCharCode( int( 97 + Math.random() * 25 ) );
                    }
                }
    
                return _boundary;
            }
    
            /**
             * Create post data to send in a UrlRequest
             */
            public static function getPostData(fileName:String, byteArray:ByteArray, parameters:Object = null):ByteArray {
    
                var i: int;
                var bytes:String;
    
                var postData:ByteArray = new ByteArray();
                postData.endian = Endian.BIG_ENDIAN;
    
                //add Filename to parameters
                if(parameters == null) {
                    parameters = new Object();
                }
                parameters.Filename = fileName;
    
                //add parameters to postData
                for(var name:String in parameters) {
                    postData = BOUNDARY(postData);
                    postData = LINEBREAK(postData);
                    bytes = 'Content-Disposition: form-data; name="' + name + '"';
                    for ( i = 0; i < bytes.length; i++ ) {
                        postData.writeByte( bytes.charCodeAt(i) );
                    }
                    postData = LINEBREAK(postData);
                    postData = LINEBREAK(postData);
                    postData.writeUTFBytes(parameters[name]);
                    postData = LINEBREAK(postData);
                }
    
                //add Filedata to postData
                postData = BOUNDARY(postData);
                postData = LINEBREAK(postData);
                bytes = 'Content-Disposition: form-data; name="Filedata"; filename="';
                for ( i = 0; i < bytes.length; i++ ) {
                    postData.writeByte( bytes.charCodeAt(i) );
                }
                postData.writeUTFBytes(fileName);
                postData = QUOTATIONMARK(postData);
                postData = LINEBREAK(postData);
                bytes = 'Content-Type: application/octet-stream';
                for ( i = 0; i < bytes.length; i++ ) {
                    postData.writeByte( bytes.charCodeAt(i) );
                }
                postData = LINEBREAK(postData);
                postData = LINEBREAK(postData);
                postData.writeBytes(byteArray, 0, byteArray.length);
                postData = LINEBREAK(postData);
    
                //add upload filed to postData
                postData = LINEBREAK(postData);
                postData = BOUNDARY(postData);
                postData = LINEBREAK(postData);
                bytes = 'Content-Disposition: form-data; name="Upload"';
                for ( i = 0; i < bytes.length; i++ ) {
                    postData.writeByte( bytes.charCodeAt(i) );
                }
                postData = LINEBREAK(postData);
                postData = LINEBREAK(postData);
                bytes = 'Submit Query';
                for ( i = 0; i < bytes.length; i++ ) {
                    postData.writeByte( bytes.charCodeAt(i) );
                }
                postData = LINEBREAK(postData);
    
                //closing boundary
                postData = BOUNDARY(postData);
                postData = DOUBLEDASH(postData);
    
                return postData;
            }
    
            /**
             * Add a boundary to the PostData with leading doubledash
             */
            private static function BOUNDARY(p:ByteArray):ByteArray {
                var l:int = UploadPostHelper.getBoundary().length;
    
                p = DOUBLEDASH(p);
                for (var i:int = 0; i < l; i++ ) {
                    p.writeByte( _boundary.charCodeAt( i ) );
                }
                return p;
            }
    
            /**
             * Add one linebreak
             */
            private static function LINEBREAK(p:ByteArray):ByteArray {
                p.writeShort(0x0d0a);
                return p;
            }
    
            /**
             * Add quotation mark
             */
            private static function QUOTATIONMARK(p:ByteArray):ByteArray {
                p.writeByte(0x22);
                return p;
            }
    
            /**
             * Add Double Dash
             */
            private static function DOUBLEDASH(p:ByteArray):ByteArray {
                p.writeShort(0x2d2d);
                return p;
            }
    
        }
    }
    

    Now, we utilize the class to create our HTTP header using good old URLRequest:

    var req:URLRequest = new URLRequest();
    req.url = //server-side url (.php)
    req.contentType = "multipart/form-data; boundary=" + UploadPostHelper.getBoundary();
    req.method = URLRequestMethod.POST;
    //Be sure to place the actual file name with its extension as the file name. Set the second argument as the ByteArray created earlier. Any other parameters (name-value pairs) can be added via an optional third parameter (see class above)
    req.data = UploadPostHelper.getPostData("test.xml", data);
    req.requestHeaders.push(new URLRequestHeader("Cache-Control", "no-cache"));
    

    Now instead of using File.upload() to complete the request, just use URLLoader:

    var loader:URLLoader = new URLLoader();
    //We have to load the data as binary as well
    loader.dataFormat = URLLoaderDataFormat.BINARY;
    loader.addEventListener(Event.COMPLETE, complete);
    loader.addEventListener(IOErrorEvent.IO_ERROR, ioerror);
    loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, secerror);
    loader.load(req);
    
    //Event handlers
    function complete(e:Event):void {
        var ba:ByteArray = e.target.data;
        //Returns the XML data
        trace(ba.readUTF()); 
    }
    function ioerror(e:IOErrorEvent):void {}
    function secerror(e:SecurityErrorEvent):void {}
    

    And all should work nicely. As far as the server-side is concerned, the name of the file is defined by the class above as Filedata. A simple PHP script to capture the file would look something like this:

    <?php
    echo file_get_contents($_FILES['Filedata']['tmp_name']);
    var_dump($_FILES['Filedata']);
    /*
    array(1) {
        ["Filedata"]=>array(5) {
            ["name"]=>string(8) "test.xml"
            ["type"]=>string(24) "application/octet-stream"
            ["tmp_name"]=>string(38) "root/tmp/php2jk8yk"
            ["error"]=>int(0)
            ["size"]=>int(58)
        }
    }
    */
    ?>