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

Open
#1,232 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug file-manager performance platform-desktop platform-proxmox platform-windows
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:

  1. Body vs. query. The client sends fields in the multipart body; the server reads them from the query string. Guaranteed 400 on the first chunk, every time.
  2. chunkIndex vs. offset. The client sends a chunk index; the server expects a byte offset. Different protocols.
  3. 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 --boundary delimiters and Content-Disposition headers into the destination file. The route is written for application/octet-stream (cf. express.raw({ limit: "5gb", type: "application/octet-stream" }) at index.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
  1. Open the file manager on any SSH host.
  2. Upload a single file larger than 1.5 GiB.
  3. 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 raw application/octet-stream body (matches the existing express.raw config and the server's req.pipe(writeStream)).
  • Option B — server parses the multipart body with busboy, the way uploadFileStream already 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.