{"record":{"id":"6f4b4755f460df58","repo":"immich-app/immich","slug":"shared-link-is-not-password-protected","errorCode":null,"errorMessage":"Shared link is not password protected","messagePattern":"Shared link is not password protected","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/shared-link.service.ts","lineNumber":36,"sourceCode":"@Injectable()\nexport class SharedLinkService extends BaseService {\n  async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {\n    return this.sharedLinkRepository\n      .getAll({ userId: auth.user.id, id, albumId })\n\n      .then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));\n  }\n\n  async login(auth: AuthDto, dto: SharedLinkLoginDto) {\n    if (!auth.sharedLink) {\n      throw new ForbiddenException();\n    }\n\n    const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);\n    const { id, password } = sharedLink;\n\n    if (!password) {\n      throw new BadRequestException('Shared link is not password protected');\n    }\n\n    if (password !== dto.password) {\n      throw new UnauthorizedException('Invalid password');\n    }\n\n    return {\n      sharedLink: mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }),\n      token: this.asToken({ id, password }),\n    };\n  }\n\n  async getMine(auth: AuthDto, authTokens: string[]) {\n    if (!auth.sharedLink) {\n      throw new ForbiddenException();\n    }\n\n    const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/shared-link.service.ts#L18-L54","documentation":"In SharedLinkService.login(), after the shared link is resolved, if it has no password set the service throws BadRequestException 'Shared link is not password protected' (shared-link.service.ts:36, HTTP 400). Logging into a link that requires no password is a category error.","triggerScenarios":"POST to the shared-link login endpoint for a shared link whose password field is null/empty. The caller should not attempt a password exchange for an unprotected link.","commonSituations":"Frontend always invoking login regardless of whether the link is password-protected, stale client state assuming a password prompt, or a link whose password was cleared via update but the client still tries to log in.","solutions":["Before showing a password prompt or calling login, check the shared link's password-protected flag from getMine/get and skip login for unprotected links.","If the link was meant to be protected, set a password via PATCH /shared-links/:id (update).","Clear client-side state that assumes a password flow for this link."],"exampleFix":"// before - always login\nawait sharedLinkApi.login({ password });\n// after - only when protected\nif (sharedLink.password) {\n  await sharedLinkApi.login({ password });\n}","handlingStrategy":"validation","validationCode":"// Resolve the link first; only login if it is password-protected.\nconst link = await sharedLinkApi.getMine();\nif (!link.password) {\n  // unprotected link — no login needed, proceed directly\n  return link;\n}\nreturn sharedLinkApi.login({ password });","typeGuard":"const isPasswordProtected = (link: { password?: string | null }): boolean => !!link.password;","tryCatchPattern":"try {\n  await sharedLinkApi.login(dto);\n} catch (e) {\n  if (e instanceof BadRequestException && /not password protected/i.test(e.message)) {\n    // skip login; this link needs no password\n    return sharedLinkApi.getMine();\n  } else throw e;\n}","preventionTips":["Gate the password prompt on the link's password flag from getMine/get.","Re-fetch link metadata if the owner may have changed protection."],"tags":["shared-link","password","validation"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}