Sunday, May 12, 2013

hash bang URL fragments and http redirects

First there were static pages.
Then came javascript and html forms.
Next came ajax.
Now we are talking about websockets and HTML5.

I remember writing JSP where the server would dynamically generate the content for a client. Today, we are still doing some of this at the server but as our web applications have started getting more sophisticated it has become imperative that we do more of this at the client. RequireJs text plugin is of great help for this. Similarly for a web application that is dynamic - it would be great if it were book-markable too.

Open GMail and then open a particular email, you will notice that the URL looks something like https://mail.google.com/mail/u/0/#inbox/13e9f320434b6c82. Now if you bookmarked that url, what do you think will happen the next time you open it?

The answer depends upon at least two factors - the browser you are using and whether you have automatic sign-in enabled.

If you logged in automatically (because of a saved cookie), you will see the proper email open in almost all browsers.

If you get redirected to the sign-in page, then its a different story. On IE9, you will see a URL like the following:
As you can see the #inbox/13e9f320434b6c82 fragment is lost! So after you sign in you will NOT see the email.

In Chrome, you would instead see: https://accounts.google.com/ServiceLogin?service=mail&passive=true&rm=false&continue=https://mail.google.com/mail/&ss=1&scc=1&ltmpl=default&ltmplcache=2#inbox/13e9f320434b6c82

While browsers do NOT send the '#' and the following string of a URL to the server (that would be a page-reload), most modern browsers will append the fragment to the redirected url. Now is this the right thing to do? It's at least not better than dropping the fragment. Why would it ever be bad in any ways? Answer: If the redirected page also interprets the fragment but in a different way? That would be an accidental collision of semantics.

Getting back to the question though, Chrome still does not open the email after I sign in! This is because the login page doesn't handle the window.hashchange event. If it did (e.g. called history.pushState()), the #fragment will be relayed to the next redirect too and we should be happy to see the email direcly.

Three pieces of advice -
  1. Do not use hashchange event directly but instead use something like jQuery which can simulate hashchange events (by a polling mechanism) for browsers like IE7
  2. Your sign-in page should relay hash fragments
  3. If the sign-in page is NOT under your control - you are mostly out of luck. Let's see how we can defend agains that below.
We could encourage users to not share the URL they see in their address bar directly but instead provide a "SHARE" button, which would generate another URL that captures, as a query-string parameter, the same information that would otherwise be in the #fragment. E.g. http://myapp.com#mypage is converted to http://myapp.com?hash=mypage

Next you would write you app to interpret the "hash" parameter as the hash fragment. But is that good enough? What happens with a url like http://myapp.com?hash=mypage#mycontacts ? Obviously, the fragment must take precedence over the query string. (You see why?)

Wednesday, May 8, 2013

Stupid download of browser self-generated content

HTML5 is here but we got IE 8 to support and then IE 9 is gonna be around for a while too. Often times we might be generating data on the browser that the server is not aware of and need not be aware of, e.g. we might be drawing some doodles and we want to save it and keep a copy locally on our desktop. Now since the content is already here at the browser, why should we not be able to write to a file? Well that's why we have data uri (also see Data_URI_scheme)
But tragically, it doesn't work with most contemporary browsers. We could then use something like downloadify (uses flash though).
So, is there a javascript/html only solution? None so far that works perfectly (Data-uri on Safari and Firefox 19- requires the user to enter a filename and there is no default name support. On IE there is the document.execCommand that you could use but it require pop-up to be enabled and once again you can't suggest a default file name).
Well it might seem very dumb but sacrificing latency and server cpu cycles and just doing a round-trip to the server and using an anchor element seems to be the safest bet:
  var linkEl = this.$('a.export-csv');
  linkEl.click( function _onExportCsv (evt) {
      var csvText = $('.scratch-pad').text();//get the content
      var filename = "mydata"+JSON.stringify(new Date()).replace(/"/g,'');//get a default filename"
      if (/chrome/i.test(navigator.userAgent) || /Mozilla\/5\.0.+\srv:2\d\.\d/i.test(navigator.userAgent)) {
        //this works only on Chrome and FF20+
        linkEl.attr('href',"data:plain/text," + encodeURIComponent(csvText));
        linkEl.attr('download',filename);
        return true; //DO follow the link NOW
      } else {
        var formTemplate = _.template(
        '<form action="<%=downloadEchoUrl%>" method="post">\
          <input type="hidden" name="filename" value="<%-filename%>" />\
          <input type="hidden" name="contentToEcho" value="<%-contentToEcho%>" />\
        &lt/form>');
        //using POST for safety - the payload could be large and IE doesn't suppor long URLs
        $(formTemplate({
          downloadEchoUrl: "http://ustaman.com/echo",
          filename:filename,
          contentToEcho: encodeURIComponent(csvText)
          //note the template will do an escape, use of &lt%- instead of $lt%= with underscore
        })).appendTo('body').submit().remove();
        return false; //do NOT follow the link
      }
  });
And here is a sample "echo" server in Java (one could write a simpler version using node or python or what not).
package com.ustaman;

import java.io.IOException;
import java.net.URLDecoder;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(name = "FileDownloadEchoServlet", asyncSupported = false, urlPatterns = { "/echo/*" }, loadOnStartup = 4)
public class FileDownloadEchoServlet extends HttpServlet {

    private static final long serialVersionUID = 1343508390612726798L;

    private static Logger logger = LoggerFactory
            .getLogger(FileDownloadEchoServlet.class);

    @Override
    /* We could use Spring MVC with @RequestBody for production code */
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    /* We could use Spring MVC with @RequestBody for production code */
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("data/plain");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader(
                "Content-Disposition",
                String.format("attachment; filename=%s",
                        req.getParameter("filename")));
        resp.getOutputStream().write(
                URLDecoder.decode(req.getParameter("contentToEcho"), "UTF-8")
                        .getBytes("UTF-8"));
        resp.getOutputStream().close();
        logger.info("File download echoed");
    }

}