immich-app/immich · error · Error
Invalid targetSize: ' + targetSize
Error message
Invalid targetSize: ' + targetSize
What it means
Immich's viewAsset controller, when handling image previews, decides between the 'original' path and a sized preview path using a `size` query parameter that must be one of the AssetMediaSize enum values. If the client supplies a size outside the enum, the controller throws this Error, indicating a bad client-side value rather than a missing asset.
Solutions
- Use only the allowed AssetMediaSize values for ?size= (e.g. preview, thumbnail) or omit the parameter entirely for the default.
- Update the client/immich-sdk version so URL building matches the server's enum.
- URL-encode or avoid interpolating raw user input into the size parameter.
- If you need a different resolution, request the original and resize client-side instead of inventing a size name.
Example fix
// before GET /api/assets/<id>/thumbnail?size=small // throws // after GET /api/assets/<id>/thumbnail?size=preview
Defensive patterns
Strategy: validation
Validate before calling
const AssetMediaSize = { Preview: 'preview', Thumbnail: 'thumbnail' } as const;
const size = new URLSearchParams(q).get('size');
if (size && !Object.values(AssetMediaSize).includes(size as any)) {
throw new Error(`unsupported size '${size}'; use ${Object.values(AssetMediaSize).join('|')}`);
} Type guard
const isAssetMediaSize = (s: string | null): s is AssetMediaSize => s !== null && Object.values(AssetMediaSize).includes(s as AssetMediaSize);
Try / catch
app.get('/assets/:id/thumbnail', async (req, res) => {
try {
/* request with ?size=... */
} catch (e) {
if (String(e).startsWith('Invalid targetSize')) {
return res.status(400).json({ error: 'size must be preview|thumbnail' });
}
throw e;
}
}); Prevention
- Whitelist ?size= against AssetMediaSize before building asset URLs
- Never interpolate raw user input into the size parameter
- Regenerate clients/URLs after Immich server upgrades in case the enum changed
When it happens
Trigger: Requesting GET /assets/:id/thumbnail (or preview) with ?size=<something> that is not exactly 'preview'/'thumbnail' (the AssetMediaSize members), e.g. size=small, size=full, or a numeric width.
Common situations: Hand-written image URLs, cached/third-party clients generating size strings, version drift where a client uses a size name the server no longer supports, or bookmarks/HTML copied with old parameters.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- await response.text()
- EOFException
- errors.unable_to_upload_file
- Expected a JSON response
- Failed to fetch activation key
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/18fa41e103d945d7.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/controllers/asset-media.controller.ts:156
if (viewThumbnailRes instanceof ImmichFileResponse) {
await sendFile(res, next, () => Promise.resolve(viewThumbnailRes), this.logger);
} else {
// viewThumbnailRes is a AssetMediaRedirectResponse
// which redirects to the original asset or a specific size to make better use of caching
const { targetSize } = viewThumbnailRes;
const [reqPath, reqSearch] = req.url.split('?', 2);
let redirPath: string;
const redirSearchParams = new URLSearchParams(reqSearch);
if (targetSize === 'original') {
// relative path to this.downloadAsset
redirPath = 'original';
redirSearchParams.delete('size');
} else if (Object.values(AssetMediaSize).includes(targetSize)) {
redirPath = reqPath;
redirSearchParams.set('size', targetSize);
} else {
throw new Error('Invalid targetSize: ' + targetSize);
}
const finalRedirPath = redirPath + '?' + redirSearchParams.toString();
return res.redirect(finalRedirPath);
}
}
@Get(':id/video/playback')
@FileResponse()
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
@Endpoint({
summary: 'Play asset video',
description: 'Streams the video file for the specified asset. This endpoint also supports byte range requests.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
})
async playAssetVideo(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Res() res: Response,View on GitHub (pinned to e55ac299a4)