immich-app/immich · warning · BadRequestException

Partner already exists

Error message

Partner already exists

What it means

Thrown by PartnerService.create when partnerRepository.get({ sharedById, sharedWithId }) already returns a row. Represents an idempotency violation: the authenticated user is already sharing their library with the requested sharedWithId. BadRequestException -> HTTP 400.

Source

Thrown at server/src/services/partner.service.ts:16

import { BadRequestException, Injectable } from '@nestjs/common';
import { Partner } from 'src/database';
import { AuthDto } from 'src/dtos/auth.dto';
import { PartnerCreateDto, PartnerResponseDto, PartnerSearchDto, PartnerUpdateDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto';
import { Permission } from 'src/enum';
import { PartnerDirection, PartnerIds } from 'src/repositories/partner.repository';
import { BaseService } from 'src/services/base.service';

@Injectable()
export class PartnerService extends BaseService {
  async create(auth: AuthDto, { sharedWithId }: PartnerCreateDto): Promise<PartnerResponseDto> {
    const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
    const exists = await this.partnerRepository.get(partnerId);
    if (exists) {
      throw new BadRequestException(`Partner already exists`);
    }

    const user = await this.userRepository.get(sharedWithId, {});
    if (!user) {
      this.logger.debug('Partner creation failed: user not found');
      throw new BadRequestException('Invalid user');
    }

    const partner = await this.partnerRepository.create(partnerId);
    return this.mapPartner(partner, PartnerDirection.SharedBy);
  }

  async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
    const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
    const partner = await this.partnerRepository.get(partnerId);
    if (!partner) {
      throw new BadRequestException('Partner not found');
    }

View on GitHub (pinned to 199723261c)

Solutions

  1. Treat 400 'Partner already exists' as success if the goal is to ensure the share exists (idempotent create).
  2. Guard the UI: fetch the existing partner list first and disable already-shared users.
  3. Catch the 400 client-side and refresh the partner list instead of retrying the POST.

Example fix

// before
const exists = await this.partnerRepository.get(partnerId);
if (exists) {
  throw new BadRequestException(`Partner already exists`);
}

// after (return the existing row so the endpoint is idempotent)
const exists = await this.partnerRepository.get(partnerId);
if (exists) {
  return this.mapPartner(exists, PartnerDirection.SharedBy);
}
Defensive patterns

Strategy: validation

Validate before calling

// Before creating, check the existing partnership for this caller.
const existing = await partnerService.search(auth, { direction: 'shared-by' });
if (existing.some((p) => p.id === sharedWithId)) {
  // already shared; treat as success, do not POST /partner again
  return existing.find((p) => p.id === sharedWithId)!;
}

Type guard

const isPartner = (p: PartnerResponseDto | null | undefined): p is PartnerResponseDto =>
  !!p && typeof p.id === 'string';

Try / catch

try {
  await partnerService.create(auth, { sharedWithId });
} catch (e) {
  if (e instanceof BadRequestException && /already exists/i.test(e.message)) {
    // idempotent success
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /partner with a sharedWithId that the caller already shares with; double submission of the same partner-create form; a UI that re-issues the request on retry.

Common situations: User clicks 'Share' twice; client retries after a timeout even though the first call succeeded; race condition where two parallel requests both pass the exists-check before either inserts.

Related errors


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