calcom/cal.diy · critical · ForbiddenException

No ooo entry id found in request params.

Error message

No ooo entry id found in request params.

What it means

Thrown by the IsUserOOO guard when request.params.oooId is falsy. The guard expects a :oooId route param identifying the specific OOO entry; without it the ownership check cannot run. ForbiddenException (HTTP 403). As with 137, this is a server-side routing defect, not an end-user error.

Source

Thrown at apps/api/v2/src/modules/ooo/guards/is-user-ooo.ts:19

import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Request } from "express";

@Injectable()
export class IsUserOOO implements CanActivate {
  constructor(private oooRepo: UserOOORepository) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<Request>();
    const oooId: string = request.params.oooId;
    const userId: string = request.params.userId;

    if (!userId) {
      throw new ForbiddenException("No user id found in request params.");
    }

    if (!oooId) {
      throw new ForbiddenException("No ooo entry id found in request params.");
    }

    const ooo = await this.oooRepo.getUserOOOByIdAndUserId(Number(oooId), Number(userId));

    if (ooo) {
      return true;
    }

    throw new ForbiddenException("This OOO entry does not belong to this user.");
  }
}

View on GitHub (pinned to 176037d0af)

Solutions

  1. Only apply IsUserOOO to routes that target a specific OOO entry by :oooId (GET/PATCH/DELETE one entry).
  2. Ensure :oooId is present in the path of every guarded route.
  3. In tests, populate request.params.oooId before invoking the guard.
  4. Move create/list routes to a controller not guarded by IsUserOOO.

Example fix

// before
@Controller('users/:userId/ooo')
@UseGuards(IsUserOOO)
export class OOOController {
  @Post()                // no :oooId — guard throws on create
  create() { ... }
}

// after — split guarded vs unguarded routes
@Controller('users/:userId/ooo')
export class OOOController {
  @Post()
  create() { ... }

  @Patch(':oooId')
  @UseGuards(IsUserOOO)
  update() { ... }
}
Defensive patterns

Strategy: validation

Validate before calling

// Only apply IsUserOOO to routes that act on a single OOO entry
function isSingleEntryRoute(httpMethod: string, path: string) {
  return /:oooId/.test(path) && httpMethod !== 'POST';
}
if (!isSingleEntryRoute(method, path)) {
  throw new Error('IsUserOOO should not guard collection/create routes');
}

Type guard

function requestHasOooId(req: { params?: Record<string, unknown> }): req is { params: { oooId: string } } {
  return typeof req.params?.oooId === 'string' && (req.params as any).oooId.length > 0;
}

Try / catch

// Routing bug — fix the route, do not catch at runtime.

Prevention

When it happens

Trigger: An OOO route guarded by IsUserOOO is mounted without a :oooId segment, or the request path doesn't populate the param. E.g. a list/create route inadvertently decorated with the guard.

Common situations: Applying IsUserOOO to a route that doesn't act on a single OOO entry (e.g. POST create); dropping :oooId during a refactor; a test omitting oooId in mocked params.

Related errors


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