Skip to content

[$175] Compose Box - Maximum comment length error when a link is pasted in the composer #102500

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: v9.4.95-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #100421
Email or phone of affected tester (no customers): agexptest+pr280926@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Windows 11/ Chrome, Samsung S23FE/ Android 16, iPhone 13 Mini/ iOS 26.5
App Component: Chat Report View

Action Performed:

  1. Navigate to https://staging.new.expensify.com/
  2. Go to any 1:1 chat
  3. Open Chrome DevTools and select the Console tab
  4. Run: copy('a'.repeat(8496) + '.com')
  5. Paste the string in the composer

Expected Result:

The maximum comment length error message is not displayed.

Actual Result:

The maximum comment length error message is displayed, but the writing is still allowed.
The pasted message is 8,500 characters long.

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7272030_1790636680146.composer_error_message.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Julia Ponikarchuk
Bug ID: 7272030

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @linhvovan29546

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 29, 2026
  2. applause-bot commented on Sep 29, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/1dfad893b0feb7d2629ca1f27f01fe7c0b49d24b419b4994a903b58eecf58042

  3. changed the title [-]Compose Box-Maximum comment length error when a link is pasted in the composer[/-] [+]Compose Box - Maximum comment length error when a link is pasted in the composer[/+] on Sep 29, 2026
  4. MelvinBot commented on Sep 29, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The length check counts the message's HTML, not the characters you see. ExpensiMark turns the pasted string into a link, so the text appears twice in the HTML (<a href="https://aaa…com" …>aaa…com</a>). The 8,500 visible characters become 17,065 HTML characters, which is over the 15,000 limit.

    • getCommentLength measures the parsed HTML on purpose, so the App matches how the backend counts AddComment length.
    • The real bug is in expensify-common. URL_WEBSITE_REGEX accepts hostname labels of any length ([-a-z0-9]*). DNS allows at most 63 characters per label, so aaa…(8,496)….com is not a real domain and should never become a link.

    Typing still works because the input's hard cap counts raw characters (MAX_COMMENT_LENGTH + 1). The HTML check only blocks sending.

    This is not a regression from fix: preserve Markdown formatting in long messages. expensify-common 2.0.204, the version before that PR, gives the same 17,065-character output.

    What changes do you think we should make in order to solve the problem?

    Change expensify-common, then bump it in App:

    1. In URL_WEBSITE_REGEX, limit each hostname label to 63 characters ([-a-z0-9]{0,61} in place of [-a-z0-9]*).
    2. Add a lookbehind so a match can't start in the middle of a label ((?<![-a-z0-9.])). Without it, the regex links just the last 63 characters of the string.
    3. Apply the same 63-character rule in the new candidate scanner (isValidHostnameLabel / findHostnameStart in ExpensiMark.ts) so both paths agree.
    4. Add unit tests: a 63-character label links, a 64-character label does not, and common URLs still link.

    With these changes, the pasted string stays plain text and counts as 8,500 characters, so no error appears.

    What alternative solutions did you explore? (Optional)

    • Count raw characters in App. This breaks parity with the backend. The HTML count was added on purpose to stop AddComment from failing.
    • Override the autolink rule in App's Parser.ts. This is a workaround, and the composer's live Markdown highlighting would still show the link.
    Investigation details

    The researcher ran the real getCommentLength in a Jest test on App 68ad1ed:

    Input getCommentLength
    'a'.repeat(8496) + '.com' 17,065 (over 15,000)
    'a'.repeat(8500) 8,500
    Same output from expensify-common 2.0.204 and 2.0.207 Yes
    • The link adds 65 characters plus a second copy of the URL: HTML length = 2 × length + 65. Any bare "domain" of 7,468 or more characters triggers the error.
    • Text over MAX_MARKUP_LENGTH (10,000) is not parsed (ReportUtils.ts:6933). So the same string at 10,001 or more characters does not show the error.
    • The composer blocks sending when the limit is exceeded (useDebouncedCommentMaxLengthValidation.ts:38).
    • We tested the 63-character cap plus lookbehind by overriding the regex in Jest. The 8,500-character string was not linked. google.com, https://www.expensify.com/path?x=1, sub.domain.example.co.uk, test@expensify.com, and _expensify.com_ still behaved as before.
    • A live web reproduction didn't finish because the test session's page went blank. The Jest run above calls the same code the composer uses.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102500) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  5. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 29, 2026
  6. melvin-bot commented on Sep 29, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @thelullabyy (External)

  7. changed the title [-]Compose Box - Maximum comment length error when a link is pasted in the composer[/-] [+][$175] Compose Box - Maximum comment length error when a link is pasted in the composer[/+] on Sep 29, 2026
  8. melvin-bot commented on Sep 29, 2026

    @melvin-bot
  9. bonnhatnguyen commented on Sep 29, 2026

    @bonnhatnguyen

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When pasting an 8,500-character string formatted like a pseudo-domain (e.g. 'a'.repeat(8496) + '.com') into the composer, the "Maximum comment length exceeded" error message is prematurely displayed even though the raw text is well within the 10,000 character limit.

    What is the root cause of that problem?

    1. In src/libs/ReportUtils.ts:10798-10802, getCommentLength computes comment length on the parsed HTML output via ExpensiMark (rather than raw plaintext characters) to ensure parity with the backend auth validator for AddComment:
      function getCommentLength(textComment: string, parsingDetails?: ParsingDetails): number {
          return getParsedComment(textComment, parsingDetails)
              .replaceAll(/[^ -~]/g, '\u????')
              .trim().length;
      }
    2. When the user pastes 'a'.repeat(8496) + '.com', ExpensiMark in expensify-common applies URL_WEBSITE_REGEX ([-a-z0-9]*\.[a-z]{2,}).
    3. Under RFC 1035 and DNS specifications, a valid domain label cannot exceed 63 characters. However, the current regex in expensify-common allows unlimited characters in the label ([-a-z0-9]*).
    4. Because the 8,496-character string is incorrectly recognized as a URL, ExpensiMark autolinks it, duplicating the string in both href and inner text:
      <a href="https://aaaa...com" target="_blank" rel="noreferrer noopener">aaaa...com</a>
    5. This balloons the 8,500 visible characters into 17,065 HTML characters, which exceeds the backend and client limit (15,000 HTML characters / CONST.MAX_COMMENT_LENGTH), triggering useDebouncedCommentMaxLengthValidation.ts:38-41 to flag a maximum length violation.

    What changes do you think we should make in order to solve the problem?

    1. Enforce RFC 1035 63-Character Label Limit in expensify-common (and ExpensiMark.ts):
      • Update URL_WEBSITE_REGEX in expensify-common/lib/Url.js (and src/libs/ExpensiMark.ts) to restrict domain label length to a maximum of 63 characters:
        Replace [-a-z0-9]* with [-a-z0-9]{1,63} and include boundary checks:
        const URL_WEBSITE_REGEX = /(?<![-a-z0-9.])(?:[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?\.)+[a-z]{2,}/gi;
      • In ExpensiMark.ts, enforce label.length <= 63 in hostname label validation (isValidHostnameLabel).
    2. Result:
      • Any string with an unsegmented label exceeding 63 characters (such as a.repeat(8496) + '.com') will not be treated as a website link.
      • It remains plain text of 8,500 characters, well within the limit, preventing the false "maximum comment length exceeded" error.
    3. Add Unit Tests in tests/unit/ExpensiMarkTest.ts:
      • Verify that domain labels <= 63 characters are correctly autolinked.
      • Verify that domain labels > 63 characters remain plain text and do not inflate HTML length.

    What alternative solutions did you explore? (Optional)

    • Calculating comment length purely on raw text in the client: Explored and rejected because if a message containing legitimate Markdown/links exceeds the backend 15,000 HTML limit, the server would reject the AddComment command with an error. Enforcing the RFC 63-character hostname standard in ExpensiMark fixes the root autolinking defect while maintaining full client-backend length parity.

    Contributor details
    Your Expensify account email: bonnhatnguyen0@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182

  10. melvin-bot commented on Sep 29, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. Uzaifm127 commented on Sep 29, 2026

    @Uzaifm127
    Contributor

    @JS00001

    Can me and @linhvovan29546 work on this issue? Because

    This issue was found during QA for our PR: #100421, and I already faced this issue locally while solving the long .comx and heavy dotted URL case for ExpensiMark follow up PR: Expensify/expensify-common#945. Since it isn't a regression introduced by our PR, solving this issue also includes updating the candidate scanning logic introduced by our ExpensiMark performance PR: Expensify/expensify-common#937

    Since we already have the full context and idea for these parser changes, happy to be assigned. Thank you!

  12. JS00001 commented on Sep 29, 2026

    @JS00001
    Contributor

    Works for me

  13. 21 remaining items

  14. Uzaifm127 commented on Oct 13, 2026

    @Uzaifm127
    Contributor

    Not overdue.

    PR is WIP.

  15. linhvovan29546 commented on Oct 13, 2026

    @linhvovan29546
    Contributor

    Not overdue. I’m reviewing the PR, so add the Weekly label.

  16. added
    ReviewingHas a PR in review
    BugSomething is broken. Auto assigns a BugZero manager.
    and removed
    BugSomething is broken. Auto assigns a BugZero manager.
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 13, 2026
  17. MelvinBot commented on Oct 13, 2026

    @MelvinBot
    Contributor

    Issue Analysis

    Status: The root cause is still the one in my earlier proposal. ExpensiMark autolinks hostnames with labels over 63 characters, so the HTML doubles to 17,065 characters. fix: enforce DNS label and hostname length limits in ExpensiMark fixes the repro. It also adds 3 regressions that the review threads haven't covered yet. I tested head 111ae2e.

    Repro fixed: 'a'.repeat(8496) + '.com' now stays plain text. Its HTML length drops from 17,065 to 8,500. All 702 tests pass, and typecheck and lint are clean.

    New regressions to fix before merge:

    1. A valid domain after dots no longer links. In these examples, google.com or b.com links on main but stays plain on the PR:

      • I checked...google.com
      • wait..google.com
      • a..b.com
      • example.com..google.com

      findRawHostnameStart walks back over the dots, so the whole run counts as one invalid hostname. analyzeUrlRange can recover a valid prefix but not a valid suffix. The new lookbehind in Url.js:10 blocks the same match. This is the leading-side version of the example.com..nonsense case that is already fixed.

    2. A valid prefix is lost when a long word follows it. In google.com. + b×64 and example.com- + b×70, google.com and example.com link on main but nothing links on the PR. The length check at ExpensiMark.ts:366-368 returns without the valid-prefix end it already found. This contradicts the PR's own example.com-thanks test.

    3. Quadratic slowdown on URLs with dotted paths. For 'https://x.com/' + 'a.co/'.repeat(n), the median parse time is:

      Length main PR
      2.5k chars 0.10 ms 1.2 ms
      5k chars 0.17 ms 4.3 ms
      10k chars 0.30 ms 17.6 ms

      After a valid URL, findInvalidUrlRanges resumes at the end of the hostname (L524) or TLD (L540), not the end of the URL. Each later .co then calls extendUrlCandidateBoundaries, which scans to the next space. Resuming at the end of the URL should fix it.

    Needs a decision (outside the agreed scope):

    • [text](https://<64-char label>.com) and the matching image markdown now show as raw markdown. Main renders them as a link and an image.
    • Main linked the valid tail of hostnames with bad label edges, such as bad-.example.com, foo_.example.com and https://-bad.example.com. The PR leaves them plain. That's defensible, but https://bad-.example.com still partly links https://bad, which is inconsistent.
    Investigation details
    • I compared new ExpensiMark().replace() output on main (a61b5b4) and the PR for about 160 inputs. 34 outputs differ. Most are intended: 64-character labels and 254-character hostnames now stay plain. 63-character labels and 253-character hostnames still link.
    • These cases are unchanged from main:
      • Ports, query strings, fragments and trailing punctuation
      • Punycode, IPv4, www. and co.uk
      • Emails, mentions and #room
      • Inline code and code blocks
      • Long paths
      • Inputs around MAX_MARKUP_LENGTH
      • htmlToMarkdown and htmlToText round-trips
    • Cases from earlier review comments are fixed and match main: example.com..nonsense, example.com.Thanks! and google.com....
    • Other timings: the repro and 'a.'.repeat(n)+'com' are the same speed or faster on the PR. Comma-separated domain lists were already super-linear on main, and the PR makes them 14–29% slower. A typical chat message takes 0.157 ms on main and 0.188 ms on the PR.
    • I tested with Jest/node against the library directly. I didn't do a live web test, because App needs a dependency bump first.

    Next Steps: Uzaifm127 can address items 1–3 in the expensify-common PR. Reply with @MelvinBot <your feedback> to refine this analysis.


    view run

  18. Uzaifm127 commented on Oct 21, 2026

    @Uzaifm127
    Contributor

    Update: expensify-common PR has been merged 🎉

    I'll open the version bump PR in the App tomorrow (IST).

    TY!

  19. Uzaifm127 commented on Oct 22, 2026

    @Uzaifm127
    Contributor

    Update: Draft PR is up.

    PR is WIP. Thanks.

  20. Uzaifm127 commented on Oct 23, 2026

    @Uzaifm127
    Contributor

    @linhvovan29546 The upgrade PR is ready for review, Thanks!

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions