Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 0caefd274c fix(middleware): throw AuthException instead of generic Error for missing workspace
https://sonarly.com/issue/32310?type=bug

The REST middleware in `hydrateRestRequest` throws a plain `Error('No data sources found')` when the authenticated token lacks workspace context, resulting in a 500 Internal Server Error instead of a proper 401/403 auth error. This causes Sentry noise and returns misleading error messages to API consumers.

Fix: Replaced the two `throw new Error('No data sources found')` in `hydrateRestRequest` with `throw new AuthException(...)` using `AuthExceptionCode.FORBIDDEN_EXCEPTION`.

**Why this fixes the bug:**
The original plain `Error` had no `status` property and wasn't an `AuthException` instance, so `getStatus()` fell through to return 500. This caused:
1. A 500 Internal Server Error response to the client (misleading — implies server bug)
2. Sentry capture via `shouldCaptureException` which captures all 500s

The new `AuthException` with `FORBIDDEN_EXCEPTION` code is handled by the existing `getStatus()` method at line 148-149, which calls `getAuthExceptionRestStatus()` and returns 403. The `shouldCaptureException` utility skips errors with status < 500, eliminating Sentry noise.

**What changed:**
- Added `AuthExceptionCode` to the existing `AuthException` import
- Line 109: `throw new Error('No data sources found')` → `throw new AuthException('Workspace not found in auth context', AuthExceptionCode.FORBIDDEN_EXCEPTION)`
- Line 113: `throw new Error('No data sources found')` → `throw new AuthException('Workspace database schema is not configured', AuthExceptionCode.FORBIDDEN_EXCEPTION)`

The error messages are now descriptive and actionable — the first tells the client their token lacks workspace context, the second tells them the workspace isn't fully set up.
2026-04-29 03:59:25 +00:00
@@ -5,7 +5,10 @@ import { type Request, type Response } from 'express';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { getAuthExceptionRestStatus } from 'src/engine/core-modules/auth/utils/get-auth-exception-rest-status.util';
@@ -106,11 +109,17 @@ export class MiddlewareService {
: undefined;
if (!data.workspace) {
throw new Error('No data sources found');
throw new AuthException(
'Workspace not found in auth context',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
if (!isNonEmptyString(data.workspace.databaseSchema)) {
throw new Error('No data sources found');
throw new AuthException(
'Workspace database schema is not configured',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
bindDataToRequestObject(data, request, metadataVersion);