Bug 314976
| Summary: | [Navigation API] Private Browsing leaves navigation.currentEntry stale after intercepted same-origin cross-document navigation | ||
|---|---|---|---|
| Product: | WebKit | Reporter: | k8o <kosakanoki> |
| Component: | DOM | Assignee: | Nobody <webkit-unassigned> |
| Status: | NEW | ||
| Severity: | Normal | CC: | basuke, cdumez, rupin |
| Priority: | P2 | ||
| Version: | Safari 26 | ||
| Hardware: | Unspecified | ||
| OS: | Unspecified | ||
k8o
## Summary
In Private Browsing (Reproduced on 26.3.1 and 26.4), after `NavigateEvent.intercept({ handler })`
commits an intercepted same-origin cross-document navigation:
- `location.href` is updated to the new URL (correct)
- `navigation.currentEntry.url` and `.id` are NOT updated — they still
reference the previous entry
- `currententrychange` event does NOT fire
- `navigatesuccess` event fires, but `navigation.currentEntry` still
reports the previous entry at that point
The same code works correctly in non-private Safari 26.4 and in Chrome
(any mode), so this is specific to WebKit Private Browsing.
This affects routing code that follows the Navigation API contract by
reading `navigation.currentEntry` (and listening to `currententrychange`)
after an intercepted navigation commits.
## Steps to Reproduce
Save the HTML below as `index.html` and serve it from an HTTP origin,
for example:
```
python3 -m http.server 8000
```
Then open `http://localhost:8000/index.html` in a Safari **Private
Browsing** window. The `./next` resource does **not** need to exist; the
navigation is same-origin and intercepted before loading a new document.
```html
<!DOCTYPE html>
<html>
<body>
<a id="link" href="./next">click me</a>
<pre id="log"></pre>
<script>
const $log = document.getElementById("log");
const out = (label, data) => {
const line = label + " " + JSON.stringify(data);
$log.textContent += line + "\n";
console.log(line);
};
if (!("navigation" in window)) {
out("unsupported", { ua: navigator.userAgent });
}
navigation.addEventListener("navigate", (event) => {
const beforeId = navigation.currentEntry?.id;
out("navigate", {
dest: event.destination.url,
canIntercept: event.canIntercept,
sameDocument: event.destination.sameDocument,
navigationType: event.navigationType,
beforeCurrentUrl: navigation.currentEntry?.url,
});
if (!event.canIntercept) return;
event.intercept({
handler: async () => {
out("handler", {
locationHref: location.href,
currentEntryUrl: navigation.currentEntry?.url,
currentEntryId: navigation.currentEntry?.id,
currentEntryUnchanged: navigation.currentEntry?.id === beforeId,
});
},
});
});
navigation.addEventListener("currententrychange", (e) => {
out("currententrychange", {
navigationType: e.navigationType,
currentEntryUrl: navigation.currentEntry?.url,
});
});
navigation.addEventListener("navigatesuccess", () => {
out("navigatesuccess", {
locationHref: location.href,
currentEntryUrl: navigation.currentEntry?.url,
currentEntryId: navigation.currentEntry?.id,
});
});
</script>
</body>
</html>
```
Click "click me". The browser attempts a same-origin cross-document
navigation to `./next`; the script intercepts it via
`event.intercept({ handler })`.
## Expected (matches non-private Safari behavior and Chrome)
```
navigate beforeCurrentUrl: ".../index.html"
currententrychange navigationType:"push", currentEntryUrl:".../next"
handler currentEntryUrl:".../next", currentEntryUnchanged:false
navigatesuccess locationHref:".../next", currentEntryUrl:".../next"
```
## Actual (Safari 26.4 Private Browsing)
```
navigate beforeCurrentUrl: ".../index.html"
handler locationHref:".../next",
currentEntryUrl:".../index.html",
currentEntryUnchanged:true
navigatesuccess locationHref:".../next",
currentEntryUrl:".../index.html" (STALE)
```
`currententrychange` never fires. `navigation.currentEntry` continues to
return the previous entry — both `.url` and `.id` are stale — for the
rest of the document's lifetime, until a reload occurs.
## Environment
- Safari 26.3.1 on macOS Tahoe 26.3.1
- Safari 26.4 on iOS 26.4.2
- Reproduces in macOS Safari Private Browsing windows
- Reproduces in iOS Safari Private tabs
- Does NOT reproduce in macOS / iOS Safari normal windows
- Does NOT reproduce in Chrome (any mode including Incognito)
## Related Bugs
- Bug 298466 — `[Navigation API] Push operation does not always create a
new history item`. Appears related because it also concerns Navigation
API push/entry creation for intercepted navigations. This report is
narrower: it reproduces only in Private Browsing, uses a different
destination URL, and additionally observes that `location.href` updates
while `navigation.currentEntry` and `currententrychange` remain
stale/missing.
## Impact
- The user-visible symptom is "URL bar changes but page content stays the
old one until reload" in Private Browsing for applications that use the
Navigation API for client-side routing.
- Any application following the documented Navigation API SPA pattern
(read `navigation.currentEntry` on `currententrychange` to update the
view) is affected.
## Workaround for Web Developers
Derive the current URL from `location.href` in Private Browsing; do not
rely on `navigation.currentEntry.url` or the `currententrychange` event
for this case. (Subscribing to `navigatesuccess` does not help on its
own because `navigation.currentEntry` is still stale at that point.)
| Attachments | ||
|---|---|---|
| Add attachment proposed patch, testcase, etc. |