You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-08-10] [$250] [Sentry: APP-GVV] IndexSizeError restoring markdown cursor on IOU description step #95347
useDiscardChangesConfirmationonCancel calls inputRef.focus() which triggers live-markdown setCursorPosition with stale DOM offsets after the description field content changed.
Reproduction
Open expense description step in create/submit flow.
Edit description, then cancel/discard changes (triggering discard confirmation).
Observe IndexSizeError on Range.setStart in console.
🚨 Edited by proposal-police: This proposal was edited at 2026-07-16 14:11:56 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
On web, an uncaught IndexSizeError is thrown from Range.setStart while restoring the markdown cursor on the IOU description step. It is reproduced by opening the expense description step, editing the description, then triggering the discard-changes confirmation and cancelling it. The error is captured by Sentry (APP-GVV) via the global onerror handler.
What is the root cause of that problem?
When the discard-changes confirmation is cancelled, the description step re-focuses the input:
Focusing the markdown input runs handleFocus in @expensify/react-native-live-markdown, which restores the previous caret by calling setCursorPosition with the last remembered selection. setCursorPosition computes the DOM offset from the markdown tree metadata and passes it straight into Range.setStart/Range.setEnd:
The starting index is clamped to target.tree.length, but the resulting offset is only validated against the tree node's length, not against the actual length of the resolved DOM node (startElement.childNodes[0]). When the tree metadata is momentarily out of sync with the rendered DOM text node — which happens here because the input is blurred while the discard modal is open and then programmatically re-focused — the computed offset can exceed the real text node length. Range.setStart/Range.setEnd throw IndexSizeError whenever the offset is greater than the node length, and nothing catches it, so it surfaces as the crash.
What changes do you think we should make in order to solve the problem?
Make the caret restoration in setCursorPosition (in @expensify/react-native-live-markdown, src/web/utils/cursorUtils.ts) resilient to a transient tree/DOM desync by clamping each offset to the bounds of the DOM node it is applied to, before calling Range.setStart/Range.setEnd:
Resolve the container node first (startElement.childNodes[0] || startElement for the start, and the equivalent for the end).
Compute the offset as today (start - startTreeNode.start), then clamp it to that node's maximum valid offset: for a text node that is its .length, and for an element node that is its childNodes.length.
Pass the clamped offset into Range.setStart/Range.setEnd.
With the offset clamped to the node's real bounds, the range API can no longer receive an out-of-range offset, so the IndexSizeError cannot be thrown. In the rare desync case the caret lands at the end of the node instead of crashing, and the next parse pass re-syncs it to the correct position — a graceful degradation instead of an app-level error.
Because this fix lives in the bundled @expensify/react-native-live-markdown library, it should be applied to App as a patch-package patch against version 0.1.329 (and upstreamed to the library repo), matching how other library-level fixes ship in App. The patch needs to cover the library's compiled lib/module and lib/commonjs builds in addition to src, because the web bundle resolves the package's module entry point rather than its source — patching src alone would have no effect on web, which is where this error occurs.
What alternative solutions did you explore? (Optional)
Wrapping the range.setStart/range.setEnd calls in setCursorPosition in a try/catch that swallows the IndexSizeError and returns early. This also stops the crash, but it silently abandons cursor restoration on any failure rather than placing the caret at the nearest valid position, so clamping the offset is preferred as it keeps the caret behaviour correct while still being crash-safe.
When a user edits the description in the IOU description step and then cancels/discards the changes, the useDiscardChangesConfirmation hook's onCancel callback is triggered. This callback first reverts the content of the MarkdownTextInput component to its original state and then explicitly calls inputRef.current?.focus() on the input field, as indicated by the stack trace originating from IOURequestStepDescription.tsx:
The MarkdownTextInput component, specifically its web implementation in MarkdownTextInput.web.js, has a handleFocus method that attempts to restore the cursor position using setCursorPosition from cursorUtils.js:
The root cause of the IndexSizeError is that setCursorPosition is called with stale cursor start and end indices that were saved when the input contained the edited content. After the content is reverted to its original (potentially shorter) state, these saved indices become out of bounds for the current input value. When setCursorPosition attempts to use these invalid indices with range.setStart (or input.setSelectionRange), it throws an IndexSizeError:
What changes do you think we should make in order to solve the problem?
We should modify the setCursorPosition function in cursorUtils.js to ensure that the start and end indices of the selection range are always clamped within the valid bounds of the current input value's length. This will prevent IndexSizeError when attempting to set a cursor position that is out of bounds for the current text content.
In cursorUtils.js, modify the setCursorPosition function to clamp the start and end values of the selection object to the current input.value.length.
This change ensures that even if selection.start or selection.end are greater than the current text length (due to content being reverted), they will be adjusted to the maximum valid index, preventing the IndexSizeError.
What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
Contributor: @x-dev90 paid $250 via Upwork
Contributor+: @suneox due $250 via NewDot
@x-dev90 can you please accept the job below? Please reply here and tag me once you have.
If you're not sent an offer in Upwork when you're assigned to an issue, please tag me. If an Upwork job exists, please submit a proposal. https://www.upwork.com/jobs/~022077755966792443403
Guessing it's due $250. @mountiny , comment if you disagree.
⚠️ Looks like this issue was linked to a Deploy Blocker #97552
and
@x-dev90@suneox unfortunately look slike this was a cache issue, so we can redo these prs
The upstream react-native-live-markdown PR fixed the root crash by clamping/restoring markdown cursor range offsets safely inside the library.
The App PR was not only a dependency version bump. It also applied the upstream fix in the App by updating the markdown package version and changing the IOU Description/Merchant refocus behavior to use focusComposerWithDelay(inputRef.current)(true) instead of immediately calling inputRef.current?.focus(). So the App-side PR included the integration work needed for the upstream fix to actually resolve the issue in NewDot.
Since both PRs were authored by me and merged, could you please confirm whether the contributor compensation should be increased, i.e. $250 for the upstream library fix and $250 for the App integration/update?
I understand compensation decisions are ultimately at Expensify’s discretion, and I’m happy to follow whatever process is correct. I just wanted to raise it here because the contributor docs say compensation reevaluation requests should be made in the GitHub issue where the job was posted.
It is normal that the cost is 250 for scenario like this, the fix is actually the dependency pr and then just a bump in the app so no need for double payment
Activity
melvin-bot commentedon Jul 9, 2026
Whoops! This issue is 2 days overdue. Let's get this updated quick!
melvin-bot commentedon Jul 10, 2026
Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Jul 14, 2026
8 days overdue is a lot. Should this be a Weekly issue? If so, feel free to change it!
melvin-bot commentedon Jul 16, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @suneox (
External)wildan-m commentedon Jul 16, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-07-16 14:11:56 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
On web, an uncaught
IndexSizeErroris thrown fromRange.setStartwhile restoring the markdown cursor on the IOU description step. It is reproduced by opening the expense description step, editing the description, then triggering the discard-changes confirmation and cancelling it. The error is captured by Sentry (APP-GVV) via the globalonerrorhandler.What is the root cause of that problem?
When the discard-changes confirmation is cancelled, the description step re-focuses the input:
App/src/pages/iou/request/step/IOURequestStepDescription.tsx
Lines 179 to 184 in 21dd5a0
Focusing the markdown input runs
handleFocusin@expensify/react-native-live-markdown, which restores the previous caret by callingsetCursorPositionwith the last remembered selection.setCursorPositioncomputes the DOM offset from the markdown tree metadata and passes it straight intoRange.setStart/Range.setEnd:range.setStart((startElement.childNodes[0] || startElement), start - startTreeNode.start)(node_modules/@expensify/react-native-live-markdown/src/web/utils/cursorUtils.ts, thesetStart/setEndlines insidesetCursorPosition).The starting index is clamped to
target.tree.length, but the resulting offset is only validated against the tree node's length, not against the actual length of the resolved DOM node (startElement.childNodes[0]). When the tree metadata is momentarily out of sync with the rendered DOM text node — which happens here because the input is blurred while the discard modal is open and then programmatically re-focused — the computed offset can exceed the real text node length.Range.setStart/Range.setEndthrowIndexSizeErrorwhenever the offset is greater than the node length, and nothing catches it, so it surfaces as the crash.What changes do you think we should make in order to solve the problem?
Make the caret restoration in
setCursorPosition(in@expensify/react-native-live-markdown,src/web/utils/cursorUtils.ts) resilient to a transient tree/DOM desync by clamping each offset to the bounds of the DOM node it is applied to, before callingRange.setStart/Range.setEnd:startElement.childNodes[0] || startElementfor the start, and the equivalent for the end).start - startTreeNode.start), then clamp it to that node's maximum valid offset: for a text node that is its.length, and for an element node that is itschildNodes.length.Range.setStart/Range.setEnd.With the offset clamped to the node's real bounds, the range API can no longer receive an out-of-range offset, so the
IndexSizeErrorcannot be thrown. In the rare desync case the caret lands at the end of the node instead of crashing, and the next parse pass re-syncs it to the correct position — a graceful degradation instead of an app-level error.Because this fix lives in the bundled
@expensify/react-native-live-markdownlibrary, it should be applied to App as apatch-packagepatch against version0.1.329(and upstreamed to the library repo), matching how other library-level fixes ship in App. The patch needs to cover the library's compiledlib/moduleandlib/commonjsbuilds in addition tosrc, because the web bundle resolves the package'smoduleentry point rather than its source — patchingsrcalone would have no effect on web, which is where this error occurs.Compare branch: https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/95347-clamp-markdown-cursor-offset?expand=1
What alternative solutions did you explore? (Optional)
Wrapping the
range.setStart/range.setEndcalls insetCursorPositionin atry/catchthat swallows theIndexSizeErrorand returns early. This also stops the crash, but it silently abandons cursor restoration on any failure rather than placing the caret at the nearest valid position, so clamping the offset is preferred as it keeps the caret behaviour correct while still being crash-safe.samranahm commentedon Jul 16, 2026
Proposal
What is the root cause of that problem?
When a user edits the description in the IOU description step and then cancels/discards the changes, the
useDiscardChangesConfirmationhook'sonCancelcallback is triggered. This callback first reverts the content of theMarkdownTextInputcomponent to its original state and then explicitly callsinputRef.current?.focus()on the input field, as indicated by the stack trace originating fromIOURequestStepDescription.tsx:https://github.com/Expensify/App/blob/2746256aef0062a22797455e96b51ca91e538b6e/src/pages/iou/steps/IOURequestStepDescription.tsx#L172
The
MarkdownTextInputcomponent, specifically its web implementation inMarkdownTextInput.web.js, has ahandleFocusmethod that attempts to restore the cursor position usingsetCursorPositionfromcursorUtils.js:https://github.com/Expensify/App/blob/2746256aef0062a22797455e96b51ca91e538b6e/src/components/MarkdownTextInput/MarkdownTextInput.web.js#L411
The root cause of the
IndexSizeErroris thatsetCursorPositionis called with stale cursorstartandendindices that were saved when the input contained the edited content. After the content is reverted to its original (potentially shorter) state, these saved indices become out of bounds for the current input value. WhensetCursorPositionattempts to use these invalid indices withrange.setStart(orinput.setSelectionRange), it throws anIndexSizeError:https://github.com/Expensify/App/blob/2746256aef0062a22797455e96b51ca91e538b6e/src/libs/cursorUtils.js#L27
What changes do you think we should make in order to solve the problem?
We should modify the
setCursorPositionfunction incursorUtils.jsto ensure that thestartandendindices of the selection range are always clamped within the valid bounds of the current input value's length. This will preventIndexSizeErrorwhen attempting to set a cursor position that is out of bounds for the current text content.In
cursorUtils.js, modify thesetCursorPositionfunction to clamp thestartandendvalues of theselectionobject to the currentinput.value.length.https://github.com/Expensify/App/blob/2746256aef0062a22797455e96b51ca91e538b6e/src/libs/cursorUtils.js#L27
This change ensures that even if
selection.startorselection.endare greater than the current text length (due to content being reverted), they will be adjusted to the maximum valid index, preventing theIndexSizeError.What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
43 remaining items
mallenexpensify commentedon Aug 12, 2026
Payment Summary
Contributor: @x-dev90 paid $250 via Upwork
Contributor+: @suneox due $250 via NewDot
@x-dev90 can you please accept the job below? Please reply here and tag me once you have.
If you're not sent an offer in Upwork when you're assigned to an issue, please tag me. If an Upwork job exists, please submit a proposal.
https://www.upwork.com/jobs/~022077755966792443403
Guessing it's due $250. @mountiny , comment if you disagree.
and
x-dev90 commentedon Aug 12, 2026
Hi @mallenexpensify @mountiny, thanks for posting the payment summary.
Before payment is finalized, I wanted to respectfully ask for a compensation reevaluation for this issue.
The current summary lists $250 for the contributor. In this case, the fix required work in two repositories:
The upstream
react-native-live-markdownPR fixed the root crash by clamping/restoring markdown cursor range offsets safely inside the library.The App PR was not only a dependency version bump. It also applied the upstream fix in the App by updating the markdown package version and changing the IOU Description/Merchant refocus behavior to use
focusComposerWithDelay(inputRef.current)(true)instead of immediately callinginputRef.current?.focus(). So the App-side PR included the integration work needed for the upstream fix to actually resolve the issue in NewDot.Since both PRs were authored by me and merged, could you please confirm whether the contributor compensation should be increased, i.e. $250 for the upstream library fix and $250 for the App integration/update?
I understand compensation decisions are ultimately at Expensify’s discretion, and I’m happy to follow whatever process is correct. I just wanted to raise it here because the contributor docs say compensation reevaluation requests should be made in the GitHub issue where the job was posted.
Thanks!
mountiny commentedon Aug 12, 2026
It is normal that the cost is 250 for scenario like this, the fix is actually the dependency pr and then just a bump in the app so no need for double payment
$250 is correct reward in this case.
Thank you 🙇♂️
mallenexpensify commentedon Aug 12, 2026
@x-dev90 can you please accept the job below? Please reply here and tag me once you have.
https://www.upwork.com/jobs/~022077755966792443403
Always accept the job ASAP buz bonuses can always be added, if needed.
x-dev90 commentedon Aug 13, 2026
@mallenexpensify, The offer accepted. thanks.
mallenexpensify commentedon Aug 13, 2026
@x-dev90 paid, summary updated above. Thx.
garrettmknight commentedon Aug 27, 2026
$250 approved for @suneox