Files
twenty/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx
T
sonarly-bot 6ad5616c76 fix(workflow): guard iterator variable schema access
https://sonarly.com/issue/38763?type=bug

Opening a specific workflow record crashes the page with a frontend TypeError when rendering workflow variable labels. The failure is caused by unsafe access to iterator schema fields during variable-chip rendering.

Fix: Implemented a defensive fix in iterator variable search so malformed iterator schemas no longer crash rendering.

What changed:
- In `searchVariableThroughIteratorOutputSchema`, I now guard `iteratorOutputSchema.currentItem` before any property access.
- If `currentItem` is missing, the function safely returns `{ variableLabel: undefined, variablePathLabel: undefined }` (same non-crashing fallback used elsewhere).
- Subsequent label/type reads now use a local `currentItem` variable, avoiding repeated unsafe property access.

Why this fixes the incident:
- The production crash was caused by direct dereference of `currentItem.value` when `currentItem` was undefined for a specific workflow schema payload.
- With this guard, malformed/incomplete iterator output schema data degrades gracefully instead of throwing in render.

I also added a regression test that explicitly covers “currentItem missing from iterator output schema” and asserts the safe undefined-label fallback.

Authored by Sonarly by autonomous analysis (run 44106).
2026-05-19 19:19:08 +00:00

90 lines
2.4 KiB
TypeScript

import { useCallback, useEffect } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
CombinedGraphQLErrors,
CombinedProtocolErrors,
LinkError,
LocalStateError,
ServerError,
ServerParseError,
UnconventionalError,
} from '@apollo/client/errors';
import { isDefined, type CustomError } from 'twenty-shared/utils';
const isApolloError = (error: unknown): boolean =>
CombinedGraphQLErrors.is(error) ||
CombinedProtocolErrors.is(error) ||
LinkError.is(error) ||
LocalStateError.is(error) ||
ServerError.is(error) ||
ServerParseError.is(error) ||
UnconventionalError.is(error);
const hasErrorCode = (
error: CustomError | any,
): error is CustomError & { code: string } => {
return 'code' in error && isDefined(error.code);
};
export const PromiseRejectionEffect = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const handlePromiseRejection = useCallback(
async (event: PromiseRejectionEvent) => {
const error = event.reason;
if (isApolloError(error)) {
enqueueErrorSnackBar({
apolloError: error,
});
return; // already handled by apolloLink
}
const isAbortError =
error?.networkError?.name === 'AbortError' ||
error?.name === 'AbortError';
if (isAbortError) {
return;
}
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
try {
const { captureException } = await import('@sentry/react');
captureException(error, (scope) => {
scope.setExtras({ mechanism: 'onUnhandle' });
const fingerprint = hasErrorCode(error) ? error.code : error.message;
if (isDefined(fingerprint)) {
scope.setFingerprint([fingerprint]);
}
if (error instanceof Error) {
error.name = error.message;
}
return scope;
});
} catch (sentryError) {
// oxlint-disable-next-line no-console
console.error('Failed to capture exception with Sentry:', sentryError);
}
},
[enqueueErrorSnackBar],
);
useEffect(() => {
window.addEventListener('unhandledrejection', handlePromiseRejection);
return () => {
window.removeEventListener('unhandledrejection', handlePromiseRejection);
};
}, [handlePromiseRejection]);
return <></>;
};