Skip to content

Commit 3e4f69f

Browse files
authored
fix: encode headers and use charset utf-8 on content-type response headers (#88)
Adds encoding to HTTP headers from the client and also adds `charset=utf-8` to all HTTP `Content-Type` headers to support exotic characters in client component and server function paths and also in outlet names. #84
1 parent e41db2e commit 3e4f69f

File tree

8 files changed

+44
-29
lines changed

8 files changed

+44
-29
lines changed

packages/react-server/client/ClientProvider.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export const streamOptions = (outlet, remote) => ({
243243
)
244244
? {}
245245
: {
246-
"React-Server-Action": id,
246+
"React-Server-Action": encodeURIComponent(id),
247247
},
248248
});
249249
emit(target, url, (err, result) => {
@@ -258,8 +258,8 @@ export const streamOptions = (outlet, remote) => ({
258258
body: formData,
259259
headers: {
260260
accept: "application/json",
261-
"React-Server-Action": id,
262-
"React-Server-Outlet": outlet || PAGE_ROOT,
261+
"React-Server-Action": encodeURIComponent(id),
262+
"React-Server-Outlet": encodeURIComponent(outlet || PAGE_ROOT),
263263
},
264264
}
265265
);
@@ -304,7 +304,9 @@ function getFlightResponse(url, options = {}) {
304304
accept: `text/x-component${
305305
options.standalone && url !== PAGE_ROOT ? ";standalone" : ""
306306
}${options.remote && url !== PAGE_ROOT ? ";remote" : ""}`,
307-
"React-Server-Outlet": options.outlet || PAGE_ROOT,
307+
"React-Server-Outlet": encodeURIComponent(
308+
options.outlet || PAGE_ROOT
309+
),
308310
...options.headers,
309311
},
310312
}),

