Files
platform/src/servers/api/photos/router.ts
T
pastilhasandClaude Opus 5 da5cd8e918 stop losing the answer when immich rejects mid-upload
An upload over a few MB came back as a 400 with an empty body, no message anywhere, and nothing logged
by Immich, the sidecar or officer. It was a race, not a size limit. Immich judges an asset from its
first few KB and rejects immediately, then closes; both our hops were still writing the body; Node
treats the leftover bytes as a protocol violation and replaces the application's answer with a bodyless
`400 Bad Request` + `Connection: close`. The real message never reached the wire.

Measured before the change: streamed lost the message 1/4 at 8 MB and 4/4 at 32 MB — probability rising
with size, which is why small photos usually worked and a phone's video never did.

Both hops needed it. Fixing only the sidecar took 32 MB from 4/4 failing to 2/4, because the platform
proxy was losing it one hop up.

Bounded at 512 MB, above which the body streams exactly as before. That ceiling is not a refusal and is
deliberately not a 413: a file Immich ACCEPTS is read to the end and never races, so a 4 GB video is
unaffected. All that is given up above the cap is the error message on a file that was going to be
rejected anyway. A first attempt refused over-cap uploads outright and would have broken the working
4 GB case to improve diagnosis of the doomed one.

`bufferRequestBody` is opt-in and off by default: the vault and wallet proxies must keep streaming so a
passphrase or macaroon never lands in the platform's heap.

Also adds the proxy error logging that made this findable at all — status and two byte counts from
headers, never the bodies. `responseBytes: "unknown"` is what exposed the stripped response.

Verified live at 32/256 MB (buffered) and 640 MB (streamed, passes through).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:22:56 +00:00

26 lines
1.2 KiB
TypeScript

import { createSidecarProxy } from '../../sidecar/create-proxy';
// /api/photos/* — auth, then forward to officer-photos. No routes of its own and no Immich knowledge:
// this file must never grow app logic.
//
// The sidecar owns the Immich contract and holds its API key.
const proxy = createSidecarProxy({
name: 'photos',
prefix: '/api/photos',
// Originals and `download/archive` zips are large and Immich builds the archive as it streams it, so the
// socket can sit quiet longer than the default 60s idle drop allows.
timeoutSeconds: 600,
// Immich judges an asset from its first few KB and rejects immediately, while we are still writing —
// and the response is lost in the teardown, which is what made every large upload fail with an
// unexplained empty 400. Buffering here removes the overlap. The sidecar does the same on its own hop
// to Immich (sidecar/photos/routes.ts → readBodyCapped), and both are needed: fixing one alone still
// lost the answer roughly two runs in three at 32 MB.
bufferRequestBody: true,
});
export const photosRouter = proxy.router;
/** Base URL of the sidecar's HTTP server, or null if it hasn't reported in yet. */
export const getPhotosServerUrl = proxy.getHttpUrl;