Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't break out of frames when frame missing #863

Merged
merged 3 commits into from
Feb 7, 2023

Conversation

kevinmcconnell
Copy link
Collaborator

This changes the default behaviour when a frame response is missing its expected <turbo-frame> element.

Previously, when the response was missing its frame, we would trigger a turbo:frame-missing event, and then (provided that event wasn't cancelled) perform a full page visit to the requested URL.

However there are cases where the full reload makes things worse:

  • If the frame contents were non-critical, reloading the page can turn a minor bug into a major one.
  • It can mask some bugs where frames were intend to explicitly navigate out of the frame (target="_top"), by incurring a second request that loads the page that makes it seem as if it's working corrects.
  • It leaves the user at a URL that may never be capable of rendering a valid response (since that URL was only intended to serve a particular frame). That means refreshing the page is no help in getting back to a working state.
  • It can lose other temporary state on a page, like form values.

With this change, we no longer perform the full page visit. Instead, we handle a missing frame by doing two things:

  • Write a short error message into the frame, so that the problem is visible on the page.
  • Throw an exception, which should make the problem quite obvious in development, and which allows it to be easily gathered by exception monitoring tools in production.

We keep the turbo:frame-missing event exactly as before, so applications can still hook in to perform alternative behaviour if they want.

@dhh
Copy link
Member

dhh commented Feb 2, 2023

We need the allowlist concept to go along with this, so you can deal with "session expired" cases without having to write a custom turbo:frame-missing handler.

@kevinmcconnell
Copy link
Collaborator Author

@dhh yep, just decided to split that piece into its own PR. We've been chatting about a couple of different ways to specify that allowlist, so I'm opening a 2nd PR for that part now.

@dhh
Copy link
Member

dhh commented Feb 2, 2023

Cools cools 👌

src/core/frames/frame_controller.ts Outdated Show resolved Hide resolved
src/core/frames/frame_controller.ts Outdated Show resolved Hide resolved
This changes the default behaviour when a frame response is missing its
expected `<turbo-frame>` element.

Previously, when the response was missing its frame, we would trigger a
`turbo:frame-missing` event, and then (provided that event wasn't
cancelled) perform a full page visit to the requested URL.

However there are cases where the full reload makes things worse:

- If the frame contents were non-critical, reloading the page can turn a
  minor bug into a major one.
- It can mask some bugs where frames were intend to explicitly navigate
  out of the frame (`target="_top"`), by incurring a second request that
  loads the page that makes it seem as if it's working corrects.
- It leaves the user at a URL that may never be capable of rendering a
  valid response (since that URL was only intended to serve a particular
  frame). That means refreshing the page is no help in getting back to a
  working state.
- It can lose other temporary state on a page, like form values.

With this change, we no longer perform the full page visit. Instead, we
handle a missing frame by doing two things:

- Write a short error message into the frame, so that the problem is
  visible on the page.
- Throw an exception, which should make the problem quite obvious in
  development, and which allows it to be easily gathered by exception
  monitoring tools in production.

We keep the `turbo:frame-missing` event exactly as before, so
applications can still hook in to perform alternative behaviour if they
want.
Also remove `invalidate` that has effectively been replaced.
@kevinmcconnell kevinmcconnell merged commit 91ee8f6 into main Feb 7, 2023
@kevinmcconnell kevinmcconnell deleted the frames/no-breakout branch February 7, 2023 12:06
another-rex referenced this pull request in google/osv.dev Mar 6, 2023
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@hotwired/turbo](https://turbo.hotwired.dev)
([source](https://togithub.com/hotwired/turbo)) | [`7.2.5` ->
`7.3.0`](https://renovatebot.com/diffs/npm/@hotwired%2fturbo/7.2.5/7.3.0)
|
[![age](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.3.0/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.3.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.3.0/compatibility-slim/7.2.5)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/npm/@hotwired%2fturbo/7.3.0/confidence-slim/7.2.5)](https://docs.renovatebot.com/merge-confidence/)
|
| [sass](https://togithub.com/sass/dart-sass) | [`1.58.0` ->
`1.58.3`](https://renovatebot.com/diffs/npm/sass/1.58.0/1.58.3) |
[![age](https://badges.renovateapi.com/packages/npm/sass/1.58.3/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/npm/sass/1.58.3/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/npm/sass/1.58.3/compatibility-slim/1.58.0)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/npm/sass/1.58.3/confidence-slim/1.58.0)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>hotwired/turbo</summary>

### [`v7.3.0`](https://togithub.com/hotwired/turbo/releases/tag/v7.3.0)

[Compare
Source](https://togithub.com/hotwired/turbo/compare/v7.2.5...v7.3.0)

#### What's Changed

- Don't break out of frames when frame missing by
[@&#8203;kevinmcconnell](https://togithub.com/kevinmcconnell) in
[https://github.com/hotwired/turbo/pull/863](https://togithub.com/hotwired/turbo/pull/863)
- Respect `turbo-visit-control` for frame requests by
[@&#8203;kevinmcconnell](https://togithub.com/kevinmcconnell) in
[https://github.com/hotwired/turbo/pull/867](https://togithub.com/hotwired/turbo/pull/867)
- Allow changing the submitter text during form submission by
[@&#8203;afcapel](https://togithub.com/afcapel) in
[https://github.com/hotwired/turbo/pull/869](https://togithub.com/hotwired/turbo/pull/869)
- Form submissions from frames should clear cache by
[@&#8203;kevinmcconnell](https://togithub.com/kevinmcconnell) in
[https://github.com/hotwired/turbo/pull/882](https://togithub.com/hotwired/turbo/pull/882)
- Deprecate `[data-turbo-cache=false]` in favor of
`[data-turbo-temporary]` by
[@&#8203;packagethief](https://togithub.com/packagethief) in
[https://github.com/hotwired/turbo/pull/871](https://togithub.com/hotwired/turbo/pull/871)
- `resume()` does not require `value` by
[@&#8203;dahlbyk](https://togithub.com/dahlbyk) in
[https://github.com/hotwired/turbo/pull/854](https://togithub.com/hotwired/turbo/pull/854)
- Rename `isIdempotent` to `isSafe` by
[@&#8203;kevinmcconnell](https://togithub.com/kevinmcconnell) in
[https://github.com/hotwired/turbo/pull/883](https://togithub.com/hotwired/turbo/pull/883)

**Full Changelog**:
hotwired/turbo@v7.2.5...v7.3.0

</details>

<details>
<summary>sass/dart-sass</summary>

###
[`v1.58.3`](https://togithub.com/sass/dart-sass/blob/HEAD/CHANGELOG.md#&#8203;1583)

[Compare
Source](https://togithub.com/sass/dart-sass/compare/1.58.2...1.58.3)

-   No user-visible changes.

###
[`v1.58.2`](https://togithub.com/sass/dart-sass/blob/HEAD/CHANGELOG.md#&#8203;1582)

[Compare
Source](https://togithub.com/sass/dart-sass/compare/1.58.1...1.58.2)

##### Command Line Interface

-   Add a timestamp to messages printed in `--watch` mode.

- Print better `calc()`-based suggestions for `/`-as-division expression
that
    contain calculation-incompatible constructs like unary minus.

###
[`v1.58.1`](https://togithub.com/sass/dart-sass/blob/HEAD/CHANGELOG.md#&#8203;1581)

[Compare
Source](https://togithub.com/sass/dart-sass/compare/1.58.0...1.58.1)

- Emit a unitless hue when serializing `hsl()` colors. The `deg` unit is
    incompatible with IE, and while that officially falls outside our
compatibility policy, it's better to lean towards greater compatibility.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "before 6am on wednesday" in timezone
Australia/Sydney, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config help](https://togithub.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://app.renovatebot.com/dashboard#github/google/osv.dev).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4xMjUuMSIsInVwZGF0ZWRJblZlciI6IjM0LjE0Ni4yIn0=-->
@jonathanloos
Copy link

jonathanloos commented Mar 14, 2023

Hey there,

Love the idea behind the change, seems like a great way to enforce the correct patterns when implementing frames! However, as bullet number two:

It can mask some bugs where frames were intend to explicitly navigate out of the frame (target="_top"), by incurring a second request that loads the page that makes it seem as if it's working corrects.

This change rendered most of our application essentially unusable as our (seemingly) external links now just return the explained Content missing error blocking our users from navigating throughout the site. Ultimately this is a fault on our end for improper implementation but IMO a very breaking change to introduce in a minor version.

Do you by chance have any strategies for identifying which frames/links will now be broken by this? We just finished rewriting our entire UI with Turbo and it's looking like the only way to make sure is by checking every single one...

Any help is appreciated! As mentioned, we love the change but were not expecting such an impact to our application.

@VGHOCIOT @krsyoung

@kevinmcconnell
Copy link
Collaborator Author

Hi @jonathanloos, yeah, I totally understand! Thanks for bringing this up.

It's hard to make this change without some disruption because the behaviour we want is a bit stricter than what we had in 7.2. (Prior to 7.2 it was stricter too). In 7.2. we have been emitting console warnings to try to highlight where the requests were missing their frames. But it's very easy to miss those, so it's totally understandable that you could end up with links missing frames/targets without noticing. (This situation was a big part of the motivation for the change.)

One idea that might help would be to use a global handler to catch any turbo:frame-missing events, and perform a visit in place of the default handling. That would give you effectively the same behaviour as was in 7.2. If you use an error monitoring service (like Sentry or similar) you could report these occurrences as they happen.

That should keep your app working as it did before, but now you can also see what's coming in to the error monitoring to find any links/frames that need to be updated, and you can make those changes gradually instead of all at once.

Something along these lines should work for that:

document.addEventListener("turbo:frame-missing", function(event) {
  // TODO: Report this event somewhere...

  // Visit the response
  event.preventDefault()
  event.detail.visit(event.detail.response)
})

Hope that helps!

@jonathanloos
Copy link

jonathanloos commented Mar 16, 2023

@kevinmcconnell we'll be taking a look into that! We appreciate the quick response as well 🙏 Definitely need to pay closer attention to the console warnings..

@jonathanloos
Copy link

jonathanloos commented Mar 29, 2023

Hey @kevinmcconnell !

Once again thank you for the idea behind mitigating the issues. Our team has successfully implemented the patch and are tracking the offending DOM elements using Ahoy. That way we can gather info on which turbo frames are offending and use it as a todo list in the future.

For reference and anyone also looking to track, here's the basis of our implementation (very similar to the code above!).

import { Controller } from '@hotwired/stimulus'
import ahoy from 'ahoy.js'

export default class extends Controller {
  connect () {
    document.addEventListener('turbo:frame-missing', this.handleMissingFrame)
  }

  handleMissingFrame (event) {
    this.trackAhoy(event)

    // Visit the response
    event.preventDefault()
    event.detail.visit(event.detail.response)
  }

  trackAhoy (event) {
    ahoy.track('turbo_frame_missing', event.target.id)
  }
}

Note we had to do some solid debouncing and event listener disconnecting in a stimulus controller to make sure we didn't get duplicates all over the place.

Hope that helps! Thanks again.

@kevinmcconnell
Copy link
Collaborator Author

@jonathanloos great! I'm glad to hear this worked out. Thanks for letting me know! 🙏

@jon-sully
Copy link

jon-sully commented Apr 6, 2023

Howdy! Just spent a couple hours integrating this (and its companion) (and/with the Turbo-Rails companion, hotwired/turbo-rails#428) into an app with a partner of mine and we had one particularly subtle hiccup I wanted to note for others / consideration.

In 7.2 (or at least in RC3, which we were running previously), if a frame requests something and the response didn't contain the matching frame, that same response ended up being painted. It wasn't requested again.

I believe the idea with the new API is, "use turbo_page_requires_reload / the raw meta tag equivalent in the <head>" but turbo-visit-control="reload" is made to force a full page load AFAIK — so in that case it's another request but it's actually a heavier request (all assets and resources in the head will be reloaded).

There's this 'escape hatch':

document.addEventListener("turbo:frame-missing", function(event) {
  event.preventDefault()
  event.detail.visit(event.detail.response)
})

Which, based on the way visit is written under the hood, should actually paint that same response's content (nice API btw, this is clean), but a full page load does end up happening.

As we found after a couple hours of digging and experimenting is that this .visit call does do what it was written to: paint that response as-is and not re-request the page... but the problem is that that response was constructed with a minimal layout — one with a <head> that will not match the <head> of the current page and thus will cause Turbo Drive to do its own full-page-reload if you have any turbo-track="reload"s in your view.

In short, the solution we found for a frame 'breaking out' to a full page load, but using the singular response it already received as the content for that new page load (e.g. not requesting again) is to use the escape hatch above plus forcing the layout to be our application layout (which will have a matching head block):

class ApplicationController < ActionController::Base
  # ...
  layout -> { "application" if turbo_frame_request? }
  # ...
end

which should override the custom layout introduced and used by default for frame responses via hotwired/turbo-rails#428.

This is sort of a half Turbo and half Turbo-Rails concern so hopefully this is an okay place to post it, just wanted to put forth this use case for consideration and thoughts as it seemed like the event.detail.visit API was wired up carefully to re-use the same Response object (e.g. not request again) but the Turbo-rails 'minimal-layout' setup is a recipe for having a forced reload no matter what.

jcowhigjr added a commit to jcowhigjr/yelp_search_demo that referenced this pull request Dec 20, 2023
jcowhigjr added a commit to jcowhigjr/yelp_search_demo that referenced this pull request Dec 20, 2023
…javascript error (#402)

* Update Gemfile

* update

* note to remove chromedriver locally for tests to pass

* fix found by consulting  hotwired/turbo#863
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

6 participants