jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/395039 )
Change subject: Fix: show original server errors
......................................................................
Fix: show original server errors
When the server encounters an error, show the original error encountered
on the server instead of allowing a client side render which may cause a
different error.
Bug: T181900
Change-Id: I3603e26ffdbb8f0313c71c8c598055639b085a54
---
M package.json
M src/client/index.tsx
A src/common/models/ssr-data.ts
M src/common/pages/error.tsx
M src/common/pages/not-found.tsx
M src/common/router/router.ts
M src/server/components/html-page.tsx
M src/server/index.tsx
M test/server/components/html-page.test.ts
9 files changed, 56 insertions(+), 14 deletions(-)
Approvals:
Jhernandez: Looks good to me, approved
jenkins-bot: Verified
diff --git a/package.json b/package.json
index 4bef766..88be03a 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
"bundlesize": [
{
"path": "dist/public/index.*.js",
- "maxSize": "3.7KB"
+ "maxSize": "3.8KB"
},
{
"path": "dist/public/runtime.*.js",
diff --git a/src/client/index.tsx b/src/client/index.tsx
index 0386958..3ded82a 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -3,6 +3,7 @@
import "wikimedia-ui-base/wikimedia-ui-base.css";
import "./index.css";
import { RouteResponse, newRouter } from "../common/router/router";
+import { SSRData } from "../common/models/ssr-data";
import { WithContext } from "../common/components/with-context";
import { routes } from "../common/router/routes";
@@ -13,6 +14,7 @@
require("preact/debug");
}
+const ssrData: SSRData = (window as any).__SSR_DATA__;
const history = newHistory();
const router = newRouter(routes);
const pageRoot = (_ => {
@@ -43,4 +45,6 @@
// Replace the server rendered root, which does not include CSS, with a styled
// page that manages navigation with History. This enables the single page app
// experience.
-route(location.pathname);
+if (!ssrData.forceSSR) {
+ route(location.pathname);
+}
diff --git a/src/common/models/ssr-data.ts b/src/common/models/ssr-data.ts
new file mode 100644
index 0000000..a881d5b
--- /dev/null
+++ b/src/common/models/ssr-data.ts
@@ -0,0 +1,9 @@
+/**
+ * Server-side rendered data. This data is serialized by the server and
+ * deserialized by the client through the `window.__SSR_DATA__` global which is
+ * set prior to any client script execution.
+ */
+export interface SSRData {
+ /** Forbid client-side rendering of subsequently requested pages. */
+ forceSSR: boolean;
+}
diff --git a/src/common/pages/error.tsx b/src/common/pages/error.tsx
index 1a13979..6c1bbe2 100644
--- a/src/common/pages/error.tsx
+++ b/src/common/pages/error.tsx
@@ -1,25 +1,25 @@
import { h } from "preact";
-import App from "../components/app/app";
import Page from "../components/page/page";
export interface Props {
error: Error;
}
+// Note: this page should not require any client-side JavaScript. It is not
+// re-rendered on the client and so does not allow interactions such as the app
+// menu.
export default {
status: 500,
Component({ error }: Props): JSX.Element {
return (
- <App>
- <Page title={`Unexpected error: ${error.message}`} subtitle="Error
500">
- <p>
- <pre>
- <code>{error.stack}</code>
- </pre>
- </p>
- </Page>
- </App>
+ <Page title={`Unexpected error: ${error.message}`} subtitle="Error 500">
+ <p>
+ <pre>
+ <code>{error.stack}</code>
+ </pre>
+ </p>
+ </Page>
);
}
};
diff --git a/src/common/pages/not-found.tsx b/src/common/pages/not-found.tsx
index 16564a6..ef7fc98 100644
--- a/src/common/pages/not-found.tsx
+++ b/src/common/pages/not-found.tsx
@@ -6,6 +6,10 @@
path: string;
}
+// Note: visually, this page may have some similarities to the generic
+// unexpected error page. However, this module should be kept distinct because
+// it permits interaction and may be rendered client-side, unlike the error
+// generic error page.
export default {
status: 404,
diff --git a/src/common/router/router.ts b/src/common/router/router.ts
index 6030598..0e5f69f 100644
--- a/src/common/router/router.ts
+++ b/src/common/router/router.ts
@@ -54,6 +54,8 @@
);
}
+// todo: can we load this page dynamically instead? Many users will never even
+// see a 404.
function respondNotFound(path: string): Promise<RouteResponse<any>> {
const props: NotFoundProps = { path };
return Promise.resolve({
diff --git a/src/server/components/html-page.tsx
b/src/server/components/html-page.tsx
index 61603ff..12fcdb6 100644
--- a/src/server/components/html-page.tsx
+++ b/src/server/components/html-page.tsx
@@ -2,6 +2,7 @@
import { PRODUCTION } from "../../common/assets/config";
import { asset } from "../../common/assets/manifest";
import { ChildrenProps } from "../../common/components/preact-utils";
+import { SSRData } from "../../common/models/ssr-data";
declare function __non_webpack_require__(name: string): any; //
eslint-disable-line camelcase
export interface Props extends ChildrenProps {
@@ -10,6 +11,7 @@
// Chunk to preload on the HTML. May not be needed if the chunks are already
// included, like the error pages
chunkName?: string;
+ ssrData: SSRData;
}
// The production asset manifest from the public build products or
@@ -23,6 +25,7 @@
export default function HTMLPage({
title = "",
chunkName,
+ ssrData,
children
}: Props): JSX.Element {
// Asset order matters.
@@ -36,6 +39,10 @@
const style = asset("index", "css", manifest);
const favicon = asset("favicon/wikipedia", "ico", manifest);
+ // Serialize the SSR data safely. See:
+ //
https://github.com/reactjs/redux/blob/cda8699/docs/recipes/ServerRendering.md#inject-initial-component-html-and-state
+ const ssrDataString = JSON.stringify(ssrData).replace(/</g, "\\u003c");
+
return (
<html lang="en">
<head>
@@ -46,6 +53,13 @@
{/* Preload the stylesheet before the scripts */}
<link rel="preload" href={style} as="style" />
<link rel="stylesheet" href={style} />
+ {/* Set the global server-side rendered data before scripts execute.
*/}
+ <script
+ type="text/javascript"
+ dangerouslySetInnerHTML={{
+ __html: `window.__SSR_DATA__ = ${ssrDataString}`
+ }}
+ />
{assets.map(path => <link rel="preload" href={path} as="script" />)}
<link rel="shortcut icon" href={favicon} />
</head>
diff --git a/src/server/index.tsx b/src/server/index.tsx
index 2d63976..efcc6c8 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -6,6 +6,7 @@
import { RedirectError } from "../common/http/fetch";
import { routes } from "../common/router/routes";
import { SERVER_PORT, SERVER_URL } from "../common/assets/config";
+import ErrorPage from "../common/pages/error";
import HTMLPage from "./components/html-page";
const server = express();
@@ -19,10 +20,13 @@
server.use("/public", express.static("dist/public"));
function render({ chunkName, Component, props }: RouteResponse<any>) {
+ // When an unexpected error occurs, forbid client re-rendering so that the
+ // original error can be shown.
+ const forceSSR = Component === ErrorPage.Component;
return (
"<!doctype html>" + // eslint-disable-line prefer-template
renderToString(
- <HTMLPage title="" chunkName={chunkName}>
+ <HTMLPage title="" chunkName={chunkName} ssrData={{ forceSSR }}>
<Component {...props} />
</HTMLPage>
)
diff --git a/test/server/components/html-page.test.ts
b/test/server/components/html-page.test.ts
index a9b1243..ef48709 100644
--- a/test/server/components/html-page.test.ts
+++ b/test/server/components/html-page.test.ts
@@ -7,6 +7,7 @@
const vNode = HTMLPage({
title: "Test",
chunkName: "",
+ ssrData: { forceSSR: false },
children: ["body"]
});
const html = render(vNode);
@@ -18,7 +19,11 @@
});
it("contains a <title/> when rendered", () => {
- const vNode = HTMLPage({ title: "Test", chunkName: "" });
+ const vNode = HTMLPage({
+ title: "Test",
+ chunkName: "",
+ ssrData: { forceSSR: false }
+ });
const html = render(vNode);
const expected = "<title>Test - Marvin</title>";
assert.ok(
--
To view, visit https://gerrit.wikimedia.org/r/395039
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I3603e26ffdbb8f0313c71c8c598055639b085a54
Gerrit-PatchSet: 8
Gerrit-Project: marvin
Gerrit-Branch: master
Gerrit-Owner: Niedzielski <[email protected]>
Gerrit-Reviewer: Jhernandez <[email protected]>
Gerrit-Reviewer: Niedzielski <[email protected]>
Gerrit-Reviewer: Sniedzielski <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits