10

Note that I don't think this problem is related to Backbone or JavaScript but it is necessary to include some Backbone code as context on the issue.

The Related Code

I have a client-side Backbone router with a route which takes a parameter called contactId. It looks similar to this:

Backbone.Router.extend({

  routes: {
    "jobs/new?contact_id=:contactId": "newForContact"
  },

  // Fetch the contact and initialize a new job model which 
  // is associated with that contact.
  newForContact: function(contactId) {
    var contact = new Contact(id: contactId);
    contact.fetch({
      success: _.bind(function(model, resp) {
        var job = new Job(contact: contact);
        this.new(job);
      }
    }, this));
  },

  // Show the JobView for the given job.
  new: function(jobModel) {
    view = new JobView(job: jobModel);
    $('body').append(view.render().el);
  }
};

Now I'm trying to use this setup with pushState turned on.

When I hit the route which triggers the newForContact route, everything works as expected. However, if I press the browser's back button at this point, I am served the JSON response from the contact.fetch() method straight out of the browser's cache. No request is sent to the server.

fetch JSON response

The App Logs

You can see this in the Rails app logs. In this part, I visit the route which triggers newForContact.

Started GET "/jobs/new?contact%5Bid%5D=1&contact%5Btype%5D=Customer" for 127.0.0.1 at 2012-10-31 22:41:48 +0000
Processing by JobsController#new as HTML
  Parameters: {"contact"=>{"id"=>"1", "type"=>"Customer"}}
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
  Business Load (0.3ms)  SELECT "businesses".* FROM "businesses" WHERE "businesses"."id" IN (1)
  Rendered shared/_search_form.html.erb (0.3ms)
  Job Load (0.4ms)  SELECT "jobs".* FROM "jobs" WHERE "jobs"."business_id" = 1 ORDER BY created_at desc
  Rendered jobs/_list.html.erb (1.5ms)
  Rendered jobs/index.html.erb within layouts/application (4.1ms)
  Rendered layouts/_head_content.html.erb (0.7ms)
  Rendered layouts/_flash.html.erb (0.0ms)
Cache read: views/jobs/main_nav/d6a805d9b6f285e424f207add4f35595
Read fragment views/jobs/main_nav/d6a805d9b6f285e424f207add4f35595 (0.4ms)
  Rendered layouts/_nav.html.erb (0.6ms)
  Rendered layouts/_header.html.erb (0.7ms)
Completed 200 OK in 12ms (Views: 8.0ms | ActiveRecord: 1.0ms)
Cache read: http://print.dev/customers/1?

You can see that it fetches the contact at this point with a JSON request.

Started GET "/customers/1" for 127.0.0.1 at 2012-10-31 22:41:48 +0000
Processing by CustomersController#show as JSON
  Parameters: {"id"=>"1"}
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 1]]
  Business Load (0.4ms)  SELECT "businesses".* FROM "businesses" WHERE "businesses"."id" IN (1)
  Customer Load (0.2ms)  SELECT "customers".* FROM "customers" WHERE "customers"."business_id" = 1 AND "customers"."id" = $1 LIMIT 1  [["id", "1"]]
  CustomerEmployee Load (0.3ms)  SELECT "customer_employees".* FROM "customer_employees" WHERE "customer_employees"."employer_id" IN (1)
  Job Load (0.4ms)  SELECT "jobs".* FROM "jobs" WHERE "jobs"."contact_type" = 'Customer' AND "jobs"."contact_id" IN (1)
  Invoice Load (0.3ms)  SELECT "invoices".* FROM "invoices" WHERE "invoices"."client_id" IN (1)
  Job Load (0.5ms)  SELECT "jobs".* FROM "jobs" WHERE "jobs"."contact_id" = 1 AND "jobs"."contact_type" = 'Customer' AND "jobs"."state" = 'finished' AND "jobs"."invoice_id" IS NULL
  Rendered customers/show.json.rabl (2.8ms)
Completed 200 OK in 67ms (Views: 3.2ms | ActiveRecord: 2.5ms)

At this point, I would press the browser's back button but no new request is logged at the server.

Rails Environments

