Copilot/fix http login issue#3
Conversation
…-camera feat: add Home Assistant add-on (ha-addon/flashforge-dashboard)
…sitory fix: avoid filesystem read in wildcard route handler
Bump Home Assistant base image from nodejs:20 to nodejs:22
Use public Node base image and drop deprecated build.yaml for add-on builds
fix: use HA base image to resolve /run.sh not found on container start
…error fix: fast-fail printer timeout + surface connection errors in the UI
…hboard Fix HA ingress API path resolution and prevent file-list JSON parse failures
Decouple camera stream from 4s polling and optimize dashboard for mobile-first layout
…ing-camera UI redesign: single-column layout, direct MJPEG camera stream, pairs grid
Refactor server.js to improve readability and structure.
…-issues Restore server.js: MQTT Discovery, Ingress injection, and API routes lost in refactor
Fix frontend bootstrap regression preventing API reads
Added 'mode=mse' parameter to WebSocket URL for stream.
Removed fetch calls for camera control actions.
Restore dashboard data flow by fixing app.js parse error
There was a problem hiding this comment.
Code Review
This pull request transitions the repository from a documentation-focused structure to a Home Assistant Add-on repository hosting the FlashForge Dashboard. The new implementation includes a Node.js backend and a frontend dashboard to monitor and control FlashForge printers, featuring live camera streaming, MQTT integration, and GCode uploading. The code review identified several important issues to address: a potential socket leak in the MJPEG proxy, a lack of error handling before parsing JSON in the frontend's status polling, brittle URL parsing when the protocol is omitted, a risk of overlapping polling requests from using setInterval, and a potential header injection vulnerability during file uploads due to unsanitized filenames.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const proxyReq = http.request( | ||
| { host, port, path: upstreamPath, method: 'GET' }, | ||
| (proxyRes) => { | ||
| if (proxyRes.statusCode !== 200) { | ||
| res.status(proxyRes.statusCode || 502).send('Upstream stream not found'); | ||
| proxyRes.resume(); // drain so the socket is released | ||
| return; | ||
| } | ||
|
|
||
| // Forward Content-Type exactly as go2rtc sends it (includes boundary). | ||
| res.setHeader( | ||
| 'Content-Type', | ||
| proxyRes.headers['content-type'] || 'multipart/x-mixed-replace; boundary=ffboundary', | ||
| ); | ||
| res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); | ||
| res.setHeader('Pragma', 'no-cache'); | ||
| res.setHeader('Expires', '0'); | ||
| res.setHeader('Connection', 'keep-alive'); | ||
| // Tell nginx (used by HA Ingress) not to buffer this streaming response. | ||
| res.setHeader('X-Accel-Buffering', 'no'); | ||
| res.flushHeaders(); // send headers immediately, before any body data | ||
|
|
||
| // Pipe every chunk straight to the browser without buffering. | ||
| proxyRes.pipe(res, { end: true }); | ||
|
|
||
| // If the browser disconnects, tear down the upstream request too. | ||
| req.on('close', () => proxyReq.destroy()); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
There is a potential resource leak in the MJPEG proxy. The req.on('close') listener is registered inside the http.request callback. If the client (browser) disconnects before the upstream server starts responding, the req.on('close') listener is never registered, and the upstream request proxyReq is never destroyed. This can lead to hanging sockets and socket exhaustion under load. Register the listener immediately after creating the request.
const proxyReq = http.request(
{ host, port, path: upstreamPath, method: 'GET' },
(proxyRes) => {
if (proxyRes.statusCode !== 200) {
res.status(proxyRes.statusCode || 502).send('Upstream stream not found');
proxyRes.resume(); // drain so the socket is released
return;
}
// Forward Content-Type exactly as go2rtc sends it (includes boundary).
res.setHeader(
'Content-Type',
proxyRes.headers['content-type'] || 'multipart/x-mixed-replace; boundary=ffboundary',
);
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
res.setHeader('Connection', 'keep-alive');
// Tell nginx (used by HA Ingress) not to buffer this streaming response.
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders(); // send headers immediately, before any body data
// Pipe every chunk straight to the browser without buffering.
proxyRes.pipe(res, { end: true });
},
);
// If the browser disconnects, tear down the upstream request too.
req.on('close', () => proxyReq.destroy());| async function fetchStatus() { | ||
| try { | ||
| const res = await fetch(`${BASE}/api/status`); | ||
| const json = await res.json(); | ||
| if (!res.ok || json.error) { | ||
| badge.textContent = 'Errore connessione'; | ||
| badge.className = 'badge badge--error'; | ||
| lastUpdate.textContent = `Errore: ${json.error || res.statusText}`; | ||
| return; | ||
| } | ||
| if (json.detail) updateUI(json.detail); |
There was a problem hiding this comment.
In fetchStatus, res.json() is called before checking res.ok. If the server returns a non-JSON error page (e.g., an HTML error page on 502/503), res.json() will throw a SyntaxError, obscuring the actual connection/HTTP error. Check res.ok first before attempting to parse the response as JSON.
async function fetchStatus() {
try {
const res = await fetch(BASE + '/api/status');
if (!res.ok) {
badge.textContent = 'Errore connessione';
badge.className = 'badge badge--error';
lastUpdate.textContent = 'Errore: HTTP ' + res.status;
return;
}
const json = await res.json();
if (json.error) {
badge.textContent = 'Errore connessione';
badge.className = 'badge badge--error';
lastUpdate.textContent = 'Errore: ' + json.error;
return;
}
if (json.detail) updateUI(json.detail);| function parseGo2rtcUrl() { | ||
| try { | ||
| const u = new URL(GO2RTC_URL); | ||
| return { host: u.hostname, port: Number(u.port) || 1984 }; | ||
| } catch (_) { | ||
| return { host: 'ccab4aaf-frigate', port: 1984 }; | ||
| } | ||
| } |
There was a problem hiding this comment.
In parseGo2rtcUrl, if GO2RTC_URL is configured without a protocol (e.g., 192.168.1.100:1984), new URL() will throw an error and fall back to the default ccab4aaf-frigate host. Prepending http:// when a protocol is missing makes the URL parsing more robust.
| function parseGo2rtcUrl() { | |
| try { | |
| const u = new URL(GO2RTC_URL); | |
| return { host: u.hostname, port: Number(u.port) || 1984 }; | |
| } catch (_) { | |
| return { host: 'ccab4aaf-frigate', port: 1984 }; | |
| } | |
| } | |
| function parseGo2rtcUrl() { | |
| try { | |
| const urlStr = GO2RTC_URL.includes('://') ? GO2RTC_URL : 'http://' + GO2RTC_URL; | |
| const u = new URL(urlStr); | |
| return { host: u.hostname, port: Number(u.port) || 1984 }; | |
| } catch (_) { | |
| return { host: 'ccab4aaf-frigate', port: 1984 }; | |
| } | |
| } |
| if (MQTT_ENABLED) { | ||
| mqttPollingTimer = setInterval(() => { | ||
| refreshPrinterState(); | ||
| }, MQTT_POLL_INTERVAL_MS); | ||
| } |
There was a problem hiding this comment.
Using setInterval for periodic asynchronous polling can cause overlapping requests if a request takes longer than the interval. It is safer to use recursive setTimeout to ensure the next poll is only scheduled after the current one completes.
if (MQTT_ENABLED) {
const poll = async () => {
await refreshPrinterState();
mqttPollingTimer = setTimeout(poll, MQTT_POLL_INTERVAL_MS);
};
poll();
}| const boundary = `----FormBoundary${Date.now()}`; | ||
| const preamble = [ | ||
| `--${boundary}`, | ||
| `Content-Disposition: form-data; name="gcodeFile"; filename="${req.file.originalname}"`, | ||
| `Content-Type: application/octet-stream`, | ||
| '', | ||
| '', | ||
| ].join('\r\n'); |
There was a problem hiding this comment.
In /api/upload, req.file.originalname is embedded directly into the multipart form header without sanitization. If the filename contains double quotes or other special characters, it can lead to header injection or malformed multipart payloads. Sanitizing the filename before embedding it is highly recommended.
| const boundary = `----FormBoundary${Date.now()}`; | |
| const preamble = [ | |
| `--${boundary}`, | |
| `Content-Disposition: form-data; name="gcodeFile"; filename="${req.file.originalname}"`, | |
| `Content-Type: application/octet-stream`, | |
| '', | |
| '', | |
| ].join('\r\n'); | |
| const safeFilename = req.file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_'); | |
| const boundary = '----FormBoundary' + Date.now(); | |
| const preamble = [ | |
| '--' + boundary, | |
| 'Content-Disposition: form-data; name="gcodeFile"; filename="' + safeFilename + '"', | |
| 'Content-Type: application/octet-stream', | |
| '', | |
| '', | |
| ].join('\r\n'); |
No description provided.