title: Fine Uploader S3 / jQuery / PHP - Failed upload = Reason: XHR returned response code 0
body: <p>I have been kicking around trying to implement the S3 uploader into my application and getting closer but no cigar.  Here is my setup in a nutshell:</p>
  
  <ul>
  <li>Running: 5.2.0 S3 with jQuery</li>
  <li>Server: PHP 5.6.6 on ArchLinux</li>
  <li>Max file size 25MB (defined in both s3.php and s3demo-cors.php)</li>
  <li>Debug enabled: which just pops the Javascript alert with the XHR error code 0 message (can't seem to gain visibility into whats going on behind the scenes with debug or in the apache logs)</li>
  <li>I specifically want to only use HTTPS for my bucket endpoint for security sake.</li>
  <li>Testing with Google Chrome Version 42.0.2311.90 m and IE 11.0.9600</li>
  <li>With Chrome it just fails, with IE it fails BUT it at least shows the progress meter for uploading the file, it hits 65% then fails.</li>
  </ul>
  
  <p>I have followed the blog post here: <a href="http://blog.fineuploader.com/2013/08/16/fine-uploader-s3-upload-directly-to-amazon-s3-from-your-browser/" rel="nofollow">http://blog.fineuploader.com/2013/08/16/fine-uploader-s3-upload-directly-to-amazon-s3-from-your-browser/</a> multiple times (hoping I am not missing something).</p>
  
  <ul>
  <li>QUESTION: One thing I am curious about is the JSON policy and signing. I am not really seeing the code in the s3demo-cors.php example that creates the json formatted details perhaps I am missing that element?  </li>
  </ul>
  
  <p>I know my IAM permissions are valid because other PHP tests allow various PutObject and list commands successfully.</p>
  
  <p>I have verified my CORS config is setup as follows for testing:</p>
  
  <pre><code>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
  &lt;CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"&gt;
  &lt;CORSRule&gt;
      &lt;AllowedOrigin&gt;*&lt;/AllowedOrigin&gt;
      &lt;AllowedMethod&gt;POST&lt;/AllowedMethod&gt;
      &lt;AllowedMethod&gt;PUT&lt;/AllowedMethod&gt;
      &lt;AllowedMethod&gt;DELETE&lt;/AllowedMethod&gt;
      &lt;MaxAgeSeconds&gt;3000&lt;/MaxAgeSeconds&gt;
      &lt;ExposeHeader&gt;ETag&lt;/ExposeHeader&gt;
      &lt;AllowedHeader&gt;*&lt;/AllowedHeader&gt;
  &lt;/CORSRule&gt;
  </code></pre>
  
  <p></p>
  
  <p>I have a few files I am using for this:</p>
  
  <p>s3.php = my test page with the fineuploader instance</p>
  
  <pre><code>&lt;!DOCTYPE html&gt;
  &lt;html&gt;
  &lt;head&gt;
  &lt;meta charset="utf-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
  
  &lt;!-- jQuery
  ====================================================================== --&gt;
  &lt;script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"&gt;&lt;/script&gt;
  
  &lt;!-- Fine Uploader Gallery CSS file
  ====================================================================== --&gt;
  &lt;link href="fine-uploader-gallery.css" rel="stylesheet"&gt;
  
  &lt;!-- Fine Uploader S3 jQuery JS file
  ====================================================================== --&gt;
  &lt;script src="s3.jquery.fine-uploader.js"&gt;&lt;/script&gt;
  
  &lt;!-- Fine Uploader Customized Gallery template
  ====================================================================== --&gt;
  &lt;script type="text/template" id="qq-template-s3"&gt;
      &lt;div class="qq-uploader-selector qq-uploader qq-gallery" qq-drop-area-text="Drop files here"&gt;
          &lt;div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container"&gt;
              &lt;div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"&gt;&lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone&gt;
              &lt;span class="qq-upload-drop-area-text-selector"&gt;&lt;/span&gt;
          &lt;/div&gt;
          &lt;div class="qq-upload-button-selector qq-upload-button"&gt;
              &lt;div&gt;Upload a file&lt;/div&gt;
          &lt;/div&gt;
          &lt;span class="qq-drop-processing-selector qq-drop-processing"&gt;
              &lt;span&gt;Processing dropped files...&lt;/span&gt;
              &lt;span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"&gt;&lt;/span&gt;
          &lt;/span&gt;
          &lt;ul class="qq-upload-list-selector qq-upload-list" role="region" aria-live="polite" aria-relevant="additions removals"&gt;
              &lt;li&gt;
                  &lt;span role="status" class="qq-upload-status-text-selector qq-upload-status-text"&gt;&lt;/span&gt;
                  &lt;div class="qq-progress-bar-container-selector qq-progress-bar-container"&gt;
                      &lt;div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"&gt;&lt;/div&gt;
                  &lt;/div&gt;
                  &lt;span class="qq-upload-spinner-selector qq-upload-spinner"&gt;&lt;/span&gt;
                  &lt;div class="qq-thumbnail-wrapper"&gt;
                      &lt;a class="preview-link" target="_blank"&gt;
                          &lt;img class="qq-thumbnail-selector" qq-max-size="120" qq-server-scale&gt;
                      &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;button class="qq-upload-cancel-selector qq-upload-cancel"&gt;X&lt;/button&gt;
                  &lt;button class="qq-upload-retry-selector qq-upload-retry"&gt;
                      &lt;span class="qq-btn qq-retry-icon" aria-label="Retry"&gt;&lt;/span&gt;
                      Retry
                  &lt;/button&gt;
  
                  &lt;div class="qq-file-info"&gt;
                      &lt;div class="qq-file-name"&gt;
                          &lt;span class="qq-upload-file-selector qq-upload-file"&gt;&lt;/span&gt;
                          &lt;span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"&gt;&lt;/span&gt;
                      &lt;/div&gt;
                      &lt;input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text"&gt;
                      &lt;span class="qq-upload-size-selector qq-upload-size"&gt;&lt;/span&gt;
                      &lt;button class="qq-btn qq-upload-delete-selector qq-upload-delete"&gt;
                          &lt;span class="qq-btn qq-delete-icon" aria-label="Delete"&gt;&lt;/span&gt;
                      &lt;/button&gt;
                      &lt;button class="qq-btn qq-upload-pause-selector qq-upload-pause"&gt;
                          &lt;span class="qq-btn qq-pause-icon" aria-label="Pause"&gt;&lt;/span&gt;
                      &lt;/button&gt;
                      &lt;button class="qq-btn qq-upload-continue-selector qq-upload-continue"&gt;
                          &lt;span class="qq-btn qq-continue-icon" aria-label="Continue"&gt;&lt;/span&gt;
                      &lt;/button&gt;
                  &lt;/div&gt;
              &lt;/li&gt;
          &lt;/ul&gt;
  
          &lt;dialog class="qq-alert-dialog-selector"&gt;
              &lt;div class="qq-dialog-message-selector"&gt;&lt;/div&gt;
              &lt;div class="qq-dialog-buttons"&gt;
                  &lt;button class="qq-cancel-button-selector"&gt;Close&lt;/button&gt;
              &lt;/div&gt;
          &lt;/dialog&gt;
  
          &lt;dialog class="qq-confirm-dialog-selector"&gt;
              &lt;div class="qq-dialog-message-selector"&gt;&lt;/div&gt;
              &lt;div class="qq-dialog-buttons"&gt;
                  &lt;button class="qq-cancel-button-selector"&gt;No&lt;/button&gt;
                  &lt;button class="qq-ok-button-selector"&gt;Yes&lt;/button&gt;
              &lt;/div&gt;
          &lt;/dialog&gt;
  
          &lt;dialog class="qq-prompt-dialog-selector"&gt;
              &lt;div class="qq-dialog-message-selector"&gt;&lt;/div&gt;
              &lt;input type="text"&gt;
              &lt;div class="qq-dialog-buttons"&gt;
                  &lt;button class="qq-cancel-button-selector"&gt;Cancel&lt;/button&gt;
                  &lt;button class="qq-ok-button-selector"&gt;Ok&lt;/button&gt;
              &lt;/div&gt;
          &lt;/dialog&gt;
      &lt;/div&gt;
  &lt;/script&gt;
  
  &lt;style&gt;
      #fine-uploader-s3 .preview-link {
          display: block;
          height: 100%;
          width: 100%;
      }
  &lt;/style&gt;
  
  &lt;title&gt;Fine Uploader S3 Demo&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;!-- Fine Uploader DOM Element
  ====================================================================== --&gt;
  &lt;div id="fine-uploader-s3"&gt;&lt;/div&gt;
  
  &lt;!-- Your code to create an instance of Fine Uploader and bind to the     DOM/template
  ====================================================================== --&gt;
  &lt;script&gt;
  $('#fine-uploader-s3').fineUploaderS3({
      debug: true,
      template: 'qq-template-s3',
      request: {
          endpoint: "https://s3.amazonaws.com/&lt;HIDDEN&gt;",
          accessKey: "&lt;HIDDEN&gt;"
      },
      signature: {
          endpoint: "s3demo-cors.php"
      },
      uploadSuccess: {
          endpoint: "s3demo-cors.php?success",
          params: {
              isBrowserPreviewCapable: qq.supportedFeatures.imagePreviews
          }
      },
      iframeSupport: {
          localBlankPagePath: "success.php"
      },
      cors: {
          expected: true
      },
      chunking: {
          enabled: true
      },
      resume: {
          enabled: true
      },
      retry: {
          enableAuto: true // defaults to false
      },
      deleteFile: {
          enabled: true,
          method: "POST",
          endpoint: "s3demo-cors.php"
      },
      validation: {
          itemLimit: 100,
          sizeLimit: 25000000
      },
      thumbnails: {
          placeholders: {
              notAvailablePath: "not_available-generic.png",
              waitingPath: "waiting-generic.png"
          }
      },
      callbacks: {
          onComplete: function(id, name, response) {
              var previewLink = qq(this.getItemByFileId(id)).getByClass('preview-link')[0];
  
              if (response.success) {
                  previewLink.setAttribute("href", response.tempLink)
              }
          },
          onError: function(id, name, errorReason, xhrOrXdr) {
              alert(qq.format("Error on file number {} - {}.  Reason: {}", id, name, errorReason));
          }
      }
  });
  &lt;/script&gt;
  &lt;/body&gt;
  &lt;/html&gt;
  </code></pre>
  
  <p>My PHP server side code is from your examples s3demo-cors.php
  (sorry code formatting got a little garbled when pasting in here)</p>
  
  <pre><code>    &lt;?php
  /**
  * PHP Server-Side Example for Fine Uploader S3.
  * Maintained by Widen Enterprises.
  *
  * Note: This is the exact server-side code used by the S3 example
  * on fineuploader.com.
  *
  * This example:
  *  - handles both CORS and non-CORS environments
  *  - handles delete file requests for both DELETE and POST methods
  *  - Performs basic inspections on the policy documents and REST headers before signing them
  *  - Ensures again the file size does not exceed the max (after file is in S3)
  *  - signs policy documents (simple uploads) and REST requests
  *    (chunked/multipart uploads)
  *  - returns a thumbnailUrl in the response for older browsers so thumbnails can be displayed next to the file
  *
  * Requirements:
  *  - PHP 5.3 or newer
  *  - Amazon PHP SDK (only if utilizing the AWS SDK for deleting files or otherwise examining them)
  *
  * If you need to install the AWS SDK, see http://docs.aws.amazon.com/aws-sdk-php-2/guide/latest/installation.html.
  */
  // You can remove these two lines if you are not using Fine Uploader's
  // delete file feature
  //require 'aws-autoloader.php';
  require 'vendor/autoload.php';
  use Aws\S3\S3Client;
  // These assume you have the associated AWS keys stored in
  // the associated system environment variables
  $clientPrivateKey = '&lt;HIDDEN&gt;';
  // These two keys are only needed if the delete file feature is enabled
  // or if you are, for example, confirming the file size in a successEndpoint
  // handler via S3's SDK, as we are doing in this example.
  $serverPublicKey = $_SERVER['PARAM1'];
  $serverPrivateKey = $_SERVER['PARAM2'];
  // The following variables are used when validating the policy document
  // sent by the uploader: 
  $expectedBucketName = "&lt;HIDDEN&gt;";
  // $expectedMaxSize is the value you set the sizeLimit property of the 
  // validation option. We assume it is `null` here. If you are performing
  // validation, then change this to match the integer value you specified
  // otherwise your policy document will be invalid.
  // http://docs.fineuploader.com/branch/develop/api/options.html#validation-    option
  $expectedMaxSize = 25000000;
  $method = getRequestMethod();
  // This first conditional will only ever evaluate to true in a
  // CORS environment
  if ($method == 'OPTIONS') {
  handlePreflight();
  }
  // This second conditional will only ever evaluate to true if
  // the delete file feature is enabled
  else if ($method == "DELETE") {
  handleCorsRequest(); // only needed in a CORS environment
  deleteObject();
  }
  // This is all you really need if not using the delete file feature
  // and not working in a CORS environment
  else if ($method == 'POST') {
  handleCorsRequest();
  // Assumes the successEndpoint has a parameter of "success" associated with it,
  // to allow the server to differentiate between a successEndpoint request
  // and other POST requests (all requests are sent to the same endpoint in this example).
  // This condition is not needed if you don't require a callback on upload success.
  if (isset($_REQUEST["success"])) {
      verifyFileInS3(shouldIncludeThumbnail());
  }
  else {
      signRequest();
  }
  }
  // This will retrieve the "intended" request method.  Normally, this is the
  // actual method of the request.  Sometimes, though, the intended request method
  // must be hidden in the parameters of the request.  For example, when attempting to
  // send a DELETE request in a cross-origin environment in IE9 or older, it is not
  // possible to send a DELETE request.  So, we send a POST with the intended method,
  // DELETE, in a "_method" parameter.
  function getRequestMethod() {
  global $HTTP_RAW_POST_DATA;
  // This should only evaluate to true if the Content-Type is undefined
  // or unrecognized, such as when XDomainRequest has been used to
  // send the request.
  if(isset($HTTP_RAW_POST_DATA)) {
      parse_str($HTTP_RAW_POST_DATA, $_POST);
  }
  if (isset($_POST['_method'])) {
      return $_POST['_method'];
  }
  return $_SERVER['REQUEST_METHOD'];
  }
  // Only needed in cross-origin setups
  function handleCorsRequest() {
  // If you are relying on CORS, you will need to adjust the allowed domain here.
  header('Access-Control-Allow-Origin: *');
  }
  // Only needed in cross-origin setups
  function handlePreflight() {
  handleCorsRequest();
  header('Access-Control-Allow-Methods: POST');
  header('Access-Control-Allow-Headers: Content-Type');
  }
  function getS3Client() {
  global $serverPublicKey, $serverPrivateKey;
  return S3Client::factory(array(
      'key' =&gt; $serverPublicKey,
      'secret' =&gt; $serverPrivateKey
  ));
  }
  // Only needed if the delete file feature is enabled
  function deleteObject() {
  getS3Client()-&gt;deleteObject(array(
      'Bucket' =&gt; $_POST['bucket'],
      'Key' =&gt; $_POST['key']
  ));
  }
  function signRequest() {
  header('Content-Type: application/json');
  $responseBody = file_get_contents('php://input');
  $contentAsObject = json_decode($responseBody, true);
  $jsonContent = json_encode($contentAsObject);
  if (!empty($contentAsObject["headers"])) {
      signRestRequest($contentAsObject["headers"]);
  }
  else {
      signPolicy($jsonContent);
  }
  }
  function signRestRequest($headersStr) {
  if (isValidRestRequest($headersStr)) {
      $response = array('signature' =&gt; sign($headersStr));
      echo json_encode($response);
  }
  else {
      echo json_encode(array("invalid" =&gt; true));
  }
  }
  function isValidRestRequest($headersStr) {
  global $expectedBucketName;
  $pattern = "/\/$expectedBucketName\/.+$/";
  preg_match($pattern, $headersStr, $matches);
  return count($matches) &gt; 0;
  }
  function signPolicy($policyStr) {
  $policyObj = json_decode($policyStr, true);
  if (isPolicyValid($policyObj)) {
      $encodedPolicy = base64_encode($policyStr);
      $response = array('policy' =&gt; $encodedPolicy, 'signature' =&gt;             sign($encodedPolicy));
      echo json_encode($response);
  }
  else {
      echo json_encode(array("invalid" =&gt; true));
  }
  }
  function isPolicyValid($policy) {
  global $expectedMaxSize, $expectedBucketName;
  $conditions = $policy["conditions"];
  $bucket = null;
  $parsedMaxSize = null;
  for ($i = 0; $i &lt; count($conditions); ++$i) {
      $condition = $conditions[$i];
      if (isset($condition["bucket"])) {
          $bucket = $condition["bucket"];
      }
      else if (isset($condition[0]) &amp;&amp; $condition[0] == "content-length-range") {
          $parsedMaxSize = $condition[2];
      }
  }
  return $bucket == $expectedBucketName &amp;&amp; $parsedMaxSize == (string)$expectedMaxSize;
  }
  function sign($stringToSign) {
  global $clientPrivateKey;
  return base64_encode(hash_hmac(
      'sha1',
      $stringToSign,
      $clientPrivateKey,
      true
  ));
  }
  // This is not needed if you don't require a callback on upload success.
  function verifyFileInS3($includeThumbnail) {
  global $expectedMaxSize;
  $bucket = $_POST["bucket"];
  $key = $_POST["key"];
  // If utilizing CORS, we return a 200 response with the error message in the body
  // to ensure Fine Uploader can parse the error message in IE9 and IE8,
  // since XDomainRequest is used on those browsers for CORS requests.      XDomainRequest
  // does not allow access to the response body for non-success responses.
  if (isset($expectedMaxSize) &amp;&amp; getObjectSize($bucket, $key) &gt; $expectedMaxSize) {
      // You can safely uncomment this next line if you are not depending on CORS
      header("HTTP/1.0 500 Internal Server Error");
      deleteObject();
      echo json_encode(array("error" =&gt; "File is too big!", "preventRetry" =&gt; true));
  }
  else {
      $link = getTempLink($bucket, $key);
      $response = array("tempLink" =&gt; $link);
      if ($includeThumbnail) {
          $response["thumbnailUrl"] = $link;
      }
      echo json_encode($response);
  }
  }
  // Provide a time-bombed public link to the file.
  function getTempLink($bucket, $key) {
  $client = getS3Client();
  $url = "{$bucket}/{$key}";
  $request = $client-&gt;get($url);
  return $client-&gt;createPresignedUrl($request, '+15 minutes');
  }
  function getObjectSize($bucket, $key) {
  $objInfo = getS3Client()-&gt;headObject(array(
      'Bucket' =&gt; $bucket,
      'Key' =&gt; $key
  ));
  return $objInfo['ContentLength'];
  }
  // Return true if it's likely that the associate file is natively
  // viewable in a browser.  For simplicity, just uses the file extension
  // to make this determination, along with an array of extensions that one
  // would expect all supported browsers are able to render natively.
  function isFileViewableImage($filename) {
  $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
  $viewableExtensions = array("jpeg", "jpg", "gif", "png");
  return in_array($ext, $viewableExtensions);
  }
  // Returns true if we should attempt to include a link
  // to a thumbnail in the uploadSuccess response.  In it's simplest form
  // (which is our goal here - keep it simple) we only include a link to
  // a viewable image and only if the browser is not capable of generating a         client-side preview.
  function shouldIncludeThumbnail() {
  $filename = $_POST["name"];
  $isPreviewCapable = $_POST["isBrowserPreviewCapable"] == "true";
  $isFileViewableImage = isFileViewableImage($filename);
  return !$isPreviewCapable &amp;&amp; $isFileViewableImage;
  }
  ?&gt;
  </code></pre>