RocketChat/Rocket.Chat · error · Error
error-not-authorized
Error message
error-not-authorized
What it means
findBusinessHours is the Enterprise helper behind the livechat business-hours listing API (apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts). It gates on the 'view-livechat-business-hours' permission via hasPermissionAsync and throws a plain Error('error-not-authorized') before the paginated LivechatBusinessHours query ever runs. The throw means the authenticated user's role set does not include that specific permission; other livechat permissions do not cover it.
Solutions
- Grant 'view-livechat-business-hours' to the caller's role under Administration > Permissions (it is normally held by admin and livechat-manager style roles).
- When calling findBusinessHours server-side, verify you are passing the authenticated requester's userId, not a system/undefined id.
- If wrapping this helper in a custom route, map Error('error-not-authorized') to HTTP 403 so clients get a meaningful status instead of a generic 500.
Example fix
// before
const { businessHours } = await findBusinessHours(uid, { offset, count }); // 500-ish raw error for under-privileged callers
// after - pre-check the same permission in the calling route
if (!(await hasPermissionAsync(uid, 'view-livechat-business-hours'))) {
return API.v1.unauthorized();
}
const { businessHours } = await findBusinessHours(uid, { offset, count }); Defensive patterns
Strategy: validation
Validate before calling
import { hasPermissionAsync } from '../../../../server/lib/authorization/hasPermission';
if (!(await hasPermissionAsync(userId, 'view-livechat-business-hours'))) {
// skip the call or return 403; do not invoke findBusinessHours
} Try / catch
try {
const res = await findBusinessHours(userId, pagination, name);
} catch (e: any) {
if (e instanceof Error && e.message === 'error-not-authorized') return API.v1.unauthorized(); // 403
throw e;
} Prevention
- Add 'view-livechat-business-hours' to any custom role that must list business hours before shipping it.
- Smoke-test omnichannel EE endpoints with the least-privileged account that should have access.
- Prefer route-level permissionsRequired so authorization failures surface as 403 instead of raw errors.
When it happens
Trigger: Calling GET livechat/business-hours (which delegates to findBusinessHours(userId, pagination, name)) with a session whose roles lack 'view-livechat-business-hours' - e.g. a livechat-agent, a plain user, or a userId resolving to no roles. The check is the first statement, so pagination/sort/name filter parameters are irrelevant; authorization alone decides.
Common situations: A custom 'business hours manager' role was created without the permission; testing the endpoint with an agent or manager account whose role lacks it; permissions trimmed or reset during a workspace migration/upgrade; assuming 'view-livechat-manager' or admin-adjacent roles cover business hours (they do not - the permission is separate).
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 RocketChat/Rocket.Chat@2a7de45707 (2026-08-18).
Data as JSON: /api/errors/0ed87b989c030ef1.
Report an issue: GitHub.
Appendix: source
Thrown at apps/meteor/ee/server/lib/omnichannel/business-hour/lib/business-hour.ts:14
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, LivechatDepartment } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/tools';
import { hasPermissionAsync } from '../../../../../../server/lib/authorization/hasPermission';
import type { IPaginatedResponse, IPagination } from '../../../../api/v1/omnichannel/lib/definition';
interface IResponse extends IPaginatedResponse {
businessHours: ILivechatBusinessHour[];
}
export async function findBusinessHours(userId: string, { offset, count, sort }: IPagination, name?: string): Promise<IResponse> {
if (!(await hasPermissionAsync(userId, 'view-livechat-business-hours'))) {
throw new Error('error-not-authorized');
}
const query = {};
if (name) {
const filterReg = new RegExp(escapeRegExp(name), 'i');
Object.assign(query, { name: filterReg });
}
const { cursor, totalCount } = LivechatBusinessHours.findPaginated(query, {
sort: sort || { name: 1 },
skip: offset,
limit: count,
});
const [businessHours, total] = await Promise.all([cursor.toArray(), totalCount]);
// add departments to businessHours
const businessHoursWithDepartments = await Promise.all(
businessHours.map(async (businessHour) => {
const currentDepartments = await LivechatDepartment.findByBusinessHourId(businessHour._id, {View on GitHub (pinned to 2a7de45707)