nestjs/nest · error · ForbiddenException

Forbidden resource

Error message

Forbidden resource

What it means

After every authorization guard runs, the framework requires an explicit verdict: a guard either throws an HttpException or returns false; returning false makes the router throw ForbiddenException('Forbidden resource'), which surfaces as HTTP 403. This is the designed authorization-denied response, not a framework defect — the default message is generic because the guard chose not to explain itself.

Source

Thrown at packages/core/router/router-execution-context.ts:390

    );
  }

  public createGuardsFn<TContext extends string = ContextType>(
    guards: CanActivate[],
    instance: Controller,
    callback: (...args: any[]) => any,
    contextType?: TContext,
  ): ((args: any[]) => Promise<void>) | null {
    const canActivateFn = async (args: any[]) => {
      const canActivate = await this.guardsConsumer.tryActivate<TContext>(
        guards,
        args,
        instance,
        callback,
        contextType,
      );
      if (!canActivate) {
        throw new ForbiddenException(FORBIDDEN_MESSAGE);
      }
    };
    return guards.length ? canActivateFn : null;
  }

  public createPipesFn(
    pipes: PipeTransform[],
    paramsOptions: (ParamProperties & { metatype?: any })[],
  ) {
    const pipesFn = async <TRequest, TResponse>(
      args: any[],
      req: TRequest,
      res: TResponse,
      next: Function,
    ) => {
      const resolveParamValue = async (
        param: ParamProperties & { metatype?: any },
      ) => {

View on GitHub (pinned to dd75d7bd8c)

Solutions

  1. On the client: ensure credentials are sent (Authorization header / withCredentials cookie) and are valid for this route.
  2. In the guard, replace `return false` with a thrown, descriptive exception: `throw new ForbiddenException('You do not have the admin role')` or `UnauthorizedException()` for anonymous access.
  3. Whitelist public routes in the global guard (`Reflector` + `@Public()` decorator set) instead of returning false everywhere.
  4. Fix accidental falsy returns: `return user?.role === 'admin'` rather than returning the role itself.

Example fix

// before
@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(ctx: ExecutionContext): boolean {
    return false; // generic 'Forbidden resource'
  }
}

// after
import { ForbiddenException } from '@nestjs/common';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(ctx: ExecutionContext): boolean {
    const { user } = ctx.switchToHttp().getRequest();
    if (!user) throw new UnauthorizedException('Authentication required');
    if (!user.roles.includes('admin'))
      throw new ForbiddenException('Admin role required');
    return true;
  }
}
Defensive patterns

Strategy: try-catch

Try / catch

// Give the generic 403 a real message at the boundary
import { Catch, ExceptionFilter, HttpException, ArgumentsHost } from '@nestjs/common';

@Catch(HttpException)
export class AuthResponseFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse();
    if (exception.getStatus?.() === 403 && exception.message === 'Forbidden resource') {
      return res.status(403).json({ statusCode: 403, message: 'Insufficient permissions' });
    }
    const body = exception.getResponse();
    res.status(exception.getStatus()).json(typeof body === 'string' ? { message: body } : body);
  }
}

Prevention

When it happens

Trigger: A global or controller/method guard's `canActivate()` returns false (missing/invalid JWT, insufficient role, csrf failure, tenant mismatch); a guard returns a falsy value accidentally (`return user.role` where role is undefined); auth integration where the guard runs before the token is attached by the passport strategy.

Common situations: RolesGuard checking `user.roles.includes(requiredRole)` for unauthenticated requests where user is undefined; API clients hitting protected endpoints without the Authorization header; cookie-based auth in cross-origin setups where the cookie is not sent; new endpoints forgotten to be whitelisted in a global guard.

Understand the failure class

Related errors


AI-assisted analysis of nestjs/nest@dd75d7bd8c (2026-08-21). Data as JSON: /api/errors/52910f3225e201db. Report an issue: GitHub.