- {#if assets && data.assets.length > 0}
+ {#if assets && assets.length > 0}
{#each assets as asset (asset.id)}
{/each}
@@ -75,7 +84,7 @@
cursor={assetCursor}
showNavigation={assets.length > 1}
{onRandom}
- {onAction}
+ {preAction}
onClose={() => {
assetViewerManager.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
From 96b6165bd38032bf8201404ccdd30611455e6a11 Mon Sep 17 00:00:00 2001
From: Timon
Date: Tue, 28 Apr 2026 19:07:39 +0200
Subject: [PATCH 014/140] refactor(server)!: move correlationId to
X-Correlation-ID response header (#28139)
---
e2e/src/responses.ts | 13 -------------
server/src/enum.ts | 1 -
server/src/middleware/global-exception.filter.ts | 6 ++++--
server/src/repositories/config.repository.ts | 5 ++---
server/test/medium/responses.ts | 10 ----------
5 files changed, 6 insertions(+), 29 deletions(-)
diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts
index 3d7971d6f0..fcb300828e 100644
--- a/e2e/src/responses.ts
+++ b/e2e/src/responses.ts
@@ -5,79 +5,66 @@ export const errorDto = {
error: 'Unauthorized',
statusCode: 401,
message: 'Authentication required',
- correlationId: expect.any(String),
},
unauthorizedWithMessage: (message: string) => ({
error: 'Unauthorized',
statusCode: 401,
message,
- correlationId: expect.any(String),
}),
forbidden: {
error: 'Forbidden',
statusCode: 403,
message: expect.any(String),
- correlationId: expect.any(String),
},
missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`,
- correlationId: expect.any(String),
}),
wrongPassword: {
error: 'Bad Request',
statusCode: 400,
message: 'Wrong password',
- correlationId: expect.any(String),
},
invalidToken: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid user token',
- correlationId: expect.any(String),
},
invalidShareKey: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid share key',
- correlationId: expect.any(String),
},
passwordRequired: {
error: 'Unauthorized',
statusCode: 401,
message: 'Password required',
- correlationId: expect.any(String),
},
badRequest: (message: any = null) => ({
error: 'Bad Request',
statusCode: 400,
message: message ?? expect.anything(),
- correlationId: expect.any(String),
}),
noPermission: {
error: 'Bad Request',
statusCode: 400,
message: expect.stringContaining('Not found or no'),
- correlationId: expect.any(String),
},
incorrectLogin: {
error: 'Unauthorized',
statusCode: 401,
message: 'Incorrect email or password',
- correlationId: expect.any(String),
},
alreadyHasAdmin: {
error: 'Bad Request',
statusCode: 400,
message: 'The server already has an admin',
- correlationId: expect.any(String),
},
invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'],
- correlationId: expect.any(String),
},
};
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 8a1993b48f..fc43f54db1 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -22,7 +22,6 @@ export enum ImmichHeader {
SharedLinkKey = 'x-immich-share-key',
SharedLinkSlug = 'x-immich-share-slug',
Checksum = 'x-immich-checksum',
- Cid = 'x-immich-cid',
}
export enum ImmichQuery {
diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts
index f91bb2b122..e83ee18fea 100644
--- a/server/src/middleware/global-exception.filter.ts
+++ b/server/src/middleware/global-exception.filter.ts
@@ -20,14 +20,16 @@ export class GlobalExceptionFilter implements ExceptionFilter {
const response = ctx.getResponse();
const { status, body } = this.fromError(error);
if (!response.headersSent) {
- response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
+ response.header('X-Correlation-ID', this.cls.getId());
+ response.status(status).json({ ...body, statusCode: status });
}
}
handleError(res: Response, error: Error) {
const { status, body } = this.fromError(error);
if (!res.headersSent) {
- res.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
+ res.header('X-Correlation-ID', this.cls.getId());
+ res.status(status).json({ ...body, statusCode: status });
}
}
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
index 97ec3f1cdc..b77f9fc8c3 100644
--- a/server/src/repositories/config.repository.ts
+++ b/server/src/repositories/config.repository.ts
@@ -15,7 +15,6 @@ import { EnvSchema } from 'src/dtos/env.dto';
import {
DatabaseExtension,
ImmichEnvironment,
- ImmichHeader,
ImmichTelemetry,
ImmichWorker,
LogFormat,
@@ -301,11 +300,11 @@ const getEnv = (): EnvData => {
mount: true,
generateId: true,
setup: (cls, req: Request, res: Response) => {
- const headerValues = req.headers[ImmichHeader.Cid];
+ const headerValues = req.headers['x-correlation-id'];
const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
const cid = headerValue || cls.get(CLS_ID);
cls.set(CLS_ID, cid);
- res.header(ImmichHeader.Cid, cid);
+ res.header('X-Correlation-ID', cid);
},
},
},
diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts
index dcc4cdd177..adff5703c1 100644
--- a/server/test/medium/responses.ts
+++ b/server/test/medium/responses.ts
@@ -5,43 +5,36 @@ export const errorDto = {
error: 'Unauthorized',
statusCode: 401,
message: 'Authentication required',
- correlationId: expect.any(String),
},
forbidden: {
error: 'Forbidden',
statusCode: 403,
message: expect.any(String),
- correlationId: expect.any(String),
},
missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`,
- correlationId: expect.any(String),
}),
wrongPassword: {
error: 'Bad Request',
statusCode: 400,
message: 'Wrong password',
- correlationId: expect.any(String),
},
invalidToken: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid user token',
- correlationId: expect.any(String),
},
invalidShareKey: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid share key',
- correlationId: expect.any(String),
},
invalidSharePassword: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid password',
- correlationId: expect.any(String),
},
badRequest: (message: any = null) => ({
error: 'Bad Request',
@@ -52,18 +45,15 @@ export const errorDto = {
error: 'Bad Request',
statusCode: 400,
message: expect.stringContaining('Not found or no'),
- correlationId: expect.any(String),
},
incorrectLogin: {
error: 'Unauthorized',
statusCode: 401,
message: 'Incorrect email or password',
- correlationId: expect.any(String),
},
alreadyHasAdmin: {
error: 'Bad Request',
statusCode: 400,
message: 'The server already has an admin',
- correlationId: expect.any(String),
},
};
From 92634f923bc6cb8b84e852d9a0caf64d6a2d068a Mon Sep 17 00:00:00 2001
From: Timon
Date: Tue, 28 Apr 2026 23:54:54 +0200
Subject: [PATCH 015/140] refactor(server)!: remove redundant error and
statusCode fields from error responses (#28140)
* refactor(server)!: remove redundant error and statusCode fields from error responses
* use enum
* enhance response management
* chore: clean up header
* fix: chaining
* refactor: handle error
* fix e2e tests
---------
Co-authored-by: Jason Rasmussen
---
e2e/src/responses.ts | 26 --------------
e2e/src/specs/server/api/oauth.e2e-spec.ts | 5 +--
server/src/enum.ts | 1 +
.../src/middleware/global-exception.filter.ts | 34 +++++++------------
server/src/repositories/config.repository.ts | 7 ++--
server/test/medium/responses.ts | 22 ------------
server/test/small.factory.ts | 2 --
7 files changed, 18 insertions(+), 79 deletions(-)
diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts
index fcb300828e..2ec7aecb0e 100644
--- a/e2e/src/responses.ts
+++ b/e2e/src/responses.ts
@@ -2,68 +2,42 @@ import { expect } from 'vitest';
export const errorDto = {
unauthorized: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Authentication required',
},
unauthorizedWithMessage: (message: string) => ({
- error: 'Unauthorized',
- statusCode: 401,
message,
}),
forbidden: {
- error: 'Forbidden',
- statusCode: 403,
message: expect.any(String),
},
missingPermission: (permission: string) => ({
- error: 'Forbidden',
- statusCode: 403,
message: `Missing required permission: ${permission}`,
}),
wrongPassword: {
- error: 'Bad Request',
- statusCode: 400,
message: 'Wrong password',
},
invalidToken: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid user token',
},
invalidShareKey: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid share key',
},
passwordRequired: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Password required',
},
badRequest: (message: any = null) => ({
- error: 'Bad Request',
- statusCode: 400,
message: message ?? expect.anything(),
}),
noPermission: {
- error: 'Bad Request',
- statusCode: 400,
message: expect.stringContaining('Not found or no'),
},
incorrectLogin: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Incorrect email or password',
},
alreadyHasAdmin: {
- error: 'Bad Request',
- statusCode: 400,
message: 'The server already has an admin',
},
invalidEmail: {
- error: 'Bad Request',
- statusCode: 400,
message: ['email must be an email'],
},
};
diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts
index 9dcb431a4b..157fdfc84c 100644
--- a/e2e/src/specs/server/api/oauth.e2e-spec.ts
+++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts
@@ -332,9 +332,7 @@ describe(`/oauth`, () => {
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500);
expect(body).toMatchObject({
- error: 'Internal Server Error',
message: 'Failed to finish oauth',
- statusCode: 500,
});
});
@@ -495,11 +493,10 @@ describe(`/oauth`, () => {
});
it('should reject OAuth discovery over HTTP', async () => {
- const { status, body } = await request(app)
+ const { status } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500);
- expect(body).toMatchObject({ statusCode: 500 });
});
});
});
diff --git a/server/src/enum.ts b/server/src/enum.ts
index fc43f54db1..9ba66145bb 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -22,6 +22,7 @@ export enum ImmichHeader {
SharedLinkKey = 'x-immich-share-key',
SharedLinkSlug = 'x-immich-share-slug',
Checksum = 'x-immich-checksum',
+ CorrelationId = 'X-Correlation-ID',
}
export enum ImmichQuery {
diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts
index e83ee18fea..f331df9147 100644
--- a/server/src/middleware/global-exception.filter.ts
+++ b/server/src/middleware/global-exception.filter.ts
@@ -2,6 +2,7 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/co
import { Response } from 'express';
import { ClsService } from 'nestjs-cls';
import { ZodSerializationException, ZodValidationException } from 'nestjs-zod';
+import { ImmichHeader } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { logGlobalError } from 'src/utils/logger';
import { ZodError } from 'zod';
@@ -16,20 +17,13 @@ export class GlobalExceptionFilter implements ExceptionFilter {
}
catch(error: Error, host: ArgumentsHost) {
- const ctx = host.switchToHttp();
- const response = ctx.getResponse();
- const { status, body } = this.fromError(error);
- if (!response.headersSent) {
- response.header('X-Correlation-ID', this.cls.getId());
- response.status(status).json({ ...body, statusCode: status });
- }
+ this.handleError(host.switchToHttp().getResponse(), error);
}
handleError(res: Response, error: Error) {
const { status, body } = this.fromError(error);
if (!res.headersSent) {
- res.header('X-Correlation-ID', this.cls.getId());
- res.status(status).json({ ...body, statusCode: status });
+ res.header(ImmichHeader.CorrelationId, this.cls.getId()).status(status).json(body);
}
}
@@ -38,26 +32,24 @@ export class GlobalExceptionFilter implements ExceptionFilter {
if (error instanceof HttpException) {
const status = error.getStatus();
- let body = error.getResponse();
-
- // unclear what circumstances would return a string
- if (typeof body === 'string') {
- body = { message: body };
- }
+ const response = error.getResponse();
+ const body: Record =
+ typeof response === 'string' ? { message: response } : { ...(response as object) };
// handle both request and response validation errors
if (error instanceof ZodValidationException || error instanceof ZodSerializationException) {
const zodError = error.getZodError();
if (zodError instanceof ZodError && zodError.issues.length > 0) {
- body = {
- message: zodError.issues.map((issue) =>
- issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message,
- ),
- error: 'Bad Request',
- };
+ body['message'] = zodError.issues.map((issue) =>
+ issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message,
+ );
}
}
+ // remove fields that duplicate the HTTP response line or will be reformatted in a later step
+ delete body['error'];
+ delete body['statusCode'];
+ delete body['errors'];
return { status, body };
}
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
index b77f9fc8c3..f6bae39897 100644
--- a/server/src/repositories/config.repository.ts
+++ b/server/src/repositories/config.repository.ts
@@ -15,6 +15,7 @@ import { EnvSchema } from 'src/dtos/env.dto';
import {
DatabaseExtension,
ImmichEnvironment,
+ ImmichHeader,
ImmichTelemetry,
ImmichWorker,
LogFormat,
@@ -300,11 +301,9 @@ const getEnv = (): EnvData => {
mount: true,
generateId: true,
setup: (cls, req: Request, res: Response) => {
- const headerValues = req.headers['x-correlation-id'];
- const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
- const cid = headerValue || cls.get(CLS_ID);
+ const cid = req.header(ImmichHeader.CorrelationId) || cls.get(CLS_ID);
cls.set(CLS_ID, cid);
- res.header('X-Correlation-ID', cid);
+ res.header(ImmichHeader.CorrelationId, cid);
},
},
},
diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts
index adff5703c1..2fcab5b2dc 100644
--- a/server/test/medium/responses.ts
+++ b/server/test/medium/responses.ts
@@ -2,58 +2,36 @@ import { expect } from 'vitest';
export const errorDto = {
unauthorized: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Authentication required',
},
forbidden: {
- error: 'Forbidden',
- statusCode: 403,
message: expect.any(String),
},
missingPermission: (permission: string) => ({
- error: 'Forbidden',
- statusCode: 403,
message: `Missing required permission: ${permission}`,
}),
wrongPassword: {
- error: 'Bad Request',
- statusCode: 400,
message: 'Wrong password',
},
invalidToken: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid user token',
},
invalidShareKey: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid share key',
},
invalidSharePassword: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid password',
},
badRequest: (message: any = null) => ({
- error: 'Bad Request',
- statusCode: 400,
message: message ?? expect.anything(),
}),
noPermission: {
- error: 'Bad Request',
- statusCode: 400,
message: expect.stringContaining('Not found or no'),
},
incorrectLogin: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Incorrect email or password',
},
alreadyHasAdmin: {
- error: 'Bad Request',
- statusCode: 400,
message: 'The server already has an admin',
},
};
diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts
index e4001d18ab..ad4dbf7524 100644
--- a/server/test/small.factory.ts
+++ b/server/test/small.factory.ts
@@ -246,8 +246,6 @@ export const factory = {
date: newDate,
responses: {
badRequest: (message: any = null) => ({
- error: 'Bad Request',
- statusCode: 400,
message: message ?? expect.anything(),
}),
},
From 7dc84f56c02faa73e36429c53c3d661e98b2c352 Mon Sep 17 00:00:00 2001
From: Yaros
Date: Wed, 29 Apr 2026 12:11:33 +0200
Subject: [PATCH 016/140] fix(web): double video playback on map timeline
(#28090)
---
.../(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 0d356fdb5c..c5a514bd96 100644
--- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -86,7 +86,7 @@