html icon indicating copy to clipboard operation
html copied to clipboard

Specify a suggested limit for repeated `replaceState()` calls

Open zcorpan opened this issue 2 months ago • 10 comments

What is the issue with the HTML Standard?

In https://bugzilla.mozilla.org/show_bug.cgi?id=1995636 Firefox ran into a web compat issue due to replaceState() throwing after reaching the UA-defined limit of repeated calls. I think Gecko's current limit is 200 replaceStates (or location.hash changes; see #11410) in 10 seconds.

WebKit's limit is 100. Chromium's limit is 200, but after that it no-ops instead of throwing.

We're considering increasing the limit to 1000. If there's interest in aligning on the number we can codify it in the spec.

zcorpan avatar Oct 28 '25 21:10 zcorpan

Does increasing the limit meaningfully improve the experience of end users? It sounds like resource abuse to me. If not throwing works when the limit is reached that seems like a much more practical solution.

cc @beidson

annevk avatar Oct 29 '25 07:10 annevk

It seems the limit has been hit (unintentionally). Note that this limit is separate to how many entries are stored in history. Calling pushState many times is trivial in terms of resource use, but a limit is needed to stop e.g. infinite loops.

Not throwing results in silent data loss which could be quite frustrating for developers to figure out what is going on.

Cc @noamr

zcorpan avatar Nov 05 '25 23:11 zcorpan

I think it's unnecessary to throw. I voiced this when we originally spec'ed this but aligned with the group at the time.

Updating the history in these cases is often a progressive enhancement and failing it silently is a better outcome to users than erroring the whole thing.

Highlighting also that this is pretty old behavior so some sites in the wild might not have active developers who would be alerted by these errors.

noamr avatar Nov 06 '25 08:11 noamr

Not throwing means unreliable API and in order for the caller to know if the method call succeeded or not, one needs to always check what value is stored in history.state after replace/pushState call. That seems rather weird and error prone API. Two implementations already throw and we've seen very few issues with the throwing behavior, and would see less if all the implementations followed the current spec.

The following will start logging false in Chrome after 200. That is just very surprising logic. Caller thinks we're in some state Y, when we actually are still in X. data:text/html,

smaug---- avatar Nov 06 '25 12:11 smaug----

Not throwing means unreliable API and in order for the caller to know if the method call succeeded or not, one needs to always check what value is stored in history.state after replace/pushState call. That seems rather weird and error prone API.

I agree, if this was a brand new API... But it's somewhat of a legacy API that likely has more unmaintained usage in the wild than usage that has active developers looking at it and responding to error throwing.

Two implementations already throw and we've seen very few issues with the throwing behavior, and would see less if all the implementations followed the current spec.

Judging by this issue, perhaps this is not the case?

The following will start logging false in Chrome after 200. That is just very surprising logic. Caller thinks we're in some state Y, when we actually are still in X. data:text/html,

btw I've just landed the throwing behavior in chromium behind an enabled-by-default flag, and we'll see what that brings up once M144 is out.

I'm guessing it would break sites on chromium that previously broke on firefox/safari without the developers knowing about it.

noamr avatar Nov 06 '25 12:11 noamr

We've seen very few (but not zero) issues due to this.

I think we should consider future websites also. If browsers all throw, it's more likely to be caught and fixed during development. If we all switch to no-op, the limit is less likely to be noticed during development and more sites may end up in confused/broken states for users.

btw I've just landed the throwing behavior in chromium behind an enabled-by-default flag, and we'll see what that brings up once M144 is out.

Great, thanks!

zcorpan avatar Nov 06 '25 13:11 zcorpan

So is the idea to attempt to standardize on 200 as the limit with throwing?

annevk avatar Nov 19 '25 07:11 annevk

So is the idea to attempt to standardize on 200 as the limit with throwing?

SGTM

noamr avatar Nov 19 '25 08:11 noamr

This has been re-evaluated, and discussed on the blink channels. https://bugzilla.mozilla.org/show_bug.cgi?id=1995636 makes us feel that #11108 isn't safe in terms of web compatibility. It seems to me that it's a reported bug that might represent many unreported broken websites. Considering this, it would be safer to either:

  1. For WebKit/Gecko to align with the existing chromium behavior (reverting #11169) .
  2. To carefully monitor this scenario with use-counters and assess the possible damage of aligning on a throwing behavior, and perhaps find a threshold that doesn't break existing potentially-unmaintained websites.

My sense is that the argument for throwing an error here would be stronger if the history API was a recently introduced API rather than legacy that is in the process of being replaced by the navigation API. Since it's more of an about-to-be-legacy API, throwing here would mainly affect legacy sites that are not going to fix it, rather than help alert developers of new sites of an error.

Happy to discuss this at WHATNOT as well.

noamr avatar Nov 21 '25 16:11 noamr

The other option available which I don't love, is to do nothing, since technically all browsers follow the looseness of the spec. It's not the best, but it is intentionally baked in, allowing no browsers to change.

domfarolino avatar Nov 25 '25 19:11 domfarolino