You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Hydration: detect common mismatch patterns and produce a better error message #56392
Which @angular/* package(s) are relevant/related to the feature request?
core
Description
During hydration, Angular relies on the application structure to be the same between the server and the client. There are a few cases where if you have a component template that does not have valid HTML structure, this could result in a DOM mismatch error during hydration, see https://angular.dev/guide/hydration#valid-html-structure.
Here is an example that we've came across on ADEV: #56390.
Proposed solution
The proposal is to auto-detect those situations when hydration mismatch happens and output additional information into the console, to make it easier to find the origin of the problem and resolve it.
Reacted by Akash Patel, robertIsaac, Dawid Kostka, Aurélien, Michael, Paul V., Sylvain Dumont and Jaime Burgos
As #56712 was merged here, I just wanted to note that it is important to detect these mismatches in production too as 3rd party scripts might interfere (proxies, browser extensions, ...).
As developers have generally no control over these (e.g. user browser extensions), hydration should recover when mismatches happen in production.
the error message is very ambiguous
as in #57063 it took me over 10 hours of debugging to be able to reproduce the problem, just to know it's because p can't have nested dev
I didn't understand at all what was the problem and my only thought it must be internal issue
#56712 (now locked) raised this concern: "Angular could bind data or apply event listeners to the wrong element during hydration" when third-party interference adds/removes DOM nodes before hydration runs, and a comment here noted it's "important to detect these mismatches in production too as 3rd party scripts might interfere."
I went looking for the current state of this on Angular 22.0.1 (latest stable as of this comment, also the version that fixed GHSA-rgjc-h3x7-9mwg) and can confirm the exact misbind described in #56712 still reproduces, with a concrete repro and click-level evidence rather than the general description.
Setup: a plain Angular 22 SSR app (ng new --ssr, provideClientHydration(), nothing custom), a component with five static sibling rows, each with its own (click) handler:
No *ngFor, no *ngIf, no <ng-container> - just plain sibling elements, which is what most of the "interference" reports above describe (browser extensions, ad widgets, CDN/edge rewrites all insert plain elements like this, not structural-directive content).
To simulate the interference without needing a real browser extension, I added an Express middleware in server.ts that rewrites the SSR response before sending it, inserting one extra same-shape <li> as the first child of the list:
This stands in for any of: a browser extension, an ad network's in-feed unit, a CDN/edge HTML rewrite, a CMS-injected banner, or a race with the app's own async-appended content. None of these require DOM clobbering, an id collision, or a sanitizer bypass - the injected element only needs to be a plausible same-tag, same-child-shape sibling, which ordinary ads/widgets are anyway.
I drove this with a real Chromium (Playwright), production build (ng build, not --configuration development), loading the page fresh and clicking the button inside whichever row's text matches a given name:
Output (production build shown; development build produces the same result):
Clicked "Alice" -> log="deleted-user-id:2" expected="deleted-user-id:1" | no console errors
Clicked "Bob" -> log="deleted-user-id:3" expected="deleted-user-id:2" | no console errors
Clicked "Carol" -> log="deleted-user-id:4" expected="deleted-user-id:3" | no console errors
Clicked "Dave" -> log="deleted-user-id:5" expected="deleted-user-id:4" | no console errors
Clicked "Eve" -> log="none" expected="deleted-user-id:5" | no console errors
Every row's button fires the next row's handler. No NG0500, no warning, nothing in the console, in either a production or development build. A user who looks at the page and clicks "Delete" next to Carol's name deletes Dave instead, with no indication anything went wrong.
For comparison, I also tried the same single-extra-sibling interference against a *ngFor-based version of the same list (<li *ngFor="let user of users()">). That case behaves differently: in a development build it throws a clear NG0500 ("expected a comment node but found <li>") pointing at the container anchor, consistent with the error-message work merged in #68165. In a production build it partially recovers - most rows end up correct because the row's displayed text and its (click) handler are bound from the same iteration context and move together, but the list visibly breaks (the last item gets duplicated, with the original instance lacking its event listener). So structural-directive content fails loud or fails visibly; the plain-sibling case fails completely silently, which seems like the more dangerous of the two and the one this issue's line "Angular could bind data or apply event listeners to the wrong element during hydration" is really describing.
Repro is a stock ng new --ssr app plus the four small changes above (app.ts, app.html, server.ts, the Playwright script) - happy to push it to a public repo if that's useful, or trim it down further if it helps narrow down a fix. Tested on @angular/core 22.0.1 / @angular/cli 22.0.1, Node 25.6.0, Chromium 149 (Playwright 1.61.0), Windows.
Activity
JeanMeche commentedon Jun 11, 2024
Other example that have been reported in the past :
tbodyin HTML thows a hydration missmatch error. #54943yjaaidi commentedon Jun 26, 2024
As #56712 was merged here, I just wanted to note that it is important to detect these mismatches in production too as 3rd party scripts might interfere (proxies, browser extensions, ...).
As developers have generally no control over these (e.g. user browser extensions), hydration should recover when mismatches happen in production.
robertIsaac commentedon Jul 21, 2024
the error message is very ambiguous
as in #57063 it took me over 10 hours of debugging to be able to reproduce the problem, just to know it's because
pcan't have nesteddevI didn't understand at all what was the problem and my only thought it must be internal issue
refactor(core): add actionable hydration mismatch hints
VenkatKwest commentedon Jun 16, 2026
Hi @alan-agius4,
#56712 (now locked) raised this concern: "Angular could bind data or apply event listeners to the wrong element during hydration" when third-party interference adds/removes DOM nodes before hydration runs, and a comment here noted it's "important to detect these mismatches in production too as 3rd party scripts might interfere."
I went looking for the current state of this on Angular 22.0.1 (latest stable as of this comment, also the version that fixed GHSA-rgjc-h3x7-9mwg) and can confirm the exact misbind described in #56712 still reproduces, with a concrete repro and click-level evidence rather than the general description.
Setup: a plain Angular 22 SSR app (
ng new --ssr,provideClientHydration(), nothing custom), a component with five static sibling rows, each with its own(click)handler:No
*ngFor, no*ngIf, no<ng-container>- just plain sibling elements, which is what most of the "interference" reports above describe (browser extensions, ad widgets, CDN/edge rewrites all insert plain elements like this, not structural-directive content).To simulate the interference without needing a real browser extension, I added an Express middleware in
server.tsthat rewrites the SSR response before sending it, inserting one extra same-shape<li>as the first child of the list:This stands in for any of: a browser extension, an ad network's in-feed unit, a CDN/edge HTML rewrite, a CMS-injected banner, or a race with the app's own async-appended content. None of these require DOM clobbering, an
idcollision, or a sanitizer bypass - the injected element only needs to be a plausible same-tag, same-child-shape sibling, which ordinary ads/widgets are anyway.The resulting HTML sent to the browser:
I drove this with a real Chromium (Playwright), production build (
ng build, not--configuration development), loading the page fresh and clicking the button inside whichever row's text matches a given name:Output (production build shown; development build produces the same result):
Every row's button fires the next row's handler. No
NG0500, no warning, nothing in the console, in either a production or development build. A user who looks at the page and clicks "Delete" next to Carol's name deletes Dave instead, with no indication anything went wrong.For comparison, I also tried the same single-extra-sibling interference against a
*ngFor-based version of the same list (<li *ngFor="let user of users()">). That case behaves differently: in a development build it throws a clearNG0500("expected a comment node but found<li>") pointing at the container anchor, consistent with the error-message work merged in #68165. In a production build it partially recovers - most rows end up correct because the row's displayed text and its(click)handler are bound from the same iteration context and move together, but the list visibly breaks (the last item gets duplicated, with the original instance lacking its event listener). So structural-directive content fails loud or fails visibly; the plain-sibling case fails completely silently, which seems like the more dangerous of the two and the one this issue's line "Angular could bind data or apply event listeners to the wrong element during hydration" is really describing.Repro is a stock
ng new --ssrapp plus the four small changes above (app.ts,app.html,server.ts, the Playwright script) - happy to push it to a public repo if that's useful, or trim it down further if it helps narrow down a fix. Tested on@angular/core22.0.1 /@angular/cli22.0.1, Node 25.6.0, Chromium 149 (Playwright 1.61.0), Windows.