From 34e6fcde9a9918300c7e5be7ebcfb1b69119eb96 Mon Sep 17 00:00:00 2001 From: sonarly-bot Date: Mon, 18 May 2026 09:00:52 +0000 Subject: [PATCH] fix(front): guard undefined nested output schema traversal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/38306?type=bug Opening a specific workflow record crashes variable-chip rendering in the workflow UI with a handled React TypeError. The page can become unusable for that workflow because variable lookup throws during render instead of returning “Not Found”. Fix: Implemented a defensive traversal fix in workflow variable base-schema search so malformed non-leaf nodes no longer throw during render. ### What changed - In `searchVariableThroughBaseOutputSchema.ts`, `navigateToTargetField` now validates that a non-leaf node has an object-like `value` before traversing deeper. - If an intermediate node is malformed (`undefined`, non-object, or array), traversal now returns `undefined` variable metadata instead of throwing a `TypeError`. - This preserves intended UX behavior (“not found” fallback) and prevents workflow-page render crashes. ### Why this fixes the incident The crash happened when traversal assigned `currentSchema = field.value` where `field.value` was invalid, then accessed `currentSchema[nextSegment]`. The new guard blocks that invalid descent path. Authored by Sonarly by autonomous analysis (run 43714). --- ...rchVariableThroughBaseOutputSchema.test.ts | 40 ++++++++++++ .../reportInvalidVariableSchemaTraversal.ts | 40 ++++++++++++ .../searchVariableThroughBaseOutputSchema.ts | 63 ++++++++++++++++--- ...archVariableThroughIteratorOutputSchema.ts | 1 + 4 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts index eccf087e1d2..23063e0f9e9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts @@ -1,7 +1,19 @@ import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema'; +import { reportInvalidVariableSchemaTraversal } from '@/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal'; import type { BaseOutputSchemaV2 } from 'twenty-shared/workflow'; +jest.mock( + '@/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal', + () => ({ + reportInvalidVariableSchemaTraversal: jest.fn(), + }), +); + describe('searchVariableThroughBaseOutputSchema', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const mockBaseSchema: BaseOutputSchemaV2 = { message: { isLeaf: true, @@ -328,4 +340,32 @@ describe('searchVariableThroughBaseOutputSchema', () => { variableType: 'unknown', }); }); + + it('should return undefined and report malformed nested schema nodes', () => { + const malformedSchema: BaseOutputSchemaV2 = { + user: { + isLeaf: false, + type: 'object', + label: 'User Information', + value: undefined as unknown as BaseOutputSchemaV2, + }, + }; + + const result = searchVariableThroughBaseOutputSchema({ + stepName: 'HTTP Request', + baseOutputSchema: malformedSchema, + rawVariableName: '{{step1.user.phone}}', + }); + + expect(result).toEqual({ + variableLabel: undefined, + variablePathLabel: undefined, + variableType: undefined, + }); + expect(reportInvalidVariableSchemaTraversal).toHaveBeenCalledWith({ + stepName: 'HTTP Request', + rawVariableName: '{{step1.user.phone}}', + pathSegment: 'user', + }); + }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal.ts new file mode 100644 index 00000000000..48c3f6452c1 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal.ts @@ -0,0 +1,40 @@ +const reportedInvalidVariableSchemaTraversalKeys = new Set(); + +export const reportInvalidVariableSchemaTraversal = ({ + stepName, + rawVariableName, + pathSegment, +}: { + stepName: string; + rawVariableName: string; + pathSegment: string; +}) => { + const issueKey = `${stepName}:${rawVariableName}:${pathSegment}`; + + if (reportedInvalidVariableSchemaTraversalKeys.has(issueKey)) { + return; + } + + reportedInvalidVariableSchemaTraversalKeys.add(issueKey); + + void import('@sentry/react').then(({ captureMessage }) => { + captureMessage('Workflow variable schema traversal skipped malformed node', { + level: 'warning', + tags: { + area: 'workflow-variables', + }, + contexts: { + workflowVariableSchemaTraversal: { + stepName, + rawVariableName, + pathSegment, + }, + }, + fingerprint: [ + 'workflow-variable-schema-traversal-malformed-node', + stepName, + pathSegment, + ], + }); + }); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts index 8918f0ebcd1..617fd6e65d6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts @@ -1,4 +1,5 @@ import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable'; +import { reportInvalidVariableSchemaTraversal } from '@/workflow/workflow-variables/utils/reportInvalidVariableSchemaTraversal'; import { isDefined } from 'twenty-shared/utils'; import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX, @@ -26,10 +27,17 @@ const parseVariableName = (rawVariableName: string) => { }; }; -const navigateToTargetField = ( - startingSchema: BaseOutputSchemaV2, - pathSegments: string[], -): { schema: BaseOutputSchemaV2; pathLabels: string[] } | null => { +const navigateToTargetField = ({ + startingSchema, + pathSegments, + stepName, + rawVariableName, +}: { + startingSchema: BaseOutputSchemaV2; + pathSegments: string[]; + stepName: string; + rawVariableName: string; +}): { schema: BaseOutputSchemaV2; pathLabels: string[] } | null => { let currentSchema: BaseOutputSchemaV2 = startingSchema; const pathLabels: string[] = []; @@ -40,6 +48,20 @@ const navigateToTargetField = ( return null; } + if ( + !isDefined(field.value) || + typeof field.value !== 'object' || + Array.isArray(field.value) + ) { + reportInvalidVariableSchemaTraversal({ + stepName, + rawVariableName, + pathSegment, + }); + + return null; + } + pathLabels.push(field.label); currentSchema = field.value; } @@ -78,13 +100,21 @@ export const searchBaseOutputSchema = ({ baseOutputSchema, path, selectedField, + rawVariableName, }: { stepName: string; baseOutputSchema: BaseOutputSchemaV2; path: string[]; selectedField: string; + rawVariableName?: string; }): VariableSearchResult => { - const navigationResult = navigateToTargetField(baseOutputSchema, path); + const navigationResult = navigateToTargetField({ + startingSchema: baseOutputSchema, + pathSegments: path, + stepName, + rawVariableName: + rawVariableName ?? `${stepName}.${path.join('.')}.${selectedField}`, + }); if (!navigationResult) { return { @@ -137,10 +167,25 @@ export const searchVariableThroughBaseOutputSchema = ({ }; } - return searchBaseOutputSchema({ + const navigationResult = navigateToTargetField({ + startingSchema: baseOutputSchema, + pathSegments, stepName, - baseOutputSchema, - path: pathSegments, - selectedField: targetFieldName, + rawVariableName, }); + + if (!navigationResult) { + return { + variableLabel: undefined, + variablePathLabel: undefined, + variableType: undefined, + }; + } + + return buildVariableResult( + stepName, + navigationResult.pathLabels, + navigationResult.schema, + targetFieldName, + ); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts index 5bee261aa45..cf0016f1dc4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts @@ -109,6 +109,7 @@ export const searchVariableThroughIteratorOutputSchema = ({ baseOutputSchema: schema, path: pathSegments, selectedField: fieldName, + rawVariableName, }); }