hotwired / turbo

The speed of a single-page web application without having to write any JavaScript
https://turbo.hotwired.dev
MIT License
6.54k stars 415 forks source link

Script tags are reexecuted when navigating back from a page with an error status code using browser's back button #1249

Open hoshiumiarata opened 2 months ago

hoshiumiarata commented 2 months ago

I ran into this issue when implementing error pages in a Rails application. When you navigate to a page with an error status code (e.g., 404) from a page with a status code of 200, and then navigate back using the browser's back button, the script tags are reexecuted. This leads to a situation where some JavaScript libraries (e.g., Stimulus) initialize multiple times and do not work correctly. There is no such behavior when navigating between pages with a status code of 200 or when not using Turbo, so it seems like a bug to me.

Minimal sample code to reproduce the issue (using Express.js, but can be reproduced with any server-side framework):

const express = require('express')
const app = express()
const port = 8888

app.get('/', (req, res) => {
  res.send(`
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Hello</title>
  <script type="module">
    import "https://unpkg.com/@hotwired/turbo@8.0.4/dist/turbo.es2017-esm.js";
  </script>
  <script type="module">
    import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stimulus.js";
    window.Stimulus = Application.start();
    Stimulus.debug = true;

    Stimulus.register("hello", class extends Controller {
      connect() {
        alert("hello connect");
      }
    })
  </script>
</head>
<body>
  <div data-controller="hello"></div>
  <a href="/404">to 404</a>
</body>
</html>
  `)
});

app.get('*', function (req, res) {
  res.status(404).send("404");
});

app.listen(port, () => {
  console.log(`Listening on port ${port}`)
})

Every time you navigate back to the page with the status code of 200, the alert message is displayed n + 1 times, where n is the number of times you navigated to the page with the status code of 404.

tpaulshippy commented 1 month ago

Looks like if you add data-turbo-track="reload" to your script tag that loads Turbo (the first one), you will no longer get your n+1 issue. This technique is mentioned here.

The difference between the behavior when hitting a 404 and hitting a 200 appears to be because of the browser feature called the Back/forward cache. As indicated here you can disable this feature by adding the following line to your example (not generally recommended):

app.get('/', (req, res) => {
  res.setHeader('Cache-Control', "no-cache, no-store, must-revalidate")
  res.send(`
  ...

If you do this, you will see that the "hello connect" alert shows up whether you navigate to a 404 or a 200 and then back. Without this, the browser will restore the initial page with the bfcache when you go back to it from a 200, whereas it will fully load the initial page when you go back to it from a 404. You can see this in your Chrome dev console if you enable "Preserve log":

image

(this message is not shown when navigating back after hitting a 404)