packages/react-server/lib/handlers/error.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function plainResponse(e) {
104104
return new Response(e?.stack ?? null, {
105105
...httpStatus,
106106
headers: {
107-
"Content-Type": "text/plain",
107+
"Content-Type": "text/plain; charset=utf-8",
108108
...(getContext(HTTP_HEADERS) ?? {}),
109109
},
110110
});
@@ -164,7 +164,7 @@ export default async function errorHandler(err) {
164164
{
165165
...httpStatus,
166166
headers: {
167-
"Content-Type": "text/html",
167+
"Content-Type": "text/html; charset=utf-8",
168168
...(getContext(HTTP_HEADERS) ?? {}),
169169
},
170170
}

packages/react-server/lib/handlers/static.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default async function staticHandler(dir, options = {}) {
5050
if (pathname.startsWith("/@source")) {
5151
return new Response(await readFile(pathname.slice(8), "utf8"), {
5252
headers: {
53-
"content-type": "text/plain",
53+
"content-type": "text/plain; charset=utf-8",
5454
},
5555
});
5656
}
@@ -178,7 +178,10 @@ export default async function staticHandler(dir, options = {}) {
178178
}
179179
return new Response(res, {
180180
headers: {
181-
"content-type": file.mime,
181+
"content-type":
182+
file.mime.includes("text/") || file.mime === "application/json"
183+
? `${file.mime}; charset=utf-8`
184+
: file.mime,
182185
"content-length": file.stats.size,
183186
etag: file.etag,
184187
"cache-control":

packages/react-server/lib/start/ssr-handler.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export default async function ssrHandler(root, options = {}) {
127127
return new Response(e?.stack ?? null, {
128128
...httpStatus,
129129
headers: {
130-
"Content-Type": "text/plain",
130+
"Content-Type": "text/plain; charset=utf-8",
131131
...(getContext(HTTP_HEADERS) ?? {}),
132132
},
133133
});

packages/react-server/server/RemoteComponent.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async function RemoteComponentLoader({ url, ttl, request = {}, onError }) {
1818
Origin: url.origin,
1919
...request.headers,
2020
Accept: "text/html;remote",
21-
"React-Server-Outlet": url.toString(),
21+
"React-Server-Outlet": encodeURIComponent(url.toString()),
2222
},
2323
}).catch((e) => {
2424
(onError ?? getContext(LOGGER_CONTEXT)?.error)?.(e);

packages/react-server/server/redirects.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function redirect(url, status = 302) {
2323
{
2424
status,
2525
headers: {
26-
"content-type": "text/html",
26+
"content-type": "text/html; charset=utf-8",
2727
Location: url,
2828
},
2929
}

packages/react-server/server/render-rsc.jsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,22 @@ export async function render(Component) {
6666
const accept = context.request.headers.get("accept");
6767
const remote = accept.includes(";remote");
6868
const standalone = accept.includes(";standalone") || remote;
69-
const outlet = (
70-
context.request.headers.get("react-server-outlet") ?? "PAGE_ROOT"
71-
).replace(/[^a-zA-Z0-9_]/g, "_");
69+
const outlet = decodeURIComponent(
70+
(
71+
context.request.headers.get("react-server-outlet") ?? "PAGE_ROOT"
72+
).replace(/[^a-zA-Z0-9_]/g, "_")
73+
);
7274

7375
const isFormData = context.request.headers
7476
.get("content-type")
7577
?.includes("multipart/form-data");
7678
let formState;
77-
const serverActionHeader =
78-
context.request.headers.get("react-server-action") ?? null;
79+
const serverActionHeader = decodeURIComponent(
80+
context.request.headers.get("react-server-action") ?? null
81+
);
7982
if (
8083
"POST,PUT,PATCH,DELETE".includes(context.request.method) &&
81-
(serverActionHeader || isFormData)
84+
((serverActionHeader && serverActionHeader !== "null") || isFormData)
8285
) {
8386
let action = async function () {
8487
throw new Error("Server action not found");
@@ -128,7 +131,7 @@ export async function render(Component) {
128131
);
129132
}
130133

131-
if (serverActionHeader) {
134+
if (serverActionHeader && serverActionHeader !== "null") {
132135
const [serverReferenceModule, serverReferenceName] =
133136
serverActionHeader.split("#");
134137
action = async () => {
@@ -157,6 +160,11 @@ export async function render(Component) {
157160
}
158161

159162
const { data, actionId, error } = await action();
163+
const httpStatus = getContext(HTTP_STATUS) ?? {
164+
status: 200,
165+
statusText: "OK",
166+
};
167+
const httpHeaders = getContext(HTTP_HEADERS) ?? {};
160168

161169
if (!isFormData) {
162170
if (error) {
@@ -165,9 +173,10 @@ export async function render(Component) {
165173

166174
return resolve(
167175
new Response(JSON.stringify(data), {
168-
status: 200,
176+
...httpStatus,
169177
headers: {
170-
"content-type": "application/json",
178+
"content-type": "application/json; charset=utf-8",
179+
...httpHeaders,
171180
},
172181
})
173182
);
@@ -182,10 +191,11 @@ export async function render(Component) {
182191
const [result, key] = formState;
183192
return resolve(
184193
new Response(JSON.stringify(result), {
185-
status: 200,
194+
...httpStatus,
186195
headers: {
187-
"React-Server-Action-Key": key,
188-
"content-type": "application/json",
196+
"React-Server-Action-Key": encodeURIComponent(key),
197+
"content-type": "application/json; charset=utf-8",
198+
...httpHeaders,
189199
},
190200
})
191201
);
@@ -281,7 +291,7 @@ export async function render(Component) {
281291
status: responseFromCache.status,
282292
statusText: responseFromCache.statusText,
283293
headers: {
284-
"content-type": "text/x-component",
294+
"content-type": "text/x-component; charset=utf-8",
285295
"cache-control":
286296
context.request.headers.get("cache-control") ===
287297
"no-cache"
@@ -352,7 +362,7 @@ export async function render(Component) {
352362
new Response(stream, {
353363
...httpStatus,
354364
headers: {
355-
"content-type": "text/x-component",
365+
"content-type": "text/x-component; charset=utf-8",
356366
"cache-control":
357367
context.request.headers.get("cache-control") ===
358368
"no-cache"
@@ -406,7 +416,7 @@ export async function render(Component) {
406416
status: responseFromCache.status,
407417
statusText: responseFromCache.statusText,
408418
headers: {
409-
"content-type": "text/html",
419+
"content-type": "text/html; charset=utf-8",
410420
"cache-control":
411421
context.request.headers.get("cache-control") ===
412422
"no-cache"
@@ -500,7 +510,7 @@ export async function render(Component) {
500510
new Response(responseStream, {
501511
...httpStatus,
502512
headers: {
503-
"content-type": "text/html",
513+
"content-type": "text/html; charset=utf-8",
504514
"cache-control":
505515
context.request.headers.get("cache-control") ===
506516
"no-cache"

packages/react-server/server/request.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ export function rewrite(pathname) {
6262
}
6363

6464
export function useOutlet() {
65-
return (
65+
return decodeURIComponent(
6666
getContext(HTTP_CONTEXT)?.request?.headers?.get("react-server-outlet") ??
67-
"PAGE_ROOT"
67+
"PAGE_ROOT"
6868
);
6969
}

0 commit comments

Comments
 (0)