immich-app/immich · warning · BadRequestException
Failed to save shared link
Error message
Failed to save shared link
What it means
SharedLinkService.create() wraps the repository write in try/catch and routes errors through handleError(). If the caught error is a PostgresError whose constraint_name is 'shared_link_slug_uq' (unique constraint on the slug column), it is converted to BadRequestException 'Failed to save shared link' (shared-link.service.ts:115, HTTP 400). The message is generic but the root cause is a duplicate slug.
Source
Thrown at server/src/services/shared-link.service.ts:115
description: dto.description || null,
password: dto.password,
expiresAt: dto.expiresAt || null,
allowUpload: dto.allowUpload ?? true,
allowDownload: dto.showMetadata === false ? false : (dto.allowDownload ?? true),
showExif: dto.showMetadata ?? true,
slug: dto.slug || null,
});
return mapSharedLink(sharedLink, { stripAssetMetadata: false });
} catch (error) {
this.handleError(error);
}
}
private handleError(error: unknown): never {
if ((error as PostgresError).constraint_name === 'shared_link_slug_uq') {
this.logger.debug('Shared link with this slug already exists');
throw new BadRequestException('Failed to save shared link');
}
throw error;
}
async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
await this.findOrFail(auth.user.id, id);
try {
const sharedLink = await this.sharedLinkRepository.update({
id,
userId: auth.user.id,
description: dto.description,
password: dto.password,
expiresAt: dto.expiresAt,
allowUpload: dto.allowUpload,
allowDownload: dto.allowDownload,
showExif: dto.showMetadata,
slug: dto.slug || null,
});View on GitHub (pinned to 199723261c)
Solutions
- Omit dto.slug to let the server generate a unique random slug.
- If supplying a custom slug, make it unique (append a random suffix or timestamp).
- On this 400, prompt the user to choose a different slug and retry.
Example fix
// before - colliding slug
await create({ type, albumId, slug: 'summer' });
// after - server-generated unique slug
await create({ type, albumId }); Defensive patterns
Strategy: try-catch
Validate before calling
// If supplying a custom slug, ensure uniqueness; otherwise omit it.
const payload = { ...dto };
if (payload.slug != null && !isSlugAvailable(payload.slug)) {
delete payload.slug; // let the server generate one
}
await sharedLinkApi.create(payload); Type guard
const hasCustomSlug = (dto: SharedLinkCreateDto): boolean => typeof dto.slug === 'string' && dto.slug.length > 0;
Try / catch
try {
await sharedLinkApi.create(dto);
} catch (e) {
if (e instanceof BadRequestException && /failed to save shared link/i.test(e.message)) {
// likely a slug collision — retry with a server-generated slug
const { slug, ...rest } = dto;
return sharedLinkApi.create(rest);
} else throw e;
} Prevention
- Omit slug to get a unique generated value, or append a random suffix to custom slugs.
- On collision, retry without the slug rather than failing the whole action.
When it happens
Trigger: Creating a shared link with a dto.slug that already exists for another shared link (the slug is globally unique), causing the DB to reject the insert and trip the shared_link_slug_uq constraint.
Common situations: User-chosen custom slugs colliding with existing ones, regenerated slugs after a failed attempt, or a front-end defaulting to a common slug like 'photos'.
Related errors
- No vector extension found. Available extensions: ${VECTOR_EX
- ${EXTENSION_NAMES[extension]} extension is not installed
- No available version for ${EXTENSION_NAMES[extension]} exten
- Invalid CLIP dimension size: ${dimSize}
- Migration "${missing[1]}" was already applied to this databa
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/a46e60f144562a8e.
Report an issue: GitHub.