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 by TimelineService.timeBucketChecks when a caller requests a locked-visibility asset timeline for a user other than themselves. Locked assets are a per-user restricted visibility; even with TimelineRead access, only the owner may view their locked timeline. It is a 400 BadRequest enforcing ownership of locked content.

Solutions

  1. Query only your own locked timeline (omit userId or use your own user id)
  2. Change the requested visibility to archive/undefined instead of locked when viewing another user
  3. Remove the visibility=locked filter when fetching partner/shared timelines

Example fix

// before
await api.getTimeBuckets({ userId: partnerId, visibility: AssetVisibility.Locked });
// after
await api.getTimeBuckets({ userId: partnerId, visibility: AssetVisibility.Archive });
Defensive patterns

Strategy: validation

Validate before calling

if (visibility === 'locked' && userId && userId !== auth.user.id) {
  throw new Error('cannot view another user\'s locked timeline');
}

Try / catch

try { await api.getTimeBuckets(params); } catch (e) {
  if (e.response?.status === 400 && /locked timeline/.test(e.response?.data?.message ?? '')) {
    return api.getTimeBuckets({ ...params, visibility: undefined });
  }
  throw e;
}

Prevention

When it happens

Trigger: GET /api/timeline/buckets (or a single bucket) with userId set to another user's ID and visibility=locked. The requireAccess(TimelineRead) check may pass (e.g. admin/partner access), but the explicit ownership comparison dto.userId !== auth.user.id still rejects the request.

Common situations: Shared album or partner flows that pass a partner's userId while visibility defaults to or is explicitly set to locked; admin dashboards iterating all users including locked visibility; copy-pasted queries with hardcoded visibility filters.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/cc80d8f5919a49c2. Report an issue: GitHub.

Appendix: 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 e55ac299a4)