Skip to content

fix(formSelectors): guard against missing action with empty string key - #42038

Closed
PedroHenrique0713 wants to merge 5 commits into
appsmithorg:releasefrom
PedroHenrique0713:fix/form-selectors-null-guard
Closed

fix(formSelectors): guard against missing action with empty string key#42038
PedroHenrique0713 wants to merge 5 commits into
appsmithorg:releasefrom
PedroHenrique0713:fix/form-selectors-null-guard

Conversation

@PedroHenrique0713

@PedroHenrique0713 PedroHenrique0713 commented Jul 23, 2026

Copy link
Copy Markdown

Summary

When getActionByBaseId returns undefined, action?.id ?? "" produces an empty string "" used as object key in state.evaluations.triggers[""], which returns undefined. This silently passes through extractConditionalOutput producing incorrect behavior.

Fix: Return early with an empty/default object when no action is found, in both getFormConfigConditionalOutput and getFormConfigConditionalDynamicValues selectors.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented errors when actions, widgets, URLs, or other inputs are missing or invalid.
    • Improved entity naming when prefixes contain special characters.
    • Corrected action comparisons and path generation during updates.
    • Fixed retry error handling and archived action reconciliation.
    • Improved array comparison and ensured array utilities avoid unintended mutations.
    • Prevented visibility and scrolling calculations from using invalid geometry.

… lookup

fix(AppsmithUtils): escape regex special characters in getNextEntityName and
getDuplicateName. Entity name prefixes containing regex metacharacters (. $ * + etc.)
caused incorrect auto-increment numbering and potential name collisions.

fix(helpers): guard against division by zero in isElementVisibleInContainer.
When an element has zero dimensions, elementArea === 0 caused Infinity/NaN
comparisons that falsely reported the element as visible.

fix(helpers): add null guard to getWidgetElementToScroll.
Accessing widget.parentId without checking if canvasWidgets[widgetId]
exists caused TypeError on stale/deleted widget IDs.
fix(retryPromise): use reject() instead of Promise.reject() inside setTimeout.
  The setTimeout callback return value is ignored, so Promise.reject() created
  a dangling rejection that never reached the outer Promise's reject handler,
  causing the promise to hang forever when retries were exhausted.

fix(areArraysEqual): use element-wise comparison instead of join(",").
  Array elements containing commas caused false positives (e.g.,
  ["a,b"] and ["a","b"] incorrectly reported as equal).
fix(helpers): pushToArray no longer mutates input array in-place.
  Previously arr1.push(item) mutated the caller's array, violating
  immutability expectations in Redux state. Now creates a new array.

fix(getPathAndValueFromActionDiffObject): fix reduce accumulator path
  computation and early return. The reduce callback referenced the outer
  'path' variable instead of 'acc', causing path to grow incorrectly
  across multiple diff elements. The return statement was also inside
  the for loop, exiting prematurely instead of checking all elements.
fix(JSPaneUtils): add missing return to findIndex callback.
  Arrow function with curly braces returned undefined, so findIndex always
  returned -1, making the archived action lookup silently fail.

fix(TypeHelpers): add null guard to isURL before regex test.
  RegExp.prototype.test throws TypeError on null/undefined input from
  dynamic data sources.

fix(URLUtils): add null guard to sanitizeString.
  str.toLowerCase() throws TypeError on null/undefined from form fields.
  When action is not found, action?.id ?? '' produced an empty string '',
  which was used as key in state.evaluations.triggers[''], returning
  undefined and silently passing through extractConditionalOutput.

  Fix: return early with empty/default object when no action found.
@PedroHenrique0713
PedroHenrique0713 requested a review from a team as a code owner July 23, 2026 19:57
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds defensive guards to form selectors and utility functions, escapes regex prefixes, corrects archived-action and diff-path handling, changes array comparison and mutation behavior, and improves retry, URL, visibility, and widget error handling.

Changes

Client corrections

