This commit is contained in:
Andras Schmelczer 2026-05-14 22:07:14 +01:00
parent 084117cea8
commit a8de0a614d
36 changed files with 1329 additions and 522 deletions

View file

@ -24,12 +24,17 @@ if (!APP_URL) {
process.exit(1);
}
if (!CACHE_DIR) {
const CACHE_ENABLED = parseOptionalBoolEnv(
"SCREENSHOT_CACHE_ENABLED",
!isDevelopmentAppUrl(APP_URL),
);
if (CACHE_ENABLED && !CACHE_DIR) {
console.error("Error: CACHE_DIR environment variable is required");
process.exit(1);
}
const cache = new ScreenshotCache(CACHE_DIR);
const cache = CACHE_ENABLED ? new ScreenshotCache(CACHE_DIR as string) : null;
const app = express();
app.set("trust proxy", true);
@ -55,6 +60,28 @@ function parseRequiredPositiveIntEnv(name: string): number {
return value;
}
function parseOptionalBoolEnv(name: string, defaultValue: boolean): boolean {
const raw = process.env[name];
if (raw == null || raw === "") return defaultValue;
if (raw === "1" || raw.toLowerCase() === "true") return true;
if (raw === "0" || raw.toLowerCase() === "false") return false;
throw new Error(`${name} must be true or false`);
}
function isDevelopmentAppUrl(rawUrl: string): boolean {
try {
const url = new URL(rawUrl);
return (
url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname === "frontend" ||
url.port === "3001"
);
} catch {
return false;
}
}
function grantScreenshotSlot(): ReleaseScreenshotSlot {
activeScreenshots += 1;
let released = false;
@ -159,24 +186,26 @@ app.get("/screenshot", async (req, res) => {
const { pagePath, qs } = buildScreenshotRequest(
req.query as Record<string, unknown>,
);
if (pagePath !== "/") qs.set("path", pagePath);
// Include auth status in cache key so authenticated screenshots
// (with hexagons outside free zone) are cached separately
const authHeader = req.headers.authorization;
if (authHeader) qs.set("_auth", "1");
const cacheKey = cache.buildKey(qs);
qs.delete("_auth");
qs.delete("path");
// Check cache first
const cached = cache.get(cacheKey);
if (cached) {
res.setHeader("Content-Type", "image/jpeg");
res.setHeader("Cache-Control", "public, max-age=86400");
res.setHeader("X-Cache", "HIT");
cached.pipe(res);
return;
let cacheKey: string | null = null;
if (cache) {
const cacheParams = new URLSearchParams(qs);
if (pagePath !== "/") cacheParams.set("path", pagePath);
// Include auth status in cache key so authenticated screenshots
// (with hexagons outside free zone) are cached separately
if (authHeader) cacheParams.set("_auth", "1");
cacheKey = cache.buildKey(cacheParams);
// Check cache first
const cached = cache.get(cacheKey);
if (cached) {
res.setHeader("Content-Type", "image/jpeg");
res.setHeader("Cache-Control", "public, max-age=86400");
res.setHeader("X-Cache", "HIT");
cached.pipe(res);
return;
}
}
if (!allowScreenshotRequest(req)) {
@ -199,11 +228,11 @@ app.get("/screenshot", async (req, res) => {
const jpeg = await takeScreenshot(url, authHeader);
// Cache it
cache.set(cacheKey, jpeg);
if (cache && cacheKey) cache.set(cacheKey, jpeg);
res.setHeader("Content-Type", "image/jpeg");
res.setHeader("Cache-Control", "public, max-age=86400");
res.setHeader("X-Cache", "MISS");
res.setHeader("Cache-Control", cache ? "public, max-age=86400" : "no-store");
res.setHeader("X-Cache", cache ? "MISS" : "BYPASS");
res.send(jpeg);
} catch (err) {
if (err instanceof ValidationError) {
@ -220,7 +249,8 @@ app.get("/screenshot", async (req, res) => {
const server = app.listen(PORT, () => {
console.log(`Screenshot service listening on port ${PORT}`);
console.log(` APP_URL: ${APP_URL}`);
console.log(` CACHE_DIR: ${CACHE_DIR}`);
console.log(` CACHE_ENABLED: ${CACHE_ENABLED}`);
if (cache) console.log(` CACHE_DIR: ${CACHE_DIR}`);
console.log(` SCREENSHOT_CONCURRENCY: ${SCREENSHOT_CONCURRENCY}`);
console.log(
` SCREENSHOT_RATE_LIMIT: ${RATE_LIMIT_MAX}/${RATE_LIMIT_WINDOW_MS}ms`,