immich-app/immich · error · BadRequestException

You may not access another user's locked timeline

Error message

You may not access another user's locked timeline

What it means

Thrown (as BadRequestException) by TimelineService when the requested visibility is AssetVisibility.Locked and the target userId is not the authenticated user. Locked timelines are private by design; even admins/partners cannot browse another user's locked assets through the timeline endpoint.

Source

Thrown at server/src/services/timeline.service.ts:64

  private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
    if (dto.visibility === AssetVisibility.Locked) {
      requireElevatedPermission(auth);
    }

    if (dto.albumId) {
      await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
    } else {
      dto.userId ||= auth.user.id;
    }

    if (dto.userId) {
      await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [dto.userId] });
      if (dto.visibility === AssetVisibility.Archive) {
        await this.requireAccess({ auth, permission: Permission.ArchiveRead, ids: [dto.userId] });
      }
      if (dto.visibility === AssetVisibility.Locked && dto.userId !== auth.user.id) {
        throw new BadRequestException("You may not access another user's locked timeline");
      }
    }

    if (dto.tagId) {
      await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.tagId] });
    }

    if (auth.sharedLink && !auth.sharedLink.showExif) {
      dto.withCoordinates = false;
    }

    if (dto.withPartners) {
      const isRequestedLocked = dto.visibility === AssetVisibility.Locked;
      const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
      const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
      const isRequestedTrash = dto.isTrashed === true;

      if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {

View on GitHub (pinned to 199723261c)

Solutions

  1. Omit visibility=Locked when querying another user; locked assets are only self-visible.
  2. If you need the current user's locked items, ensure dto.userId equals auth.user.id (or omit userId).
  3. Use the admin asset list endpoint if a true audit of locked assets is required.

Example fix

// before
buckets({ userId: otherUserId, visibility: AssetVisibility.Locked });
// after
buckets({ userId: auth.user.id, visibility: AssetVisibility.Locked });
Defensive patterns

Strategy: validation

Validate before calling

function canViewLocked(authUserId: string, targetUserId: string | undefined, vis: AssetVisibility) {
  return vis !== AssetVisibility.Locked || targetUserId === undefined || targetUserId === authUserId;
}
if (!canViewLocked(auth.user.id, dto.userId, dto.visibility)) { /* hide the locked filter */ }

Try / catch

try { await timelineApi.buckets(dto); }
catch (e) {
  if (e instanceof BadRequestException && /locked timeline/.test(e.message)) {
    // clear visibility=Locked when userId != self
  }
}

Prevention

When it happens

Trigger: GET timeline/buckets or timeline/search with visibility=Locked and userId pointing at a different user than auth.user.id.

Common situations: Admin tooling tries to audit locked assets of a user; partner UI accidentally requests a partner's locked scope; client passed the wrong userId.

Related errors


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