From 7ccfdc1aea564b7d056a2e75385c6534c1729b0b Mon Sep 17 00:00:00 2001
From: Luca <102960244+Luca-Timo@users.noreply.github.com>
Date: Wed, 3 Jun 2026 18:35:46 +0200
Subject: [PATCH] fix(branding): stream favicon bytes directly (Safari ignores
the 302)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The /favicon.ico route 302-redirected to the uploaded file. Firefox/Chrome
follow that, but Safari does NOT reliably follow a redirect for favicon
requests — it falls back to the HTML , i.e. the bundled picpeak
default. Stream the file bytes directly for local /uploads favicons (with a
path-containment guard); only external URLs and the missing-favicon fallback
still redirect. sendFile sets the content-type from the extension.
---
backend/server.js | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/backend/server.js b/backend/server.js
index cfd08923..10d72ac8 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -573,12 +573,21 @@ app.get(
const raw = await getAppSetting('branding_favicon_url', null);
const url = (raw && String(raw).trim()) || null;
if (url) {
- // Absolute URL → redirect as-is. Otherwise it's an /uploads path the
- // backend already serves with the correct content-type + headers.
- const target = /^https?:\/\//i.test(url)
- ? url
- : (url.startsWith('/') ? url : `/uploads/${url.replace(/^uploads\//, '')}`);
- return res.redirect(302, target);
+ // External URL — can't stream the bytes, so redirect (best effort).
+ if (/^https?:\/\//i.test(url)) return res.redirect(302, url);
+ // Local upload → stream the file bytes DIRECTLY rather than 302'ing.
+ // Safari does NOT reliably follow a redirect for favicon requests
+ // (it falls back to the HTML , i.e. the bundled default),
+ // whereas Firefox/Chrome do — so a 302 worked everywhere except
+ // Safari. sendFile sets the right content-type from the extension.
+ const rel = String(url).replace(/^\/+/, '').replace(/^uploads\//, '');
+ const uploadsRoot = path.resolve(path.join(storagePath, 'uploads'));
+ const resolved = path.resolve(path.join(uploadsRoot, rel));
+ // Path containment — never serve outside the uploads dir.
+ if (resolved.startsWith(uploadsRoot + path.sep) && fs.existsSync(resolved)) {
+ res.setHeader('Cache-Control', 'public, max-age=86400');
+ return res.sendFile(resolved);
+ }
}
} catch (error) {
logger.warn('Favicon lookup failed; serving bundled default', { error: error.message });