fix(server): upload filters answer multer once on a throwing callback; positive call-graph gate test

This commit is contained in:
jubnl
2026-09-22 08:10:23 +02:00
parent ae2af05cad
commit de2d13b1af
5 changed files with 72 additions and 15 deletions
+10 -3
View File
@@ -58,8 +58,8 @@ export function filesUploadFileFilter(allowedTypes: AllowedFileTypesService): Op
// detached async function that always answers through `cb` — multer holds
// the file stream until it does (make-middleware.js says as much). A
// rejection refuses the file, so the filter still fails closed. Only the
// await is wrapped in try/catch, so a throw from cb() itself is never
// re-routed into a second cb() call.
// await is wrapped in try/catch; a throw from cb() itself lands in the
// trailing .catch, which logs it and answers multer one final time.
(async () => {
let allowed: string[];
try {
@@ -73,7 +73,14 @@ export function filesUploadFileFilter(allowedTypes: AllowedFileTypesService): Op
reject();
})().catch((err: unknown) => {
logError(`files upload fileFilter: ${err instanceof Error ? err.message : String(err)}`);
cb(err instanceof Error ? err : new Error(String(err)));
// cb() itself is the only thing that can reach here (every await is
// inside the try above); answer once more so multer never hangs, but a
// second throw from cb() means multer already answered — swallow it.
try {
cb(err instanceof Error ? err : new Error(String(err)));
} catch {
/* multer already answered */
}
});
};
}
+10 -3
View File
@@ -88,8 +88,8 @@ export function journeyImageFileFilter(allowedTypes: AllowedFileTypesService): O
// allowed-extension list is now an async read. The decision runs in a
// detached async function that always answers through `cb`; a rejection
// refuses the file, so the filter still fails closed. Only the await is
// wrapped in try/catch, so a throw from cb() itself is never re-routed
// into a second cb() call.
// wrapped in try/catch; a throw from cb() itself lands in the trailing
// .catch, which logs it and answers multer one final time.
(async () => {
let allowed: string[];
try {
@@ -101,7 +101,14 @@ export function journeyImageFileFilter(allowedTypes: AllowedFileTypesService): O
cb(null, true);
})().catch((err: unknown) => {
logError(`journey photo fileFilter: ${err instanceof Error ? err.message : String(err)}`);
cb(err instanceof Error ? err : new Error(String(err)));
// cb() itself is the only thing that can reach here (every await is
// inside the try above); answer once more so multer never hangs, but a
// second throw from cb() means multer already answered — swallow it.
try {
cb(err instanceof Error ? err : new Error(String(err)));
} catch {
/* multer already answered */
}
});
};
}
+1 -1
View File
@@ -62,7 +62,7 @@ export async function getActiveNoticesFor(
// `evaluate` runs inside a .filter(), which cannot await, so the addon flags
// the registry is able to ask about are resolved up front and handed to it as
// a lookup. The ids come from the notices themselves, so every question the
// conditions can ask has an answer here and the fallback is unreachable.
// conditions can ask has an answer here.
const addonFlags = new Map<string, boolean>();
for (const condition of SYSTEM_NOTICES.flatMap(n => n.conditions)) {
if (condition.kind === 'addonEnabled' && !addonFlags.has(condition.addonId)) {