This only happens on my staging server (Heroku), not in development. I can recreate it locally by running the app with Pow in the staging environment which has caching turned on in the rails configs.

config.action_controller.perform_caching = true

Note that even with caching turned on, I can't recreate the bug in the development environment.

This issue occurs in Chrome 22.0.1229.94, FF 16.0.2 and Safari 6.0.1. I'm using Rails 3.2.8.

Possibly Related Questions

It seems like this guy was having a very similar problem to me.

Live Sample

If you really want to you can view the problem live on my staging server on Heroku.

Steps to Repro (edit: these don't work any longer since I patched the problem).

  1. Log in here with email: user@example.com and pass: foobar
  2. Visit http://print-staging.herokuapp.com/customers/2. You should see a messed up looking dialog box open up.
  3. Click the small "New job" link in the dialog. The page should change and a new dialog should open.
  4. Click the browser back button.
Community
  • 1
  • 1
David Tuite
  • 22,258
  • 25
  • 106
  • 176

2 Answers2

10

You can add a no-cache header to your response on the server-side, this should instruct the browser to not cache the response:

response.headers["Cache-Control"] = "no-cache"
kabaros
  • 5,083
  • 2
  • 22
  • 35
  • 1
    Ok but why do I have to do that? I actually do want the browser to cache the response most of the time (it probably won't change much). I just don't want the back button to show me the cached response. I think that basically I want to send a JSON request but then when the user presses the back button, I want to make a HTML request rather than a JSON request. – David Tuite Oct 31 '12 at 23:20
  • sorry I don't get what you mean David ... how do you want to use caching but you don't want the back button to show the cached response? – kabaros Oct 31 '12 at 23:23
  • 1
    I guess I need a way to get the JSON GET request to not make an entry in the browsers history stack (despite the fact that I'm using `pushState` with Backbone. That way I could press the back button and it would skip over the JSON request and show the user the request previous to that (this would hopefully be a HTML request should is capable of forming an actual page to show the user). Perhaps this is impossible, I'm not sure? – David Tuite Oct 31 '12 at 23:28
  • ok I see what you mean .. but I notice that clicking New Job launches a new request (as opposed to clicking new Employee), is it even matching that route that you created in Backbone .. it's hard to debug on my side since the code is minified – kabaros Oct 31 '12 at 23:45
  • So basically, when you click "New Job", it does a full HTML request for "/jobs/new". I have modified the `jobs#new` action so that it simply renders the index template for the jobs controller. Once that template is rendered, the JS kicks in and the client side router runs the `newForContact` method you see in the original questions. – David Tuite Oct 31 '12 at 23:54
  • You know, just playing with your solution a bit more.. It actually does work. I expanded it a little [to look like this](http://stackoverflow.com/q/711418/574190) and it does stop the problem. I think that if I can modify it so that it only prevents browser caching of JSON requests then it will be good enough. – David Tuite Nov 01 '12 at 00:02
  • you could always add the no-cache header just to that controller that returns the json – kabaros Nov 01 '12 at 00:19
  • I understand what you're trying to do, but when JS kicks in, is it really hitting the Backbone router, if u just have a simple alert in newController function does it fire? like I said, it is hard for me to tell with minified js – kabaros Nov 01 '12 at 00:21
2

I am not using Backbone, this is just what I have done with AJAX / pushstate


I use history.pushState() in my AJAX responses.

var url = "http://foo.com/path/to/page";
var relative_url = UrlToRelative(url); // some function to convert a URL to relative
var absolute_url = UrlToAbsolute(url); // some function to convert a URL to absolute
history.pushState(
  {
    hash: "#"+relative_url,
    title: document.title,
    initialHref: absolute_url
  },
  document.title,
  url
);

And I have an event listener for onPopState:

window.onpopstate = function(event) {
  if (event.state && event.state.initialHref) {
    $.ajax({
      complete: null,
      data: {},
      dataType: "script",
      success: null,
      type: 'GET',
      url: event.state.initialHref
    });
  }
}

This event listener makes an AJAX request. The only problem is that the screen takes a few seconds to change when going back to pages that take a few seconds to get a server response. And there is not spinner or busy progress indicator.

Teddy
  • 18,357
  • 2
  • 30
  • 42