Layer / File(s) Summary
Form selector guards
app/client/src/selectors/formSelectors.ts
Form selectors return empty outputs when action?.id is unavailable.
Naming, retry, and comparison utilities
app/client/src/utils/AppsmithUtils.tsx
Entity-name prefixes are regex-escaped, retry exhaustion rejects directly, and array comparison uses sorted element checks.
Diff and archived-action reconciliation
app/client/src/utils/getPathAndValueFromActionDiffObject.ts, app/client/src/utils/JSPaneUtils.tsx
Diff paths and return control flow are corrected, and archived actions are located with a returning predicate.
Input and widget safety guards
app/client/src/utils/TypeHelpers.ts, app/client/src/utils/URLUtils.ts, app/client/src/utils/helpers.tsx
Invalid URL inputs, empty strings, zero-area elements, missing widgets, and array mutation are handled defensively.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

Regex stars now stay in line,
Empty inputs decline to shine.
Paths find their proper trail,
Arrays leave no mutated tale.
Guards stand watch through every flow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is missing most required template sections, including Fixes, Automation, Cypress results, and Communication. Rewrite the PR description using the template and add the missing sections: issue reference, Automation, Cypress results, Communication, and fuller motivation/context.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main form-selector guard and the empty-string-key bug.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/client/src/utils/AppsmithUtils.tsx (1)

318-330: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make all retry termination paths settle the promise.

retriesLeft === 1 retries forever when initialized with 0 or a negative value. Also, shouldRetry(e) === false leaves the promise pending, and the recursive call drops the caller’s custom predicate. Use retriesLeft <= 1, reject when retrying is disallowed, and pass shouldRetry recursively.

