Additional data point: if I modify the page to replace jQuery with native Javascript+DOM, the problem goes away. This seems to be because hitting the 'back' button reverts straight to the previous page as it was (complete with its AJAX updates), rather than refreshing the original HTML and then re-running the Javascript to re-insert the AJAX bits.
You can see the difference simply by inserting <script type="text/javascript" src="/jquery.js"> </script> before <script language="javascript"> function update_bottom() { If you do this, you'll find that pressing 'Back' on your browser causes the AJAX request to be re-issued. So: something about loading jQuery is changing the behaviour of Firefox, even if you don't call any jQuery functions! And yet this still isn't a problem, because the Javascript is fetching a new version of the AJAX page, rather than the wrong cached one with HTML layout. Regards, Brian. ---- 8< ----- # Program to demonstrate an issue with AJAX getting wrong page from cache. # Install ruby and 'gem install sinatra'. Then: # # ruby js-xhr-moz.rb (this file) # # and point web browser to http://127.0.0.1:4567/ require 'rubygems' require 'sinatra' get '/' do head = <<'EOS' <script language="javascript"> function update_bottom() { var request = new XMLHttpRequest(); request.onreadystatechange = function() { if (request.readyState ==4 && request.status == 200) { document.getElementById('bottom').innerHTML = request.responseText; alert("Lower pane updated"); } } request.open('GET', '/docs/1234'); request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); request.send(null); } window.onload = update_bottom; </script> EOS content = <<'EOS' <div id='top'> This is the top pane. The bottom is updated by AJAX. <br /> Click to go to <a href="/docs/1234">the doc page</a>, then click your browser's Back button <br /> Click to <a href="#" onclick="update_bottom();false">update lower pane</a> </div> <hr /> <div id='bottom'> </div> EOS layout(content, head) end get '/docs/:id' do #response.headers['Cache-Control'] = 'max-age=0, private, must- revalidate' #response.headers['Expires'] = 'Mon, 20 Dec 1998 01:00:00 GMT' layout("<div id='docs_#{params[:id]}'>This is document with id # {params[:id]}</div>") end # Apply layout to object unless request is from AJAX def layout(content, head=nil) return content if request.xhr? <<EOS <html> <head> <title>My awesome app</title> #{head} </head> <body> <h1>My awesome app layout page</h1> <hr /> #{content} </body> EOS end