Repository: sveltejs/svelte (JavaScript)
Date: 2022-03-03 00:53:42+00:00
Task type: Bug Fix

Issue:
Hydration Inconsistency:  Escaped linebreaks in attributes
### Describe the bug

When setting an attribute value to a string (eg: `<input type=hidden name="foo" value={bar} />`), if the string include a line break (eg: `\n` or `\r\n`), the HTML sent to the browser results in the the value being escaped.    After hydration occurs, the value is reset to the expected value.

### Reproduction

* Clone https://github.com/moatra/attr_enc_newline
* `npm install`
* `npm run dev`
* Open `localhost:3000` and ensure javascript is enabled on the page.  
    * Continuously submitting the form will not result in excessive escaping.
    * If you spam submit fast enough (ie, before hydration completes), you will see excessive escaping.
* Open `localhost:3000` and ensure javascript is **disabled** on the page (to prevent hydration).
    * Continuously submitting the form will result in excessive escaping.

### Logs

```shell
No log output
```


### System Info

```shell
❯ npx envinfo --system --binaries --browsers --npmPackages "{svelte,@sveltejs/*,vite}"

  System:
    OS: Linux 5.15 Fedora Linux 35 (Workstation Edition)
    CPU: (16) x64 AMD Ryzen 7 3700X 8-Core Processor
    Memory: 20.63 GB / 31.33 GB
    Container: Yes
    Shell: 5.1.8 - /bin/bash
  Binaries:
    Node: 16.13.0 - /usr/bin/node
    npm: 8.1.0 - /usr/bin/npm
  Browsers:
    Firefox: 96.0
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.30 
    @sveltejs/kit: next => 1.0.0-next.288 
    svelte: ^3.44.0 => 3.46.4
```


### Severity

serious, but I can work around it

### Additional Information

_No response_


Issue discussion:
This seems to be a bug in Svelte itself; it's rendering attributes inconsistently between the client and the server.

It looks like when server-side rendering, Svelte JSON-encodes attributes. If you have a string `str === 'multiple\nlines'` and use it in an attribute `<element attr={str}/>`, it gets turned into `<element attr="multiple\nlines"/>`, which means `element.getAttribute('attr') === 'multiple\\nlines' !== str`.

SvelteKit does include the original data as a payload in the server-side rendered page, so the components get the original data back during hydration—that's why it's only messing with the values before hydration (or with javascript disabled).

Solution patch:
diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts
--- a/src/runtime/internal/ssr.ts
+++ b/src/runtime/internal/ssr.ts
@@ -177,7 +177,8 @@ export function create_ssr_component(fn) {
 
 export function add_attribute(name, value, boolean) {
 	if (value == null || (boolean && !value)) return '';
-	return ` ${name}${value === true && boolean_attributes.has(name) ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `"${value}"`}`}`;
+	const assignment = (boolean && value === true) ? '' : `="${escape_attribute_value(value.toString())}"`;
+	return ` ${name}${assignment}`;
 }
 
 export function add_classes(classes) {


Test patch:
diff --git a/test/server-side-rendering/samples/attribute-escaped-quotes/_expected.html b/test/server-side-rendering/samples/attribute-escaped-quotes/_expected.html
--- a/test/server-side-rendering/samples/attribute-escaped-quotes/_expected.html
+++ b/test/server-side-rendering/samples/attribute-escaped-quotes/_expected.html
@@ -1,3 +1,4 @@
 <div
-	foo="&#34;></div><script>alert(42)</script>"
-></div>
\ No newline at end of file
+	foo="&#34;></div>\<script>alert(42)</script>"
+	bar="&#34;></div>\<script>alert(42)</script>"
+></div>
diff --git a/test/server-side-rendering/samples/attribute-escaped-quotes/main.svelte b/test/server-side-rendering/samples/attribute-escaped-quotes/main.svelte
--- a/test/server-side-rendering/samples/attribute-escaped-quotes/main.svelte
+++ b/test/server-side-rendering/samples/attribute-escaped-quotes/main.svelte
@@ -1,5 +1,6 @@
 <script>
-	export let foo = '"></div><script>alert(42)</' + 'script>';
+	export let foo = '"></div>\\<script>alert(42)</' + 'script>';
+	export let bar = { toString: () => '"></div>\\<script>alert(42)<\/script>' };
 </script>
 
-<div foo={foo}></div>
\ No newline at end of file
+<div foo={foo} bar={bar}></div>