Repository: sveltejs/svelte (2025-06-26T16:54:57Z)

Issue:
Cloudflare's Email Address Obfuscation may cause crashing on hydration
### Describe the bug

[Cloudflare's Email Address Obfuscation](https://developers.cloudflare.com/waf/tools/scrape-shield/email-address-obfuscation/) feature replaces emails in text with something like
```html
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="205744534b4d54474d48601112160e434f4d">[email&nbsp;protected]</a>
```
and then restores before SvelteKit hydration. If there is a text before and/or after the email, it results in multiple text nodes in a row, while only one is expected.

Example of such malformed DOM:
<img width="750" alt="image" src="https://github.com/user-attachments/assets/5b2e223b-bfc7-4299-b4d4-cb4de5c20ced" />


### Reproduction

Found on not mine site on a personal page.

### Logs

```shell
Uncaught (in promise) DOMException: Node.appendChild: Cannot add children to a Text
```


### System Info

```shell
Svelte 5
```


### Severity

annoyance
Hydration crashes when using app via Google Translate (or many other HTML whitespace manipulations)
### Describe the bug

When visiting a non trivial Svelte 5 page using [Google Translate](https://translate.google.com) the correct content flashes before turning into a blank page.

The reason is, that Google Translate inserts whitespace into the DOM. This breaks critical assumptions within the hydration logic. In my case a component tried to access `$.child(x)` while `hydration_node.nodeType === TEXT_NODE`. Which lead to **Uncaught (in promise) DOMException: Node.appendChild: Cannot add children to a Text**.

This issue is closely related to [#15819](https://github.com/sveltejs/svelte/issues/15819), but was not fixed by [PR #15851](https://github.com/sveltejs/svelte/pull/15851).

### Reproduction

I will send a pull request with a failing test and a proposed solution.

### Logs

```shell

```

### System Info

```shell
System:
    OS: Linux 6.6 cpe:/o:nixos:nixos:24.11 24.11 (Vicuna)
    CPU: (16) arm64 Neoverse-N1
    Memory: 20.70 GB / 30.53 GB
    Container: Yes
    Shell: 3.7.1 - /run/current-system/sw/bin/fish
  Binaries:
    Node: 20.19.1 - /nix/store/k770bck0i7d6c8ffc9gb4v2rxj3dvfbs-nodejs-20.19.1/bin/node
    npm: 10.8.2 - /nix/store/k770bck0i7d6c8ffc9gb4v2rxj3dvfbs-nodejs-20.19.1/bin/npm
    pnpm: 9.15.9 - /nix/store/9ah9ki946jl3jx6lcmwvzxcx3irgji29-pnpm-9.15.9/bin/pnpm
  npmPackages:
    svelte: workspace:^ => 5.34.8
```

### Severity

blocking all usage of svelte


Discussion:
I have no idea how we can support these external services that mess about with the HTML output of the SSR payload. Other than suggesting people don't use them or complain to CloudFlare that their implementation is buggy.
I agree that it's an external bug and the best solution is to ask CF to also merge text nodes during the restoring. But it needs a simple public reproduction, and I don't even have a CF account.
According to the [Cloudflare documentation](https://developers.cloudflare.com/waf/tools/scrape-shield/email-address-obfuscation/#prevent-cloudflare-from-obfuscating-email), one can prevent the obfuscation using html comments `<!--email_off-->contact@example.com<!--/email_off-->`

This way, Cloudflare doesn't change the dom, which causes the hydration to fail.

To keep the html comments in the output, use:

```svelte
{@html '<!--email_off-->'}{user.email}{@html '<!--/email_off-->'}
```

Note: For a public-facing app where you actually want the obfuscation to happen, perhaps this workaround is not preferred.

The issue with trying to fix this is that Google could change how translate works at any time without warning, rendering any previous attempts to prevent issues useless. Other DOM-manipulating extensions have similar issues. 
I agree that providing official Google Translate support is unreasonable. However, hydration should not crash for any HTML. Getting a white page, because any middleware changed whitespace in HTML should never be acceptable. I will provide a PR with a failing test, that is completely unrelated to Google. Also, I believe, I have a fix, however I need to further investigate wether that fix makes sense and does not break something else.
PR is opened.
Sorry for hijacking that issue, but I don't understand why hydration can fail at all? If there's a mismatch, then destroy everything and make it from scratch on the client. There should never be a blank page. The client has all the code needed to create a functional app without any hydration.
You are exactly right. This is the core issue. Failing hydration gracefully would be an annoyance. The current state is a - imho severe - bug.

For a reproduction take a look at my pr, it contains a commit with a failing test, because hydration attempts to `appendChild` on a text node. So a trivial fix would be to catch that error and fail hydration. I believe the proposed solution is better, as hydration with it succeeds. And it succeeds as well for another test case related to issue [#15819](https://github.com/sveltejs/svelte/issues/15819), where the fix was to fail gracefully. 



Patch:
diff --git a/.changeset/hip-eagles-yawn.md b/.changeset/hip-eagles-yawn.md
new file mode 100644
index 000000000000..c987e7c7f2eb
--- /dev/null
+++ b/.changeset/hip-eagles-yawn.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: remount at any hydration error
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index ff6844453dcc..3a489f9f927b 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -136,20 +136,28 @@ export function hydrate(component, options) {
 
 		return /**  @type {Exports} */ (instance);
 	} catch (error) {
-		if (error === HYDRATION_ERROR) {
-			if (options.recover === false) {
-				e.hydration_failed();
-			}
-
-			// If an error occured above, the operations might not yet have been initialised.
-			init_operations();
-			clear_text_content(target);
+		// re-throw Svelte errors - they are certainly not related to hydration
+		if (
+			error instanceof Error &&
+			error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/'))
+		) {
+			throw error;
+		}
+		if (error !== HYDRATION_ERROR) {
+			// eslint-disable-next-line no-console
+			console.warn('Failed to hydrate: ', error);
+		}
 
-			set_hydrating(false);
-			return mount(component, options);
+		if (options.recover === false) {
+			e.hydration_failed();
 		}
 
-		throw error;
+		// If an error occured above, the operations might not yet have been initialised.
+		init_operations();
+		clear_text_content(target);
+
+		set_hydrating(false);
+		return mount(component, options);
 	} finally {
 		set_hydrating(was_hydrating);
 		set_hydrate_node(previous_hydrate_node);


Test patch:
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte
new file mode 100644
index 000000000000..70bf63ad9de2
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte
@@ -0,0 +1,1 @@
+<p>nested</p>
\ No newline at end of file
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js
new file mode 100644
index 000000000000..457eeb2201d2
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js
@@ -0,0 +1,8 @@
+import { test } from '../../test';
+
+export default test({
+	errors: [
+		'Failed to hydrate: ',
+		new DOMException("Node can't be inserted in a #text parent.", 'HierarchyRequestError')
+	]
+});
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html
new file mode 100644
index 000000000000..46f8e8a7ace4
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html
@@ -0,0 +1,1 @@
+<main><p>nested</p><!----></main>
\ No newline at end of file
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html
new file mode 100644
index 000000000000..90ca4ef4b8c9
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html
@@ -0,0 +1,2 @@
+<!--[-->
+<main><p>nested</p><!----></main><!--]-->
\ No newline at end of file
diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte
new file mode 100644
index 000000000000..f1f962b95848
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte
@@ -0,0 +1,7 @@
+<script>
+	import Nested from './Nested.svelte';
+</script>
+
+<main>
+	<Nested />
+</main>
\ No newline at end of file