-
Notifications
You must be signed in to change notification settings - Fork 338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nicer error handling #1024
Nicer error handling #1024
Changes from 13 commits
f31cd42
ab3fed3
bcee6aa
d944140
8f2d964
76f0292
025daaa
3996cda
23d7751
dbee253
5068df6
900bf20
24c4427
4f1d357
256420b
7241abc
75d2fb8
cbee588
e542aa8
a8e2b43
117e85a
8d0b2e0
044cc88
fe69f8e
83fc9f3
1f54933
51c0c98
498de66
7fbb12a
fd39b21
19408b2
ee95bd0
5af62eb
c311bca
696c5b0
7030eb8
06a553c
65fc023
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,10 +16,16 @@ import { getHttpBase, getHttpBaseInternal } from "../shared/env"; | |
import { | ||
ILemmyConfig, | ||
InitialFetchRequest, | ||
IsoData, | ||
IsoDataOptionalSite, | ||
} from "../shared/interfaces"; | ||
import { routes } from "../shared/routes"; | ||
import { favIconPngUrl, favIconUrl, initializeSite } from "../shared/utils"; | ||
import { | ||
ErrorPageData, | ||
favIconPngUrl, | ||
favIconUrl, | ||
initializeSite, | ||
isAuthPath, | ||
} from "../shared/utils"; | ||
|
||
const server = express(); | ||
const [hostname, port] = process.env["LEMMY_UI_HOST"] | ||
|
@@ -70,6 +76,7 @@ server.get("/css/themes/:name", async (req, res) => { | |
res.contentType("text/css"); | ||
const theme = req.params.name; | ||
if (!theme.endsWith(".css")) { | ||
res.statusCode = 400; | ||
res.send("Theme must be a css file"); | ||
} | ||
|
||
|
@@ -109,44 +116,57 @@ server.get("/css/themelist", async (_req, res) => { | |
server.get("/*", async (req, res) => { | ||
try { | ||
const activeRoute = routes.find(route => matchPath(req.path, route)); | ||
const context = {} as any; | ||
let auth: string | undefined = IsomorphicCookie.load("jwt", req); | ||
|
||
const getSiteForm: GetSite = { auth }; | ||
|
||
const promises: Promise<any>[] = []; | ||
|
||
const headers = setForwardedHeaders(req.headers); | ||
const client = new LemmyHttp(getHttpBaseInternal(), headers); | ||
|
||
const { path, url, query } = req; | ||
|
||
// Get site data first | ||
// This bypasses errors, so that the client can hit the error on its own, | ||
// in order to remove the jwt on the browser. Necessary for wrong jwts | ||
let try_site: any = await client.getSite(getSiteForm); | ||
if (try_site.error == "not_logged_in") { | ||
console.error( | ||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie" | ||
); | ||
getSiteForm.auth = undefined; | ||
auth = undefined; | ||
try_site = await client.getSite(getSiteForm); | ||
} | ||
const site: GetSiteResponse = try_site; | ||
initializeSite(site); | ||
|
||
const initialFetchReq: InitialFetchRequest = { | ||
client, | ||
auth, | ||
path: req.path, | ||
query: req.query, | ||
site, | ||
}; | ||
let site: GetSiteResponse | undefined = undefined; | ||
let routeData: any[] = []; | ||
try { | ||
let try_site: any = await client.getSite(getSiteForm); | ||
if (try_site.error == "not_logged_in") { | ||
console.error( | ||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie" | ||
); | ||
getSiteForm.auth = undefined; | ||
auth = undefined; | ||
try_site = await client.getSite(getSiteForm); | ||
} | ||
|
||
if (activeRoute?.fetchInitialData) { | ||
promises.push(...activeRoute.fetchInitialData(initialFetchReq)); | ||
} | ||
if (!auth && isAuthPath(path)) { | ||
res.redirect("/login"); | ||
return; | ||
} | ||
|
||
const routeData = await Promise.all(promises); | ||
site = try_site; | ||
initializeSite(site); | ||
|
||
if (site) { | ||
const initialFetchReq: InitialFetchRequest = { | ||
client, | ||
auth, | ||
path, | ||
query, | ||
site, | ||
}; | ||
|
||
if (activeRoute?.fetchInitialData) { | ||
routeData = await Promise.all([ | ||
...activeRoute.fetchInitialData(initialFetchReq), | ||
]); | ||
} | ||
} | ||
} catch (error) { | ||
routeData = getErrorRouteData(error, site); | ||
} | ||
|
||
// Redirect to the 404 if there's an API error | ||
if (routeData[0] && routeData[0].error) { | ||
|
@@ -155,112 +175,32 @@ server.get("/*", async (req, res) => { | |
if (error === "instance_is_private") { | ||
return res.redirect(`/signup`); | ||
} else { | ||
return res.send(`404: ${removeAuthParam(error)}`); | ||
routeData = getErrorRouteData(error, site); | ||
} | ||
} | ||
|
||
const isoData: IsoData = { | ||
path: req.path, | ||
const isoData: IsoDataOptionalSite = { | ||
path, | ||
site_res: site, | ||
routeData, | ||
}; | ||
|
||
const wrapper = ( | ||
<StaticRouter location={req.url} context={isoData}> | ||
<StaticRouter location={url} context={isoData}> | ||
<App /> | ||
</StaticRouter> | ||
); | ||
if (context.url) { | ||
return res.redirect(context.url); | ||
} | ||
|
||
const eruda = ( | ||
<> | ||
<script src="//cdn.jsdelivr.net/npm/eruda"></script> | ||
<script>eruda.init();</script> | ||
</> | ||
); | ||
|
||
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : ""; | ||
const root = renderToString(wrapper); | ||
const helmet = Helmet.renderStatic(); | ||
|
||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST }; | ||
|
||
const appleTouchIcon = site.site_view.site.icon | ||
? `data:image/png;base64,${sharp( | ||
await fetchIconPng(site.site_view.site.icon) | ||
) | ||
.resize(180, 180) | ||
.extend({ | ||
bottom: 20, | ||
top: 20, | ||
left: 20, | ||
right: 20, | ||
background: "#222222", | ||
}) | ||
.png() | ||
.toBuffer() | ||
.then(buf => buf.toString("base64"))}` | ||
: favIconPngUrl; | ||
|
||
res.send(` | ||
<!DOCTYPE html> | ||
<html ${helmet.htmlAttributes.toString()} lang="en"> | ||
<head> | ||
<script>window.isoData = ${JSON.stringify(isoData)}</script> | ||
<script>window.lemmyConfig = ${serialize(config)}</script> | ||
|
||
<!-- A remote debugging utility for mobile --> | ||
${erudaStr} | ||
|
||
<!-- Custom injected script --> | ||
${customHtmlHeader} | ||
|
||
${helmet.title.toString()} | ||
${helmet.meta.toString()} | ||
|
||
<!-- Required meta tags --> | ||
<meta name="Description" content="Lemmy"> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||
<link | ||
id="favicon" | ||
rel="shortcut icon" | ||
type="image/x-icon" | ||
href=${site.site_view.site.icon ?? favIconUrl} | ||
/> | ||
|
||
<!-- Web app manifest --> | ||
<link rel="manifest" href="data:application/manifest+json;base64,${await generateManifestBase64( | ||
site.site_view.site | ||
)}"> | ||
<link rel="apple-touch-icon" href=${appleTouchIcon} /> | ||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} /> | ||
|
||
<!-- Styles --> | ||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" /> | ||
|
||
<!-- Current theme and more --> | ||
${helmet.link.toString()} | ||
|
||
</head> | ||
|
||
<body ${helmet.bodyAttributes.toString()}> | ||
<noscript> | ||
<div class="alert alert-danger rounded-0" role="alert"> | ||
<b>Javascript is disabled. Actions will not work.</b> | ||
</div> | ||
</noscript> | ||
|
||
<div id='root'>${root}</div> | ||
<script defer src='/static/js/client.js'></script> | ||
</body> | ||
</html> | ||
`); | ||
|
||
res.send(await createSsrHtml(root, isoData)); | ||
} catch (err) { | ||
// If an error is caught here, the error page couldn't even be rendered | ||
console.error(err); | ||
return res.send(`404: ${removeAuthParam(err)}`); | ||
res.statusCode = 500; | ||
return res.send( | ||
process.env.NODE_ENV === "development" ? err.message : "Server error" | ||
); | ||
} | ||
}); | ||
|
||
|
@@ -292,16 +232,6 @@ process.on("SIGINT", () => { | |
process.exit(0); | ||
}); | ||
|
||
function removeAuthParam(err: any): string { | ||
return removeParam(err.toString(), "auth"); | ||
} | ||
|
||
function removeParam(url: string, parameter: string): string { | ||
return url | ||
.replace(new RegExp("[?&]" + parameter + "=[^&#]*(#.*)?$"), "$1") | ||
.replace(new RegExp("([?&])" + parameter + "=[^&]*&"), "$1"); | ||
} | ||
|
||
const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512]; | ||
const defaultLogoPathDirectory = path.join( | ||
process.cwd(), | ||
|
@@ -361,3 +291,116 @@ async function fetchIconPng(iconUrl: string) { | |
.then(res => res.blob()) | ||
.then(blob => blob.arrayBuffer()); | ||
} | ||
|
||
function getErrorRouteData(error: string, site?: GetSiteResponse) { | ||
const errorPageData: ErrorPageData = { type: "error" }; | ||
|
||
// Exact error should only be seen in a development environment. Users | ||
// in production will get a more generic message. | ||
if (process.env.NODE_ENV === "development") { | ||
errorPageData.error = error; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im not sure this makes sense. The reason not to show error messages is usually for security, but anyway the error messages are available if you use the API directly. Another reason might be to keep things simple for non-techie users. But even if they dont understand the error, there can be a text telling them to copy-paste the error message to admins/devs. That can be very helpful for debugging. |
||
|
||
const adminMatrixIds = site?.admins | ||
.map(({ person: { matrix_user_id } }) => matrix_user_id) | ||
.filter(id => id) as string[] | undefined; | ||
if (adminMatrixIds && adminMatrixIds.length > 0) { | ||
errorPageData.adminMatrixIds = adminMatrixIds; | ||
} | ||
|
||
return [errorPageData]; | ||
} | ||
|
||
async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) { | ||
const site = isoData.site_res; | ||
const appleTouchIcon = site?.site_view.site.icon | ||
? `data:image/png;base64,${sharp( | ||
await fetchIconPng(site.site_view.site.icon) | ||
) | ||
.resize(180, 180) | ||
.extend({ | ||
bottom: 20, | ||
top: 20, | ||
left: 20, | ||
right: 20, | ||
background: "#222222", | ||
}) | ||
.png() | ||
.toBuffer() | ||
.then(buf => buf.toString("base64"))}` | ||
: favIconPngUrl; | ||
|
||
const eruda = ( | ||
<> | ||
<script src="//cdn.jsdelivr.net/npm/eruda"></script> | ||
<script>eruda.init();</script> | ||
</> | ||
); | ||
|
||
const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : ""; | ||
|
||
const helmet = Helmet.renderStatic(); | ||
|
||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST }; | ||
|
||
return ` | ||
<!DOCTYPE html> | ||
<html ${helmet.htmlAttributes.toString()} lang="en"> | ||
<head> | ||
<script>window.isoData = ${JSON.stringify(isoData)}</script> | ||
<script>window.lemmyConfig = ${serialize(config)}</script> | ||
|
||
<!-- A remote debugging utility for mobile --> | ||
${erudaStr} | ||
|
||
<!-- Custom injected script --> | ||
${customHtmlHeader} | ||
|
||
${helmet.title.toString()} | ||
${helmet.meta.toString()} | ||
|
||
<!-- Required meta tags --> | ||
<meta name="Description" content="Lemmy"> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||
<link | ||
id="favicon" | ||
rel="shortcut icon" | ||
type="image/x-icon" | ||
href=${site?.site_view.site.icon ?? favIconUrl} | ||
/> | ||
|
||
<!-- Web app manifest --> | ||
${ | ||
site && | ||
`<link | ||
rel="manifest" | ||
href={${`data:application/manifest+json;base64,${await generateManifestBase64( | ||
site.site_view.site | ||
)}`}} | ||
/>` | ||
} | ||
<link rel="apple-touch-icon" href=${appleTouchIcon} /> | ||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} /> | ||
|
||
<!-- Styles --> | ||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" /> | ||
|
||
<!-- Current theme and more --> | ||
${helmet.link.toString()} | ||
|
||
</head> | ||
|
||
<body ${helmet.bodyAttributes.toString()}> | ||
<noscript> | ||
<div class="alert alert-danger rounded-0" role="alert"> | ||
<b>Javascript is disabled. Actions will not work.</b> | ||
</div> | ||
</noscript> | ||
|
||
<div id='root'>${root}</div> | ||
<script defer src='/static/js/client.js'></script> | ||
</body> | ||
</html> | ||
`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lodash seems unused, and I'd def like to avoid it if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lodash
is only being used inwebpack.config.js
. I originally tried having it as a dev dependency, but this caused issues when I tested the prod Dockerfile. This shouldn't be included in the bundle.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah i got this issue too when trying to run docker compose against lemmy-ui