{"record":{"id":"c7fb0cac50990523","repo":"immich-app/immich","slug":"shared-link-access-is-only-allowed-in-combination","errorCode":null,"errorMessage":"Shared link access is only allowed in combination with an albumIds filter","messagePattern":"Shared link access is only allowed in combination with an albumIds filter","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/search.service.ts","lineNumber":81,"sourceCode":"  }\n\n  async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {\n    if (dto.visibility === AssetVisibility.Locked) {\n      requireElevatedPermission(auth);\n    }\n\n    let checksum: Buffer | undefined;\n    if (dto.checksum) {\n      const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';\n      checksum = Buffer.from(dto.checksum, encoding);\n    }\n\n    let userIds: string[] | undefined;\n\n    if (dto.albumIds && dto.albumIds.length > 0) {\n      await this.requireAccess({ auth, ids: dto.albumIds, permission: Permission.AlbumRead });\n    } else if (auth.sharedLink) {\n      throw new BadRequestException('Shared link access is only allowed in combination with an albumIds filter');\n    } else {\n      userIds = await this.getUserIdsToSearch(auth, dto.visibility);\n    }\n\n    const page = dto.page ?? 1;\n    const size = dto.size || 250;\n    const { hasNextPage, items } = await this.searchRepository.searchMetadata(\n      { page, size },\n      {\n        ...dto,\n        checksum,\n        visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),\n        userIds,\n        orderDirection: dto.order ?? AssetOrder.Desc,\n      },\n    );\n\n    return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/search.service.ts#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["When calling search from a shared-link context, always include the shared album's id in albumIds.","Detect shared-link auth client-side and pre-fill the albumIds filter.","Provide a separate, scoped search endpoint for shared links that injects albumIds server-side."],"exampleFix":"// before\n} else if (auth.sharedLink) {\n  throw new BadRequestException('Shared link access is only allowed in combination with an albumIds filter');\n}\n\n// after (auto-scope to the shared link's album)\n} else if (auth.sharedLink) {\n  albumIds = [auth.sharedLink.albumId];\n}","handlingStrategy":"validation","validationCode":"// In a shared-link context, always pass the shared album's id.\nif (auth.sharedLink) {\n  dto.albumIds = [auth.sharedLink.albumId];\n}\nawait searchService.searchMetadata(auth, dto);","typeGuard":"const isSharedLinkAuth = (auth: AuthDto): boolean => !!auth.sharedLink;","tryCatchPattern":"try {\n  await searchService.searchMetadata(auth, dto);\n} catch (e) {\n  if (e instanceof BadRequestException && /shared link/i.test(e.message)) {\n    // retry with the shared album's id injected\n    dto.albumIds = [auth.sharedLink!.albumId];\n    return searchService.searchMetadata(auth, dto);\n  }\n  throw e;\n}","preventionTips":["Inject albumIds server-side for shared-link sessions to remove this footgun.","Detect shared-link auth in the client and pre-fill albumIds.","Document that shared-link search is always album-scoped."],"tags":["search","shared-link","album","authorization","nestjs","validation"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}