Recently we improved performance of one of our Ruby on Rails application. We used NewRelic tool to identify areas that were slow. One of which was file and image uploads to S3 from a text editor (Redactor).

When a user uploads a big file or image - the app's server response time shoots up degrading the performance and overall throughput because the files and images were being uploaded via rails action. The server process was busy uploading file on S3 while other requests to server were being queued increasing the request processing time. To improve this, we decided to delegate the job of file uploading to a new microservice reducing some load on our main server.

We created a microservice for uploading files and images and that brought us in front of another challenge.

The Redactor - our text editor, uploads the files and saves the response in an iframe. You can read the contents in an iframe easily via jQuery ($(iframe).contents()) if the domain of the webpage which contains the iframe is same as the domain of the web-page opened in iframe.

The microservice responsible for uploading file was on different domain and the main app is on another domain. When we try to access the response in the iframe, we get security error - "Access denied: Cross-domain security error".

To access cross-domain iframe, the best approach is to use Javascript's postMessage() method. This method provides a way to securely pass messages across domains.

Sending Messages with postMessage()

The postMessage() method accepts two parameters.

  • message - A string or object that will be sent to receiving window.
  • targetOrigin - The URL of the window the message is being sent to.

This method should be called on the window the message is being sent to.

targetWindow.postMessage("Hello !!", "http://example.com")

In case of iframes, the window can be obtained by accessing the contentWindow property on the desired iframe.

Now lets receive messages sent to a window.

Setting up Event Listeners to Receive Messages

When a call to postMessage() is executed successfully a MessageEvent is fired on the receiving window. A standard event listener is enough to watch for this event and execute some code when it occurs.

To access the string or object that was sent by postMessage(), data property of the event passed into the listener callback can be used.

window.addEventListener('message', function(e) {
  var message = e.data;
});

How we used in our app

Code in response of microservice -

<pre id="s3_response"><%= s3_response %></pre>
<script>
  respondToMessage = function(event) {
    if(event.origin == "http://our_domain.com"){
      if (event.data == 's3_upload') {
        var data = document.getElementById("s3_response").textContent;
        window.parent.postMessage(data, event.origin);
      }
    }
  }
  window.addEventListener("message", respondToMessage, false);
</script>

Code in uploadLoaded callback of Redactor in main app -

redactorUpload = function(e) {
  // Check to make sure that this message came from the correct domain.
  if (e.origin != "http://microservice_domain.com")
    return;
  
  // Get message received
  var data = e.data;
  
  postUploadProcessing(data);

  // remove MessageEvent to prevent executing this function twice when another file is uploaded without refreshing the window
  window.removeEventListener("message", redactorUpload, false)
}
window.addEventListener("message", redactorUpload, false);

var iframe = document.getElementById(this.id); // this.id gives id of iframe containing upload response
iframe.contentWindow.postMessage("s3_upload", microservice_upload_url);

Series of events happening sequentially explaining the flow -

  1. File is uploaded to S3 via microservice.
  2. The response from microservice is embedded inside iframe which is appended to body of main app initializing Event listener on iframe window.
  3. uploadLoaded callback of redactor editor is executed (code mentioned above). Event listener initialised on main window.
  4. postMessage method is called on iframe window.
  5. iFrame event listener accepts the message, executes the function respondToMessage which sends S3 response to main window by calling postMessage method.
  6. Event listener on main window listens to the postMessage method and executes redactorUpload function.