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
- Query only your own locked timeline (omit userId or use your own user id)
- Change the requested visibility to archive/undefined instead of locked when viewing another user
- 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
- Never request locked visibility for a userId other than your own
- Guard UI toggles that combine partner/user switching with locked filters
- Default to omitting visibility when querying other users
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
- Admin status can only be changed by another admin
- Album must have an owner
- Cannot delete your own account
- Live photo video does not belong to the user
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)