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

  1. Use only the allowed AssetMediaSize values for ?size= (e.g. preview, thumbnail) or omit the parameter entirely for the default.
  2. Update the client/immich-sdk version so URL building matches the server's enum.
  3. URL-encode or avoid interpolating raw user input into the size parameter.
  4. 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

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


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)