calcom/cal.diy · error · BadRequestException

Please specify both ooo start and end time.

Error message

Please specify both ooo start and end time.

What it means

Thrown by OooService.isStartBeforeEnd() when exactly one of start or end is present ((end && !start) || (start && !end)). The OOO contract requires both bounds together; a partial date range is rejected as ambiguous. Both-absent is allowed (used by no-op updates) and is the only way to skip the check.

Source

Thrown at apps/api/v2/src/modules/ooo/services/ooo.service.ts:46

@Injectable()
export class UserOOOService {
  constructor(
    private readonly oooRepository: UserOOORepository,
    private readonly usersRepository: UsersRepository
  ) {}

  formatOooReason(ooo: OutOfOfficeEntry) {
    return {
      ...ooo,
      reason: ooo.reasonId
        ? OOO_REASON_ID_TO_REASON[ooo.reasonId as keyof typeof OOO_REASON_ID_TO_REASON]
        : OOO_REASON_ID_TO_REASON[1],
    };
  }

  isStartBeforeEnd(start?: Date, end?: Date) {
    if ((end && !start) || (start && !end)) {
      throw new BadRequestException("Please specify both ooo start and end time.");
    }

    if (start && end) {
      if (start.getTime() > end.getTime()) {
        throw new BadRequestException("Start date must be before end date.");
      }
    }
    return true;
  }

  async checkUserEligibleForRedirect(userId: number, toUserId?: number) {
    if (toUserId) {
      const user = await this.usersRepository.findUserOOORedirectEligible(userId, toUserId);
      if (!user) {
        throw new BadRequestException("Cannot redirect to this user.");
      }
    }
  }

View on GitHub (pinned to 176037d0af)

Solutions

  1. Always send start and end together in any OOO create/update payload.
  2. When patching, if the user only changed one bound, re-read the existing entry and send both the new and the unchanged bound.
  3. Omit both start and end from the patch body entirely if you do not intend to change the time window.

Example fix

// before
await api.patch(`/ooo/${id}`, { end: newEnd }); // missing start → 400
// after
const current = await api.get(`/ooo/${id}`);
await api.patch(`/ooo/${id}`, { start: current.start, end: newEnd });
Defensive patterns

Strategy: validation

Validate before calling

function buildOooWindow(start?: Date, end?: Date) {
  if ((start && !end) || (end && !start))
    throw new Error('Send both start and end, or neither.');
  const out: { start?: string; end?: string } = {};
  if (start && end) { out.start = start.toISOString(); out.end = end.toISOString(); }
  return out;
}

Type guard

const hasBothOrNeither = (start?: Date, end?: Date): boolean =>
  (start && end ? true : !start && !end);

Try / catch

try { await api.patch(`/ooo/${id}`, patch); }
catch (e) { if (/both ooo start and end/i.test(e.message)) { /* re-send both bounds */ } else throw e; }

Prevention

When it happens

Trigger: PATCH /v2/ooo/:id sending only { "start": "..." } or only { "end": "..." }; a create/update caller that conditionally includes one bound based on UI state but forgets the other.

Common situations: A 'reschedule OOO' feature that lets the user move only the end date; merging a partial patch object via spread that drops an undefined field; deserialization that omits null fields.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/183bcda4f4f01746. Report an issue: GitHub.