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

  1. Omit dto.slug to let the server generate a unique random slug.
  2. If supplying a custom slug, make it unique (append a random suffix or timestamp).
  3. 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

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


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/a46e60f144562a8e. Report an issue: GitHub.