Files
zulip/web/server/katex_server.ts
Alex Vandiver c13e3dee24 katex: Replace subprocess call with minimal external service.
Replace a separate call to subprocess, starting `node` from scratch,
with an optional standalone node Express service which performs the
rendering.  In benchmarking, this reduces the overhead of a KaTeX call
from 120ms to 2.8ms.  This is notable because enough calls to KaTeX in
a single message would previously time out the whole message
rendering.

The service is optional because he majority of deployments do not use
enough LaTeX to merit the additional memory usage (60Mb).

Fixes: #17425.
2024-03-15 15:34:12 -07:00

79 lines
2.1 KiB
TypeScript

import crypto from "node:crypto";
import bodyParser from "@koa/bodyparser";
import katex from "katex";
import Koa from "koa";
const host = "localhost";
const port = Number(process.argv[2] ?? "9700");
if (!Number.isInteger(port)) {
throw new TypeError("Invalid port");
}
const shared_secret = process.env.SHARED_SECRET;
if (typeof shared_secret !== "string") {
console.error("No SHARED_SECRET set!");
process.exit(1);
}
const compare_secret = (given_secret: string): boolean => {
try {
// Throws an exception if the strings are unequal length
return crypto.timingSafeEqual(
Buffer.from(shared_secret, "utf8"),
Buffer.from(given_secret, "utf8"),
);
} catch {
return false;
}
};
const app = new Koa();
app.use(bodyParser());
app.use((ctx, _next) => {
if (ctx.request.method !== "POST" || ctx.request.path !== "/") {
ctx.status = 404;
return;
}
if (ctx.request.body === undefined) {
ctx.status = 400;
ctx.type = "text/plain";
ctx.body = "Missing POST body";
return;
}
const given_secret = ctx.request.body.shared_secret;
if (typeof given_secret !== "string" || !compare_secret(given_secret)) {
ctx.status = 403;
ctx.type = "text/plain";
ctx.body = "Invalid 'shared_secret' argument";
return;
}
const content = ctx.request.body.content;
const is_display = ctx.request.body.is_display === "true";
if (typeof content !== "string") {
ctx.status = 400;
ctx.type = "text/plain";
ctx.body = "Invalid 'content' argument";
return;
}
try {
ctx.body = katex.renderToString(content, {displayMode: is_display});
} catch (error) {
if (error instanceof katex.ParseError) {
ctx.status = 400;
ctx.type = "text/plain";
ctx.body = error.message;
} else {
ctx.status = 500;
console.error(error);
}
}
});
app.listen(port, host, () => {
console.log(`Server started on http://${host}:${port}`);
});