Repository navigation
[Due for payment 2026-10-14] [$250] Chat - Editing a broken attachment link duplicates the URL #102671
Description
Version Number: Not provided
Reproducible in staging?: Yes, confirmed by the reporter
Reproducible in production?: Not confirmed
If this was caught during regression testing, add the test name, ID and link from BrowserStack: Found while testing #101717; no BrowserStack test provided
Email or phone of affected tester (no customers): Not provided
Logs: Not provided
Expensify/Expensify Issue URL: N/A
Issue reported by: @mukhrr
Slack conversation: N/A — original report
App Component: Chat Report View
Device used: Desktop web; OS and browser not specified
Action Performed:
- Open a chat on staging and send a file attachment, such as a
.csvfile. Wait for the upload to finish. - Open the message's context menu and select Edit comment.
- Delete the final
)from the attachment's Markdown link, optionally removing a few trailing URL characters as well, and save the message. - Open Edit comment again and inspect the attachment link in the editor.
- Save another edit to the malformed link and reopen Edit comment to observe the behavior on subsequent edits.
Expected Result:
Opening Edit comment preserves the saved text, including an incomplete Markdown link. The editor should not duplicate the URL or insert additional Markdown link syntax. Subsequent edits should not accumulate extra copies of the link.
Actual Result:
Reopening a message containing a broken attachment link inserts an additional copy of the URL into the editor, wrapping the URL in Markdown link syntax inside the already incomplete attachment link. Further edits can introduce more nested/duplicated links, as shown in the recording.
For example, saved text resembling [file.csv](https://example.com/file is reopened with the URL expanded into another Markdown link inside that incomplete link.
The issue was discovered during review of #101717 and is also reproducible on staging. A follow-up fix was agreed in the PR discussion.
Workaround:
No confirmed workaround.
Platforms:
Reproduced on desktop web in the attached recording; the exact OS/browser and other platforms have not been confirmed.
- Android: App
- Android: mWeb Chrome
- iOS: App
- iOS: mWeb Safari
- iOS: mWeb Chrome
- Windows: Chrome
- MacOS: Chrome / Safari
Screenshots/Videos
REC-20260929095734.mp4
Issue Owner
Current Issue Owner: @mallenexpensifyUpwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022105313130665411715
- Upwork Job ID: 2105313130665411715
- Last Price Increase: 2026-10-14
Activity
luacmartins commentedon Sep 30, 2026
@mukhrr please comment on this issue so I can assign it to you
mukhrr commentedon Sep 30, 2026
thanks. done
MelvinBot commentedon Sep 30, 2026
Proposal
What is the root cause of that problem?
This is not specific to attachments. Any Markdown link with its closing
)deleted behaves the same way.[file.csv](URL(no)), ExpensiMark'slinkrule no longer matches. Theautolinkrule then wraps the leftover URL, so the saved HTML is[file.csv](<a href="URL">URL</a>.htmlToMarkdownturns every anchor into[text](href). The draft becomes[file.csv]([URL](URL).handleUserDeletedLinksInHtml, which strips autolinks the user deliberately broke (issues [HOLD for payment 2022-07-15] [$250] When sending text that NewDot thinks is a link, it adds the link markdown automatically and doesn't allow to edit #9090 and [$1000] Auto link does not work in edit mode #13221). But it only strips an anchor whosehrefexactly matches a link removed in this edit. So the guard fails when:), because thehrefchangesEach time a
)is broken again, another level of nesting is added.Code references and round-trip evidence
ContextMenuActions.tsx:672callsParser.htmlToMarkdown(getActionHtml(reportAction))Report/index.ts:3680-3689ineditReportCommentReport/index.ts:3616-3658(removeLinksFromHtml/handleUserDeletedLinksInHtml)getRemovedMarkdownLinksonly runs thelinkrule:ExpensiMark.ts:2073autolinkrule doesn't check for a preceding](:ExpensiMark.ts:1141anchorrule always outputs[text](href):ExpensiMark.ts:1443I reproduced this with App's real
ParserandhandleUserDeletedLinksInHtml:csv)[file.csv](<a href="…/w_abc123.">…/w_abc123.</a>[file.csv]([…/w_abc123.](…/w_abc123.)❌)[file.csv](URL(guard works)[file.csv](URL✅[file.csv](<a href="URL">URL</a> z[file.csv]([URL](URL) z❌[Google](https://www.google.com/search), deleteh)[Google]([https://www.google.com/searc](https://www.google.com/searc)❌This matches the PR discussion, which said the bug exists on staging and is not a regression.
What changes do you think we should make in order to solve the problem?
In
handleUserDeletedLinksInHtml, after the new comment is parsed to HTML, unwrap any autolink anchor that directly follows a dangling](, leaving its plain text. This extends the guard's existing intent: "don't autolink a link the user broke."Results from a local simulation:
)back creates a normal link again.Add unit tests for the cases above, in
tests/actions/ReportTest.tsor a similar file.What alternative solutions did you explore? (Optional)
autolinkrule, and its candidate scannerfindUrlCandidates, skip a URL directly after](. This also covers newly sent messages and live Markdown. However, it needs a public expensify-common PR and a version bump in App. It is also a product decision: should a dangling[text](urlshow a clickable URL?htmlToMarkdownoutput a bare URL when the link text equals thehref: Not recommended. It breaks existing expensify-common tests and the link-removal behavior App relies on.Open question: I couldn't confirm whether the backend re-autolinks the
UpdateCommentHTML. If it does, even the "delete only)" case would nest on the first reopen. The App-side fix still helps on the next save.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/102671)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
melvin-bot commentedon Sep 30, 2026
Current assignee @brunovjk is eligible for the External assigner, not assigning anyone new.
[-]Chat - Editing a broken attachment link duplicates the URL[/-][+][$175] Chat - Editing a broken attachment link duplicates the URL[/+]melvin-bot commentedon Sep 30, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022105313130665411715
mukhrr commentedon Oct 1, 2026
Melvin's proposal only runs when an edit is saved (
handleUserDeletedLinksInHtml), so it misses some cases. I checked on dev:[Google](https://www.google.com/searchand open Edit comment without changing anything, the draft is already[Google]([https://www.google.com/search](https://www.google.com/search)editReportCommentRCA: when the
)is missing, theautolinkrule wraps the URL after](. On Edit comment, theanchorrule inhtmlToMarkdownturns every anchor into[text](href), so URL gets nested inside the broken link. Backend isn't involved, UpdateComment stores the html exactly as we send it.Solution: fix the
anchorrule in expensify-common, the same way it already returns a bare email formailto:anchors. If an autolinked anchor comes right after](, return the bare URL. The draft round-trips, already broken messages come back clean on open, and chat rendering doesn't change. Then bump expensify-common in App.mukhrr commentedon Oct 1, 2026
@brunovjk Expensify/expensify-common#950 is ready in expensify-common repo. Not sure you review it there or not.
brunovjk commentedon Oct 6, 2026
Update: PR merged and waiting for next checklist deployment.
17 remaining items
github-actions commentedon Oct 14, 2026
🤖 ProposalPolice™ is tracking duplicate proposals for this issue using an OpenAI Conversation. This comment stores that Conversation's ID and can be safely ignored.
[-][$250] Chat - Editing a broken attachment link duplicates the URL[/-][+][Due for payment 2026-10-14] [$250] Chat - Editing a broken attachment link duplicates the URL[/+]melvin-bot commentedon Oct 14, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)haiquywork commentedon Oct 14, 2026
Proposal
What is the root cause of that problem?
On save, the
linkrule can't match without), so theautolinkrule wraps only the bare URL. The stored HTML is[file.csv](<a href="URL">URL</a>. On Edit,saveReportActionDraftis fedParser.htmlToMarkdown(getActionHtml(reportAction)), and ExpensiMark'sanchorHTML-to-Markdown rule turns every anchor into[text](href). That nests a new link inside the dangling](.App/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
Lines 695 to 701 in 7cb35bb
The existing guard
handleUserDeletedLinksInHtmlonly strips anchors whose href matches a link removed in the same edit, so it misses cases where trailing URL characters were also deleted. It also never runs for a later unchanged reopen, becauseeditReportCommentreturns early when the draft is unchanged.App/src/libs/actions/Report/index.ts
Lines 3647 to 3673 in 7cb35bb
App/src/libs/actions/Report/index.ts
Lines 3696 to 3705 in 7cb35bb
The real defect is that the HTML-to-Markdown conversion isn't a faithful inverse of the autolink step. An anchor that was an autolink (text equals href) and sits right after a literal
](should go back to the bare URL.What changes do you think we should make in order to solve the problem?
ExpensiMark.ts, change theanchorrule'sreplacementfor HTML to Markdown. Use the replace offset and the full source string to check the preceding text.](, return just the href instead of[href](href).handleUserDeletedLinksInHtmlneed no change. The reopened draft then equals the saved text, and repeated edits no longer grow.[file.csv](<a href="U">U</a>converts to[file.csv](U. Add a round-trip test (Markdown to HTML to Markdown) that stays stable across repeated cycles.tests/unit/ReportUtilsTestor the existing edit-comment tests. It checks thatParser.htmlToMarkdownof a message with an unterminated attachment link returns the same text.What alternative solutions did you explore? (Optional)
autolinkskip URLs after](. This changes how messages are rendered and stored, and already-stored messages would still break on edit.handleUserDeletedLinksInHtmlin App. It only runs on a changed save, so the first reopen is still wrong.trasnake87 commentedon Oct 14, 2026
Withdrawn. This issue was already fixed by @mukhrr and is awaiting payment; the
Help Wantedlabel was only added for a moment to update the price, so it was never an open race. Sorry for the noise.melvin-bot commentedon Oct 14, 2026
Payment Summary
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Oct 16, 2026
Payment Summary
Contributor+: @mukhrr due $175 via NewDot
@brunovjk are you due payment? If so, please link the PR.
mukhrr commentedon Oct 17, 2026
cc @mallenexpensify
mallenexpensify commentedon Oct 17, 2026
@mukhrr I'm unsure. The payment trigger is the
Help Wantedlabel, and that label wasn't added until later.CONTRIBUTING.md says to post proposals only after
Help Wantedis added. That rule also covers comments that say Melvin's solution is wrong or can be improved. We already get flooded with proposals onceHelp Wantedis added. If we pay for comments posted before the label, contributors will flood issues with improvements to Melvin's proposals in the hope of getting paid.I plan to propose something in the coming weeks to address this.
mukhrr commentedon Oct 18, 2026
@mallenexpensify i unserstand your concern! But this is a bit different scenario where we don't "motivate" contributors to flood with proposals.
I mean I didn't post proposal or comment before Help Wanted here. I got assigned intentionally since I had more context from previous related PR/issue. And I think this is good experience since our main goal on Expensify is to fix bugs faster and more regression free, isn't it?! So this is not something everybody can claim/flood with comments/proposals. What do you think?