Termix-SSH / Termix-SSH/Support
[BUG] Chunked upload path for files over 1.5 GiB always fails with 400 — client sends form fields, server reads query
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 28
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
Title
Files larger than 1.5 GiB fail immediately; the chunked upload client and server disagree on the request format
Platform
Desktop App - Windows
Server Installation Method
Proxmox (Community Scripts)
Version
2.7.1
CLI Installation Method
None
CLI Version
No response
Troubleshooting
- I have examined logs and tried to find the issue
- I have reviewed opened and closed issues
- I have tried restarting the application
- I have checked open issues and ensured this is not a duplicate
The Problem
Found while investigating bulk upload failures ( #1231 ). This one is independent and reproducible on its own with a single file.
The client switches to a chunked upload above 1.5 GiB (src/ui/api/ssh-file-operations-api.ts:444-497), building a multipart FormData body with no query string:
form.append("sessionId", sessionId);
form.append("path", path);
form.append("fileName", fileName);
form.append("chunkIndex", String(i));
form.append("totalChunks", String(totalChunks));
form.append("totalSize", String(file.size));
form.append("chunk", chunkBlob, fileName);
await getFileManagerApiForSession(sessionId)
.postForm("/ssh/uploadFileChunk", form, { timeout: 0 });
The server reads only req.query (src/backend/hosts/file-manager/content-routes.ts:1538-1558):
const sessionId = getRequiredQueryParam(req.query.sessionId as string);
const remotePath = getRequiredQueryParam(req.query.path as string);
const fileName = getRequiredQueryParam(req.query.fileName as string);
const offset = parseByteOffset(getRequiredQueryParam(req.query.offset as string));
const totalSize = parseByteOffset(getRequiredQueryParam(req.query.totalSize as string));
if (!sessionId || !remotePath || !fileName) {
req.resume();
return res.status(400).json({ error: "Missing sessionId, path, or fileName" });
}
I ran the route's exact parsing logic against the client's exact FormData shape:
POST /ssh/uploadFileChunk (multipart body, no query string)
server saw req.query = {}
-> HTTP 400 { error: 'Missing sessionId, path, or fileName' }
There are three separate contract mismatches:
- Body vs. query. The client sends fields in the multipart body; the server reads them from the query string. Guaranteed
400on the first chunk, every time. chunkIndexvs.offset. The client sends a chunk index; the server expects a byte offset. Different protocols.- Raw body piped into the file. The server pipes the request body straight into the SFTP write stream (
content-routes.ts:1636). With a multipart body that would write--boundarydelimiters andContent-Dispositionheaders into the destination file. The route is written forapplication/octet-stream(cf.express.raw({ limit: "5gb", type: "application/octet-stream" })atindex.ts:126), which is not what the client sends.
The client also ignores the nextOffset and complete fields the server returns, so there's no resume even in principle.
Net effect: every file over 1.5 GiB fails immediately, and the fallback specifically designed to rescue very large uploads is the one path that can never run.
How to Reproduce
- Open the file manager on any SSH host.
- Upload a single file larger than 1.5 GiB.
- It fails immediately. The server responds
400 {"error":"Missing sessionId, path, or fileName"}on the very first chunk.
Additional Context
There is no test coverage for this route — src/backend/tests/hosts/file-manager/ has no tests for uploadFileChunk, which is presumably why the mismatch went unnoticed.
Suggested fix
Pick one side of the contract and make both match:
- Option A — client sends params as a query string with a real byte
offset, and posts the chunk as a rawapplication/octet-streambody (matches the existingexpress.rawconfig and the server'sreq.pipe(writeStream)). - Option B — server parses the multipart body with busboy, the way
uploadFileStreamalready does.
Either way, the client should then honour the returned nextOffset / complete so resume actually works.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the chunked-upload client in src/ui/api/ssh-file-operations-api.ts:444-497 and the server route in src/backend/hosts/file-manager/content-routes.ts:1538-1636; check the raw-body setup in index.ts:126. Compare this contract with uploadFileStream, then add route coverage under src/backend/tests/hosts/file-manager/. Done means uploads over 1.5 GiB complete successfully and the returned nextOffset and complete values support resume.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend, full-stack, testing
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100