immich-app/immich · warning · BadRequestException

Shared link access is only allowed in combination with an al

Error message

Shared link access is only allowed in combination with an albumIds filter

What it means

Thrown by SearchService.searchMetadata when the caller authenticated via a shared link (auth.sharedLink is set) but dto.albumIds is empty. Shared-link access is scoped to the album the link belongs to, so without an albumIds filter the search would have no bounded scope. BadRequestException -> HTTP 400.

Source

Thrown at server/src/services/search.service.ts:81

  }

  async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
    if (dto.visibility === AssetVisibility.Locked) {
      requireElevatedPermission(auth);
    }

    let checksum: Buffer | undefined;
    if (dto.checksum) {
      const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
      checksum = Buffer.from(dto.checksum, encoding);
    }

    let userIds: string[] | undefined;

    if (dto.albumIds && dto.albumIds.length > 0) {
      await this.requireAccess({ auth, ids: dto.albumIds, permission: Permission.AlbumRead });
    } else if (auth.sharedLink) {
      throw new BadRequestException('Shared link access is only allowed in combination with an albumIds filter');
    } else {
      userIds = await this.getUserIdsToSearch(auth, dto.visibility);
    }

    const page = dto.page ?? 1;
    const size = dto.size || 250;
    const { hasNextPage, items } = await this.searchRepository.searchMetadata(
      { page, size },
      {
        ...dto,
        checksum,
        visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
        userIds,
        orderDirection: dto.order ?? AssetOrder.Desc,
      },
    );

    return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });

View on GitHub (pinned to 199723261c)

Solutions

  1. When calling search from a shared-link context, always include the shared album's id in albumIds.
  2. Detect shared-link auth client-side and pre-fill the albumIds filter.
  3. Provide a separate, scoped search endpoint for shared links that injects albumIds server-side.

Example fix

// before
} else if (auth.sharedLink) {
  throw new BadRequestException('Shared link access is only allowed in combination with an albumIds filter');
}

// after (auto-scope to the shared link's album)
} else if (auth.sharedLink) {
  albumIds = [auth.sharedLink.albumId];
}
Defensive patterns

Strategy: validation

Validate before calling

// In a shared-link context, always pass the shared album's id.
if (auth.sharedLink) {
  dto.albumIds = [auth.sharedLink.albumId];
}
await searchService.searchMetadata(auth, dto);

Type guard

const isSharedLinkAuth = (auth: AuthDto): boolean => !!auth.sharedLink;

Try / catch

try {
  await searchService.searchMetadata(auth, dto);
} catch (e) {
  if (e instanceof BadRequestException && /shared link/i.test(e.message)) {
    // retry with the shared album's id injected
    dto.albumIds = [auth.sharedLink!.albumId];
    return searchService.searchMetadata(auth, dto);
  }
  throw e;
}

Prevention

When it happens

Trigger: GET /search/metadata from a public/shared-link session without passing ?albumIds=<the shared album's id>; a shared-link client calling the global search endpoint.

Common situations: Shared-link UI (public gallery) issues a metadata search that omits the album filter; a third-party embed using a shared link token calls the search API directly.

Related errors


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