Suggested fix
-        if (shouldRetry(e)) {
+        if (!shouldRetry(e)) {
+          reject(e);
+          return;
+        }
+
+        {
           setTimeout(async () => {
-            if (retriesLeft === 1) {
+            if (retriesLeft <= 1) {
               reject({
                 code: ERROR_CODES.SERVER_ERROR,
                 message: createMessage(ERROR_500),
                 show: false,
               });
               return;
             }
 
-            retryPromise(fn, retriesLeft - 1, interval).then(resolve, reject);
+            retryPromise(
+              fn,
+              retriesLeft - 1,
+              interval,
+              shouldRetry,
+            ).then(resolve, reject);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/client/src/utils/AppsmithUtils.tsx` around lines 318 - 330, Update
retryPromise in the shouldRetry failure path so retriesLeft <= 1 rejects
immediately, shouldRetry(e) === false also rejects instead of leaving the
promise pending, and recursive retryPromise calls forward the caller’s
shouldRetry predicate while preserving existing error details and retry
behavior.
🧹 Nitpick comments (1)
app/client/src/utils/AppsmithUtils.tsx (1)

442-442: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sort arr2 once outside the loop.

The current expression copies and sorts arr2 for every element, causing unnecessary allocations and worst-case O(n² log n) work. Hoist both sorted arrays before calling every.

Suggested fix
-  return [...arr1].sort().every((val, i) => val === [...arr2].sort()[i]);
+  const sortedArr1 = [...arr1].sort();
+  const sortedArr2 = [...arr2].sort();
+  return sortedArr1.every((val, i) => val === sortedArr2[i]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/client/src/utils/AppsmithUtils.tsx` at line 442, Update the array
comparison expression in AppsmithUtils so both arr1 and arr2 are copied and
sorted once before every runs, then compare the precomputed sorted arrays by
index. Preserve the existing equality behavior while eliminating repeated arr2
sorting inside the callback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/client/src/selectors/formSelectors.ts`:
- Around line 57-58: Update getFormConfigConditionalOutput and
getDynamicFetchedValues to use shared typed empty defaults containing the
consumer-read fields isLoading, data, evaluatedConfig, and config. Replace the
missing-action `{}` casts and the fallback near line 84 with the appropriate
shared defaults, preserving each function’s output contract, then run yarn run
check-types.

---

Outside diff comments:
In `@app/client/src/utils/AppsmithUtils.tsx`:
- Around line 318-330: Update retryPromise in the shouldRetry failure path so
retriesLeft <= 1 rejects immediately, shouldRetry(e) === false also rejects
instead of leaving the promise pending, and recursive retryPromise calls forward
the caller’s shouldRetry predicate while preserving existing error details and
retry behavior.

---

Nitpick comments:
In `@app/client/src/utils/AppsmithUtils.tsx`:
- Line 442: Update the array comparison expression in AppsmithUtils so both arr1
and arr2 are copied and sorted once before every runs, then compare the
precomputed sorted arrays by index. Preserve the existing equality behavior
while eliminating repeated arr2 sorting inside the callback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b47ebb78-99bf-4a60-b82a-1b338a79f027

📥 Commits

Reviewing files that changed from the base of the PR and between 583811d and 4acd54f.

📒 Files selected for processing (7)
  • app/client/src/selectors/formSelectors.ts
  • app/client/src/utils/AppsmithUtils.tsx
  • app/client/src/utils/JSPaneUtils.tsx
  • app/client/src/utils/TypeHelpers.ts
  • app/client/src/utils/URLUtils.ts
  • app/client/src/utils/getPathAndValueFromActionDiffObject.ts
  • app/client/src/utils/helpers.tsx

Comment on lines +57 to +58
const actionId = action?.id;
if (!actionId) return {} as ConditionalOutput;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
yarn run check-types

Repository: appsmithorg/appsmith

Length of output: 325


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files =="
git ls-files | sed -n '1,120p'

echo
echo "== locate file =="
fd -a 'formSelectors\.ts$' . || true

echo
echo "== inspect formSelectors =="
file="$(fd 'formSelectors\.ts$' . | head -n 1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  cat -n "$file"
fi

echo
echo "== consumers of selectors =="
rg -n "fetchDynamicValues|dynamicFetchedValues|SegmentedControl|DropDownControl|isLoading|data" app/client/src -g '*.ts' -g '*.tsx' | sed -n '1,240p'

Repository: appsmithorg/appsmith

Length of output: 37166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package locations =="
find . -maxdepth 4 -path '*/package.json' | sed 's#^\./##' | head -80

echo
echo "== check-types scripts =="
python3 - <<'PY'
import json, pathlib
for p in pathlib.Path('.').rglob('package.json'):
    if 'node_modules' in p.parts or '.git' in p.parts:
        continue
    try:
        data=json.loads(p.read_text())
    except Exception:
        continue
    for section in ('scripts','devDependencies','dependencies'):
        if 'typescript' in str(data.get(section,'')) or 'check-types' in str(data.get('scripts',{})):
            print(p)
            print(json.dumps({section:data.get(section)}, indent=2))
PY

echo
echo "== locate DynamicValues/types =="
rg -n "export (interface|type) .*ConditionalOutput|export (interface|type) .*DynamicValues|DynamicValues|ConditionalOutput" app/client/src -g '*.ts' -g '*.tsx' | sed -n '1,200p'

echo
echo "== precise selectors and imports =="
for f in $(rg -l "getFormConfigConditionalOutput|getDynamicFetchedValues|extractConditionalOutput" app/client/src -g '*.ts' -g '*.tsx'); do
  echo "--- $f ---"
  wc -l "$f"
  rg -n "getFormConfigConditionalOutput|getDynamicFetchedValues|extractConditionalOutput|isLoading|dynamicFetchedValues|SegmentedControl|DropDownControl" "$f" -C 4 | sed -n '1,220p'
done

Repository: appsmithorg/appsmith

Length of output: 45796


Return shared typed default values from the missing-action guards.

When actionId is missing, both getFormConfigConditionalOutput and getDynamicFetchedValues return {} cast to their output types, so consumers receive undefined for fields such as isLoading, data, evaluatedConfig, and config. Define shared empty default values with the fields consumers read and return them here and from the fallback on line 84. Run yarn run check-types to keep TypeScript contracts consistent.

Also applies to: 77-78

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/client/src/selectors/formSelectors.ts` around lines 57 - 58, Update
getFormConfigConditionalOutput and getDynamicFetchedValues to use shared typed
empty defaults containing the consumer-read fields isLoading, data,
evaluatedConfig, and config. Replace the missing-action `{}` casts and the
fallback near line 84 with the appropriate shared defaults, preserving each
function’s output contract, then run yarn run check-types.

Source: Coding guidelines

@PedroHenrique0713

Copy link
Copy Markdown
Author

Closing this one. I opened a stack of PRs in this repo today and, because each branch was cut from the previous one instead of from the base, they overlap: this PR carries the commits of the earlier ones as well. I am consolidating the work in #42033 and will resubmit the remaining fixes individually, on top of the base branch, once that one has been reviewed. Sorry for the noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant