immich-app/immich · error · BadRequestException

Failed to save shared link

Error message

Failed to save shared link

What it means

handleError wraps shared-link writes and converts a Postgres unique-constraint violation on shared_link_slug_uq (duplicate slug/key) into BadRequestException('Failed to save shared link'); any other error is rethrown untouched. It is invoked by create and update.

Solutions

  1. Choose a different slug/key value for the link and retry.
  2. On create, retry — the service will generate a fresh slug.
  3. If collisions recur programmatically, generate slugs with a random suffix.
  4. Inspect server debug logs ('Shared link with this slug already exists') to confirm the cause.

Example fix

// before
await api.updateSharedLink(id, { slug: existingSlug });
// after
await api.updateSharedLink(id, { slug: existingSlug + '-2' });
Defensive patterns

Strategy: retry

Try / catch

try {
  return await api.createSharedLink(dto);
} catch (e) {
  if (e.status === 400 && e.message === 'Failed to save shared link') {
    // duplicate slug: retry with a modified slug
    return await api.createSharedLink({ ...dto, slug: `${dto.slug}-${Date.now()}` });
  }
  throw e;
}

Prevention

When it happens

Trigger: Creating or updating a shared link whose generated/edited slug collides with an existing row, violating the shared_link_slug_uq unique constraint in Postgres.

Common situations: Manually editing a link's slug/slug-like key to one already in use; a slug-collision in the generated short key; retries inserting the same link twice.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/a46e60f144562a8e. Report an issue: GitHub.

Appendix: 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 e55ac299a4)