From 9e2d0cb246ffd7ecaf9677afd4ffde76198d215a Mon Sep 17 00:00:00 2001
From: Ayush Debnath <139256624+Solventerritory@users.noreply.github.com>
Date: Mon, 1 Dec 2025 06:44:13 +0000
Subject: [PATCH 1/5] video: validate uploads; surface errors on client; add
test helper & docs
---
video/README.md | 26 ++++++++++++++++++++++++++
video/server/index.mjs | 7 +++++--
video/server/upload.mjs | 37 ++++++++++++++++++++++++++++++++-----
video/src/App.jsx | 24 +++++++++++++++++++-----
video/src/VideoPlayer.jsx | 10 +++++-----
video/test-upload.sh | 19 +++++++++++++++++++
6 files changed, 106 insertions(+), 17 deletions(-)
create mode 100644 video/README.md
create mode 100644 video/test-upload.sh
diff --git a/video/README.md b/video/README.md
new file mode 100644
index 00000000..cabdb481
--- /dev/null
+++ b/video/README.md
@@ -0,0 +1,26 @@
+## Video app — deployment & upload troubleshooting
+
+When uploads fail on a deployed instance but work locally, this is commonly caused by missing environment variables or the runtime not allowing temporary files to be read by the server.
+
+Quick checks:
+
+- Ensure the server environment has the API key available under `VITE_GEMINI_API_KEY`.
+ - Locally the dev server reads `.env` (see `package.json` dev script) or you can set the environment variable before running the server.
+- If your hosting provider prevents the runtime from writing to `/tmp`, change `multer`'s destination in `server/index.mjs` to a writable location.
+- If the upload fails the server will now return a helpful JSON message in the response body (e.g. `{ "error": "VITE_GEMINI_API_KEY is not set on the server" }`). These messages will be visible in the UI.
+
+How to run locally for quick debugging:
+
+1. Add a `.env` file in `/video` with a valid key (or set `VITE_GEMINI_API_KEY` in your shell):
+
+ VITE_GEMINI_API_KEY=your_api_key_here
+
+2. Start the app:
+
+ npm run dev
+
+3. Use the included curl helper (`test-upload.sh`) to simulate uploading a file to the server:
+
+ ./test-upload.sh /path/to/sample.mp4
+
+This will show the raw API response and help identify cases like missing API keys, invalid mime types, or other upload errors.
diff --git a/video/server/index.mjs b/video/server/index.mjs
index 9ed24079..c675aff6 100644
--- a/video/server/index.mjs
+++ b/video/server/index.mjs
@@ -27,7 +27,9 @@ app.post('/api/upload', upload.single('video'), async (req, res) => {
const resp = await uploadVideo(file)
res.json({data: resp})
} catch (error) {
- res.status(500).json({error})
+ // Error can be an Error object; return a structured JSON message to the client
+ console.error('/api/upload error:', error)
+ res.status(500).json({error: error?.message || String(error)})
}
})
@@ -50,7 +52,8 @@ app.post('/api/prompt', async (req, res) => {
)
res.json(videoResponse)
} catch (error) {
- res.json({error}, {status: 400})
+ console.error('/api/prompt error:', error)
+ res.status(400).json({error: error?.message || String(error)})
}
})
diff --git a/video/server/upload.mjs b/video/server/upload.mjs
index 050106fc..23ff79d6 100644
--- a/video/server/upload.mjs
+++ b/video/server/upload.mjs
@@ -16,14 +16,41 @@ import {GoogleGenerativeAI} from '@google/generative-ai'
import {GoogleAIFileManager} from '@google/generative-ai/server'
const key = process.env.VITE_GEMINI_API_KEY
+
+if (!key) {
+ // Fail fast with a clear message so deployments without a key don't silently
+ // accept uploads then fail later.
+ console.error('VITE_GEMINI_API_KEY is missing from environment variables')
+ throw new Error('VITE_GEMINI_API_KEY is not set on the server')
+}
+
const fileManager = new GoogleAIFileManager(key)
const genAI = new GoogleGenerativeAI(key)
export const uploadVideo = async file => {
try {
+ // Basic validation: ensure the incoming file looks like a video and has a
+ // filename. This avoids uploading unsupported files (or empty bodies) to
+ // the file manager which will fail later and is harder to debug.
+ if (!file || !file.path || !file.originalname) {
+ const msg = 'Missing file or invalid upload payload (file.path or originalname)'
+ console.error(msg, file)
+ throw new Error(msg)
+ }
+
+ // Prefer explicit video MIME types; multer may sometimes set generic
+ // application/octet-stream depending on environment. Only allow common
+ // video types for now.
+ const mimeType = file.mimetype || 'application/octet-stream'
+ if (!mimeType.startsWith('video/')) {
+ const msg = `Unsupported mimeType: ${mimeType}. Expected a video/* type.`
+ console.error(msg)
+ throw new Error(msg)
+ }
+
const uploadResult = await fileManager.uploadFile(file.path, {
displayName: file.originalname,
- mimeType: file.mimetype
+ mimeType: mimeType
})
return uploadResult.file
} catch (error) {
@@ -37,8 +64,8 @@ export const checkProgress = async fileId => {
const result = await fileManager.getFile(fileId)
return result
} catch (error) {
- console.error(error)
- return {error}
+ console.error('checkProgress errored:', error)
+ return {error: error?.message || String(error)}
}
}
@@ -61,7 +88,7 @@ export const promptVideo = async (uploadResult, prompt, model) => {
feedback: result.response.promptFeedback
}
} catch (error) {
- console.error(error)
- return {error}
+ console.error('promptVideo errored:', error)
+ return {error: error?.message || String(error)}
}
}
diff --git a/video/src/App.jsx b/video/src/App.jsx
index f904cddd..b7719d59 100644
--- a/video/src/App.jsx
+++ b/video/src/App.jsx
@@ -33,6 +33,7 @@ export default function App() {
const [isLoading, setIsLoading] = useState(false)
const [showSidebar, setShowSidebar] = useState(true)
const [isLoadingVideo, setIsLoadingVideo] = useState(false)
+ // videoError will hold a user-friendly message string or false when no error
const [videoError, setVideoError] = useState(false)
const [customPrompt, setCustomPrompt] = useState('')
const [chartMode, setChartMode] = useState(chartModes[0])
@@ -96,14 +97,27 @@ export default function App() {
const formData = new FormData()
formData.set('video', e.dataTransfer.files[0])
- const resp = await (
- await fetch('/api/upload', {
+ try {
+ const uploadResponse = await fetch('/api/upload', {
method: 'POST',
body: formData
})
- ).json()
- setFile(resp.data)
- checkProgress(resp.data.name)
+
+ if (!uploadResponse.ok) {
+ const errJson = await uploadResponse.json().catch(() => ({}))
+ const msg = errJson?.error || `Upload failed with status ${uploadResponse.status}`
+ setVideoError(msg)
+ setIsLoadingVideo(false)
+ return
+ }
+
+ const resp = await uploadResponse.json()
+ setFile(resp.data)
+ checkProgress(resp.data.name)
+ } catch (err) {
+ setVideoError(err?.message || String(err))
+ setIsLoadingVideo(false)
+ }
}
const checkProgress = async fileId => {
diff --git a/video/src/VideoPlayer.jsx b/video/src/VideoPlayer.jsx
index b09bd64b..601615e8 100644
--- a/video/src/VideoPlayer.jsx
+++ b/video/src/VideoPlayer.jsx
@@ -176,11 +176,11 @@ export default function VideoPlayer({
) : (
- {isLoadingVideo
- ? 'Processing video...'
- : videoError
- ? 'Error processing video.'
- : 'Drag and drop a video file here to get started.'}
+ {isLoadingVideo
+ ? 'Processing video...'
+ : videoError
+ ? `Error processing video: ${videoError}`
+ : 'Drag and drop a video file here to get started.'}
)}
diff --git a/video/test-upload.sh b/video/test-upload.sh
new file mode 100644
index 00000000..b341f1a3
--- /dev/null
+++ b/video/test-upload.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# Simple helper to upload a file to a running local dev server
+# Usage: ./test-upload.sh /path/to/file.mp4
+
+set -eu
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 /path/to/video.mp4"
+ exit 1
+fi
+
+FILE=$1
+ENDPOINT=${ENDPOINT:-http://localhost:8000/api/upload}
+
+if [ ! -f "$FILE" ]; then
+ echo "File not found: $FILE"
+ exit 1
+fi
+
+curl -v -F "video=@${FILE}" "$ENDPOINT" | jq || true
From 6264348359d1a4bbb50f4cda2d1b71c007f8f8a0 Mon Sep 17 00:00:00 2001
From: Ayush Debnath <139256624+Solventerritory@users.noreply.github.com>
Date: Mon, 1 Dec 2025 06:59:05 +0000
Subject: [PATCH 2/5] video: accept octet-stream for known video extensions;
configurable tmp dir; add mime-fallback test
---
video/README.md | 7 +++++++
video/server/index.mjs | 4 +++-
video/server/upload.mjs | 33 ++++++++++++++++++++++++++++-----
video/test-mime-fallback.sh | 17 +++++++++++++++++
video/test-upload.sh | 0
5 files changed, 55 insertions(+), 6 deletions(-)
create mode 100755 video/test-mime-fallback.sh
mode change 100644 => 100755 video/test-upload.sh
diff --git a/video/README.md b/video/README.md
index cabdb481..fbf0b59b 100644
--- a/video/README.md
+++ b/video/README.md
@@ -7,6 +7,7 @@ Quick checks:
- Ensure the server environment has the API key available under `VITE_GEMINI_API_KEY`.
- Locally the dev server reads `.env` (see `package.json` dev script) or you can set the environment variable before running the server.
- If your hosting provider prevents the runtime from writing to `/tmp`, change `multer`'s destination in `server/index.mjs` to a writable location.
+ - If your hosting provider prevents the runtime from writing to `/tmp`, set the `VIDEO_UPLOAD_TMP_DIR` env var to a writable directory; the server will use that directory for temporary uploads.
- If the upload fails the server will now return a helpful JSON message in the response body (e.g. `{ "error": "VITE_GEMINI_API_KEY is not set on the server" }`). These messages will be visible in the UI.
How to run locally for quick debugging:
@@ -24,3 +25,9 @@ How to run locally for quick debugging:
./test-upload.sh /path/to/sample.mp4
This will show the raw API response and help identify cases like missing API keys, invalid mime types, or other upload errors.
+
+Advanced testing:
+
+- When some deployments strip or normalize Content-Type headers, the server will accept `application/octet-stream` uploads for files with a known video extension (e.g. .mp4) and will promote the MIME type accordingly. Use `test-mime-fallback.sh` to explicitly test that behavior:
+
+ ./test-mime-fallback.sh /path/to/sample.mp4
diff --git a/video/server/index.mjs b/video/server/index.mjs
index c675aff6..418aa1de 100644
--- a/video/server/index.mjs
+++ b/video/server/index.mjs
@@ -20,7 +20,9 @@ import {checkProgress, promptVideo, uploadVideo} from './upload.mjs'
const app = express()
app.use(express.json())
-const upload = multer({dest: '/tmp/'})
+// Allow configuring the temporary uploads directory (some hosts restrict /tmp)
+const tmpDir = process.env.VIDEO_UPLOAD_TMP_DIR || '/tmp/'
+const upload = multer({dest: tmpDir})
app.post('/api/upload', upload.single('video'), async (req, res) => {
try {
const file = req.file
diff --git a/video/server/upload.mjs b/video/server/upload.mjs
index 23ff79d6..1fc72cc9 100644
--- a/video/server/upload.mjs
+++ b/video/server/upload.mjs
@@ -14,6 +14,7 @@
import {GoogleGenerativeAI} from '@google/generative-ai'
import {GoogleAIFileManager} from '@google/generative-ai/server'
+import path from 'path'
const key = process.env.VITE_GEMINI_API_KEY
@@ -39,11 +40,33 @@ export const uploadVideo = async file => {
}
// Prefer explicit video MIME types; multer may sometimes set generic
- // application/octet-stream depending on environment. Only allow common
- // video types for now.
- const mimeType = file.mimetype || 'application/octet-stream'
- if (!mimeType.startsWith('video/')) {
- const msg = `Unsupported mimeType: ${mimeType}. Expected a video/* type.`
+ // application/octet-stream depending on environment. Allow the common
+ // video types, and accept application/octet-stream only when the file
+ // extension strongly indicates a video (this helps deployed hosts which
+ // sometimes strip or normalize Content-Type headers).
+ const mimeTypeRaw = file.mimetype || 'application/octet-stream'
+ let mimeType = mimeTypeRaw
+
+ const ext = path.extname(file.originalname || '').toLowerCase()
+ const knownVideoExt = new Set(['.mp4', '.mov', '.webm', '.mkv', '.avi', '.ogg'])
+
+ const extMap = {
+ '.mp4': 'video/mp4',
+ '.mov': 'video/quicktime',
+ '.webm': 'video/webm',
+ '.mkv': 'video/x-matroska',
+ '.avi': 'video/x-msvideo',
+ '.ogg': 'video/ogg'
+ }
+
+ if (mimeTypeRaw.startsWith('video/')) {
+ // OK as-is
+ } else if (mimeTypeRaw === 'application/octet-stream' && knownVideoExt.has(ext)) {
+ // Fallback: promote to a reasonable video MIME type based on extension
+ mimeType = extMap[ext] || 'application/octet-stream'
+ console.warn(`uploadVideo: promoted mimeType from application/octet-stream to ${mimeType} based on extension ${ext}`)
+ } else {
+ const msg = `Unsupported mimeType: ${mimeTypeRaw}. Expected a video/* type or application/octet-stream with a video extension.`
console.error(msg)
throw new Error(msg)
}
diff --git a/video/test-mime-fallback.sh b/video/test-mime-fallback.sh
new file mode 100755
index 00000000..5bd899a6
--- /dev/null
+++ b/video/test-mime-fallback.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# Helper to upload using application/octet-stream to test MIME fallback behavior
+# Usage: ./test-mime-fallback.sh /path/to/file.mp4
+set -eu
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 /path/to/video.mp4"
+ exit 1
+fi
+FILE=$1
+ENDPOINT=${ENDPOINT:-http://localhost:8000/api/upload}
+if [ ! -f "$FILE" ]; then
+ echo "File not found: $FILE"
+ exit 1
+fi
+
+# Force the file content-type to application/octet-stream when sending the multipart form
+curl -v -F "video=@${FILE};type=application/octet-stream" "$ENDPOINT" | jq || true
diff --git a/video/test-upload.sh b/video/test-upload.sh
old mode 100644
new mode 100755
From 48dba936401e65816a47b58700fe531dd4649091 Mon Sep 17 00:00:00 2001
From: Ayush Debnath <139256624+Solventerritory@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:36:22 +0530
Subject: [PATCH 3/5] Update video/server/upload.mjs
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
video/server/upload.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/video/server/upload.mjs b/video/server/upload.mjs
index 1fc72cc9..bf6f786f 100644
--- a/video/server/upload.mjs
+++ b/video/server/upload.mjs
@@ -88,7 +88,7 @@ export const checkProgress = async fileId => {
return result
} catch (error) {
console.error('checkProgress errored:', error)
- return {error: error?.message || String(error)}
+ throw error
}
}
From c80a5461093d19803e33cef1dfe983abc3ac5f41 Mon Sep 17 00:00:00 2001
From: Ayush Debnath <139256624+Solventerritory@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:36:34 +0530
Subject: [PATCH 4/5] Update video/server/upload.mjs
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
video/server/upload.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/video/server/upload.mjs b/video/server/upload.mjs
index bf6f786f..dbe135fd 100644
--- a/video/server/upload.mjs
+++ b/video/server/upload.mjs
@@ -112,6 +112,6 @@ export const promptVideo = async (uploadResult, prompt, model) => {
}
} catch (error) {
console.error('promptVideo errored:', error)
- return {error: error?.message || String(error)}
+ throw error
}
}
From 2505eb3126582820f03c73ff004b2d0bcf46dfde Mon Sep 17 00:00:00 2001
From: Ayush Debnath <139256624+Solventerritory@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:36:44 +0530
Subject: [PATCH 5/5] Update video/src/VideoPlayer.jsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---
video/src/VideoPlayer.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/video/src/VideoPlayer.jsx b/video/src/VideoPlayer.jsx
index 601615e8..0ce1dca5 100644
--- a/video/src/VideoPlayer.jsx
+++ b/video/src/VideoPlayer.jsx
@@ -179,7 +179,7 @@ export default function VideoPlayer({
{isLoadingVideo
? 'Processing video...'
: videoError
- ? `Error processing video: ${videoError}`
+ ? `Error processing video: ${typeof videoError === 'string' ? videoError : 'An unknown error occurred.'}`
: 'Drag and drop a video file here to get started.'}