{"record":{"id":"3fc3e839050a2778","repo":"immich-app/immich","slug":"invalid-storage-template","errorCode":null,"errorMessage":"Invalid storage template","messagePattern":"Invalid storage template","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/storage-template.service.ts","lineNumber":128,"sourceCode":"      this.render(compiled, {\n        asset: {\n          fileCreatedAt: new Date(),\n          originalPath: '/upload/test/IMG_123.jpg',\n          type: AssetType.Image,\n          id: 'd587e44b-f8c0-4832-9ba3-43268bbf5d4e',\n        } as StorageAsset,\n        filename: 'IMG_123',\n        extension: 'jpg',\n        albumName: 'album',\n        albumStartDate: new Date(),\n        albumEndDate: new Date(),\n        make: 'FUJIFILM',\n        model: 'X-T50',\n        lensModel: 'XF27mm F2.8 R WR',\n      });\n    } catch (error) {\n      this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`);\n      throw new Error('Invalid storage template', { cause: error });\n    }\n  }\n\n  getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {\n    return { ...storageTokens, presetOptions: storagePresets };\n  }\n\n  @OnEvent({ name: 'AssetMetadataExtracted' })\n  async onAssetMetadataExtracted({ source, assetId }: ArgOf<'AssetMetadataExtracted'>) {\n    await this.jobRepository.queue({ name: JobName.StorageTemplateMigrationSingle, data: { source, id: assetId } });\n  }\n\n  @OnJob({ name: JobName.StorageTemplateMigrationSingle, queue: QueueName.StorageTemplateMigration })\n  async handleMigrationSingle({ id }: JobOf<JobName.StorageTemplateMigrationSingle>): Promise<JobStatus> {\n    const config = await this.getConfig({ withCache: true });\n    const isStorageTemplateEnabled = config.storageTemplate.enabled;\n    if (!isStorageTemplateEnabled) {\n      return JobStatus.Skipped;","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/storage-template.service.ts#L110-L146","documentation":"StorageTemplateService validates a candidate storage template by rendering it against a sample asset. If rendering throws (unknown token, bad token syntax, malformed template), the catch rethrows a plain Error 'Invalid storage template' with the original error as cause (storage-template.service.ts:128). This blocks the template from being saved.","triggerScenarios":"Saving system config (PUT /system-config) with a storageTemplate.template value containing an unrecognized token like {{foo}} or malformed handlebars/bracket syntax that the renderer cannot parse.","commonSituations":"Hand-editing the template with a token not in storageTokens, version upgrades that renamed/removed tokens, or copy-pasting a template from an incompatible Immich version.","solutions":["Build the template only from tokens listed by getStorageTemplateOptions() (exposed via the config options endpoint).","Start from a known preset and modify incrementally, validating each change.","After an upgrade, review release notes for renamed/removed storage tokens and update custom templates."],"exampleFix":"// before\n{ storageTemplate: { template: '{{albumName}}/{{foo}}/{{fullname}}' } }\n// after\n{ storageTemplate: { template: '{{y}}/{{MM}}/{{fullname}}' } }","handlingStrategy":"validation","validationCode":"const options = await systemConfigApi.getStorageTemplateOptions(); // tokens + presets\nconst allowedTokens = new Set(Object.keys(options.storageTokens ?? {}));\nconst usedTokens = extractTokens(newConfig.storageTemplate.template); // e.g. ['y','MM','fullname']\nif (usedTokens.some((t) => !allowedTokens.has(t))) {\n  throw new Error('Storage template contains an unknown token.');\n}\nawait systemConfigApi.update(newConfig);","typeGuard":"const isKnownToken = (token: string, allowed: Set<string>): token is string => allowed.has(token);","tryCatchPattern":"try {\n  await systemConfigApi.update(newConfig);\n} catch (e) {\n  if (/Invalid storage template/i.test(String((e as Error).message))) {\n    showTemplateEditorWithError((e as Error).cause);\n  } else throw e;\n}","preventionTips":["Build templates from tokens returned by the config options endpoint.","Modify presets incrementally and validate each change.","Re-check token names after Immich upgrades."],"tags":["storage-template","config","validation"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}