Skip to content

[Due for payment 2026-06-02] [$250] [Sentry: APP-1BQ] Mobile Safari SyntaxError (DOMException SYNTAX_ERR) on /search/r/:reportID #91227

Closed

Description

@mountiny

Sentry

https://expensify.sentry.io/issues/APP-1BQ

Impact (snapshot at filing)

  • Users (total since first seen): 1,783
  • Events: 4,411
  • Users (last 14d): ~68
  • First seen: 2025-11-18
  • Last seen: now (ongoing)
  • Platform: web standalone — Mobile Safari only (iOS 18.x)
  • App version(s): seen through 9.3.77
  • Affected route(s) (parameterized): /search/r/:reportID
  • View name: SearchMoneyRequestReport
  • Sentry tag: DOMException.code: 12 (WebKit's SYNTAX_ERR)
  • Mechanism: auto.browser.global_handlers.onunhandledrejection — unhandled Promise rejection

Stack trace

Sentry reports No stacktrace available — typical for WebKit Promise rejections without a captured stack.

Suspected cause

Unknown. WebKit raises DOMException code 12 / SYNTAX_ERR from a handful of APIs that fail to parse a string:

  • new URL(maybeRelativeOrEmpty) — most likely candidate given the routing layer is involved
  • IDBKeyRange.only/bound(...) with an invalid key
  • CSS.parse/QuerySelector with an unrecognized selector
  • RegExp literal with a Mobile Safari-specific incompatibility (e.g. lookbehind in older WebKit)

Mobile Safari only, only on /search/r/:reportID, and only as an unhandled Promise rejection strongly suggests a new URL(...) constructed from data that's permitted by Chromium/Firefox but rejected by WebKit's stricter validator. A try/catch around the suspect URL construction (and an instrumented log of the offending input) would both stabilize the page and confirm the source.

Reproduction

Unknown deterministically. Open /search/r/:reportID on iOS Safari with an unusual report payload; the failure rate suggests a specific data shape rather than a UA-wide bug.

Investigation suggestions

  • Search the codebase for unconditional new URL(...) calls in the search/report rendering path
  • Add a window.addEventListener('unhandledrejection', ...) instrumentation that logs the message + input in a Sentry tag, to capture the offending string
  • Bisect by report attributes (description with embedded URLs, custom slug, etc.)

Related

  • Sibling Sentry IDs (if any): none identified
  • Prior GH issues: none found for string did not match the expected pattern
Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. self-assigned this
    on May 21, 2026
  2. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on May 21, 2026
  3. MelvinBot commented on May 21, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    There are two likely sources of the DOMException code 12 (SYNTAX_ERR) on Mobile Safari, both involving window.history.pushState / replaceState calls that WebKit validates more strictly than Chromium:

    1. Modal/index.tsx:71 — passes null as the URL argument to pushState:

      window.history.pushState({...currentState, shouldGoBack: true}, '', null);

      WebKit can throw SYNTAX_ERR when null is passed as the URL to pushState. Any modal opened during the search flow (e.g. hold/reject reason modals in the RHP) triggers this.

    2. Sound/index.ts:14-28 and lines 58-69 — Cache API .then() chains with no .catch() handlers. Safari's Cache API can throw DOMException SYNTAX_ERR for certain inputs, and without error handling these become unhandled Promise rejections. cacheSoundAssets() runs eagerly on module load (line 92).

    The Sentry mechanism (auto.browser.global_handlers.onunhandledrejection) confirms this is an unhandled Promise rejection. The Modal/index.tsx call is synchronous (not in a Promise), so it would surface as an uncaught exception rather than an unhandled rejection. This makes the Sound module's Cache API calls the most likely culprit — they are Promise-based, lack .catch(), run on every page load, and Safari's Cache API is known to throw SYNTAX_ERR in certain conditions.

    Additionally, Link.ts:306 has an unprotected new URL(url) inside a nested .then() chain that would also produce an unhandled rejection if the deep link URL is malformed on WebKit, though this only fires for deep-link navigations.

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

    1. Add .catch() handlers to all Cache API Promise chains in Sound/index.ts — both cacheSoundAssets() (line 14) and playSound() (line 58). Log errors via Log.alert (consistent with the existing clearSoundAssetsCache pattern at line 86).

    2. Change null to '' (empty string) in Modal/index.tsx:71 — pushState({...}, '', '') preserves the current URL without triggering WebKit's strict URL validation on null.

    3. Wrap the new URL(url) at Link.ts:306 in a try/catch — if parsing fails, skip the exitTo check (safe default since it only gates a workspace-new redirect).

    What alternative solutions did you explore? (Optional)

    • Adding a global unhandledrejection listener with instrumented logging to capture the exact failing input string — useful for diagnosis but doesn't fix the underlying issue.
    • Wrapping all new URL() calls in a shared safeParseURL utility — would help long-term but is broader than needed for this fix. There are ~6 unprotected new URL() calls in the runtime code (in Url.ts, shouldOpenOnAdminRoom.ts, ReportsSplitNavigator.tsx, App.ts) that could benefit from this, but those all parse window.location.href which is always a valid URL.
    Supporting evidence

    Why the Sound module Cache API is the primary suspect:

    • Sentry reports auto.browser.global_handlers.onunhandledrejection — this is a Promise rejection, not a synchronous throw
    • cacheSoundAssets() runs on module import (line 92), which happens on every page load including /search/r/:reportID
    • The .then() chains at lines 14-28 and 58-69 have zero error handling — any failure becomes an unhandled rejection
    • Safari's Cache API is documented to throw DOMException for operations it considers invalid
    • The pattern matches: Mobile Safari only, ongoing since 2025-11-18, affects ~68 users in last 14 days (subset of iOS Safari users)

    Why Modal/index.tsx is a secondary suspect:

    • pushState(state, '', null) is synchronous — it would typically surface as an uncaught exception, not an unhandled Promise rejection
    • However, if called during a Promise-based navigation flow (e.g., useLinking's state change handler), the exception could be caught by the Promise and become an unhandled rejection
    • The route is in RightModalNavigator, so modal lifecycle events are definitely involved

    Other unprotected new URL() calls found:

    • Link.ts:306 — inside nested .then(), high risk for deep-link flows
    • Url.ts:71, shouldOpenOnAdminRoom.ts:5, ReportsSplitNavigator.tsx:42, App.ts:792 — all parse window.location.href, lower risk

    Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on May 21, 2026
  5. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on May 21, 2026
  6. melvin-bot commented on May 21, 2026

    @melvin-bot

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

  7. KJ21-ENG commented on May 21, 2026

    @KJ21-ENG
    Contributor

    Proposal

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

    Mobile Safari users can hit an unhandled DOMException SYNTAX_ERR while opening a money request report from Search at /search/r/:reportID. The page has no useful stack trace in Sentry, so the practical goal is to remove the Safari-only parser edge in the route initialization path and add enough defensive handling that malformed browser URL state cannot take down the report view.

    What is the root cause of that problem?

    The fragile path is in report split initialization. ReportsSplitNavigator reads window.location.href through getCurrentUrl(), then immediately calls new URL(currentURL) and runs the result through CONST.REGEX.REPORT_ID_FROM_PATH.

    const currentURL = getCurrentUrl();
    const isTransitioning = currentURL.includes(ROUTES.TRANSITION_BETWEEN_APPS);
    const reportIdFromPath = currentURL ? new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1) : undefined;

    That route-initialization code runs while the app is mounting its report stack under the active route, including /search/r/:reportID. On Mobile Safari, new URL(...) is one of the APIs that throws WebKit DOMException code 12 / SYNTAX_ERR for strings it refuses to parse. Because this happens during initial state setup, the exception escapes the navigation render path and shows up as an unhandled promise rejection with no useful stack.

    There is a second Safari-sensitive detail in the same statement: REPORT_ID_FROM_PATH uses a negative lookbehind to exclude /search/r/:id.

    REPORT_ID_FROM_PATH: /(?<!\/search)\/r\/(\d+)/,

    Even if current iOS generally supports lookbehind, this is unnecessary for this route extraction and leaves the same code path dependent on WebKit parser behavior. The report ID parser only needs to recognize top-level /r/:id, not /search/r/:id.

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

    1. Replace the new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH) call in ReportsSplitNavigator with a small helper that extracts the pathname without throwing. On web, prefer window.location.pathname or the already guarded Url.getPathFromURL(currentURL); if parsing fails, return undefined and continue to the existing transition / last-accessed-report fallback.
    2. Replace CONST.REGEX.REPORT_ID_FROM_PATH with a Safari-safe parser for top-level report routes. For example, split the normalized pathname into segments and return the report ID only when the first segment is r and the second segment is numeric. That preserves the intentional exclusion of /search/r/:reportID without relying on negative lookbehind.
    3. Add focused unit coverage for the helper with these cases: /r/123 returns 123, /search/r/123 returns undefined, malformed or empty URL input returns undefined, and URLs with query/hash still extract the top-level report ID.

    Scope boundaries: I would touch ReportsSplitNavigator and the shared report-ID-from-path helper/constant only. This affects web navigation startup, especially Mobile Safari standalone, and should not change native behavior. I would not change Url.getPathFromURL(), link opening, or SearchMoneyRequestReport rendering because those paths are already guarded or are not the throwing parser edge shown above.

    What alternative solutions did you explore? (Optional)

    I checked for competing proposals and there are no issue comments yet. I also considered adding a global unhandledrejection listener for this Sentry event, but that would only improve logging after the page has already failed. The better fix is to remove the throwing URL/regex parser from navigation initialization and make this path non-throwing by construction.

  8. changed the title [-][Sentry: APP-1BQ] Mobile Safari SyntaxError (DOMException SYNTAX_ERR) on /search/r/:reportID[/-] [+][$250] [Sentry: APP-1BQ] Mobile Safari SyntaxError (DOMException SYNTAX_ERR) on /search/r/:reportID[/+] on May 21, 2026
  9. melvin-bot commented on May 21, 2026

    @melvin-bot
  10. keeze12 commented on May 21, 2026

    @keeze12

    Proposal

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

    Mobile Safari users can hit an unhandled DOMException SYNTAX_ERR while visiting /search/r/:reportID, which breaks the money request report flow without a useful JS stack in Sentry.

    What is the root cause of that problem?

    I think the most concrete browser-API failure in this path is the modal navigation guard in src/components/Modal/index.tsx:

    window.history.pushState({...currentState, shouldGoBack: true}, '', null);

    The third pushState argument is the target URL. Passing null is unnecessary here because the code is only trying to push a same-page guard entry. Chromium tolerates this, but WebKit is stricter around URL parsing for History API calls and can throw DOMException / SYNTAX_ERR when the URL argument is not parseable in the current document context.

    This also fits the symptom better than ReportsSplitNavigator's new URL(getCurrentUrl()) path: on web, getCurrentUrl() returns window.location.href, which is already an absolute URL. The report screen can still open modals/RHP flows after landing from /search/r/:reportID, so the guard-entry pushState can be reached during this view even when the top-level route parsing succeeds.

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

    Replace the null URL argument with an explicit current URL, or omit the third argument entirely. I would use the current URL to preserve the existing guard-entry behavior without asking WebKit to parse null:

    window.history.pushState({...currentState, shouldGoBack: true}, '', window.location.href);

    Then add a focused test around the modal navigation guard to ensure pushState is called with a non-null URL when shouldHandleNavigationBack is enabled.

    What specific scenarios should we cover in automated tests?

    1. Opening a modal with shouldHandleNavigationBack calls history.pushState with shouldGoBack: true and a string URL, not null.
    2. Closing the modal still removes the popstate listener and keeps the existing history.back() behavior when the guard state is active.
    3. Existing modals without shouldHandleNavigationBack do not push a guard history entry.

    What alternative solutions did you explore?

    • Wrapping the existing pushState(..., null) in try/catch: this would hide the crash but would silently disable the back-button guard on Safari.
    • Changing ReportsSplitNavigator URL parsing: reasonable as a defensive follow-up, but less likely to be the root cause because window.location.href should already be absolute on web.
  11. melvin-bot commented on May 21, 2026

    @melvin-bot

    📣 @keeze12! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  12. 36 remaining items

  13. mallenexpensify commented on Jun 3, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @gijoe0295 due $125 via NewDot

    no tests on the PR.

  14. gijoe0295 commented on Jun 4, 2026

    @gijoe0295
    Contributor

    @mallenexpensify I investigated the issue myself here because Melvin's/others' proposals are not correct. Melvin implemented with my support and I reviewed it so I think I'm due 100% here. No test was done because there are no consistently reproducible steps.

    cc @mountiny

  15. mallenexpensify commented on Jun 5, 2026

    @mallenexpensify
    Contributor

    @mountiny what do you think? I can see it both ways, if we pay 100% we'd need/want to consider this an edge case and/or provide reason, to avoid setting precedence (even though 'no consistently reliable steps' is rate (but. 'no test to complete' is common)

    Standard prices are
    - PR review where testing isn’t involved $125 ie. here

    • PR review for minor edits $125. ie. text or single-character changes, even when screenshots/tests are included. Example issue and PR
    • When followup PRs are needed, the default is $125. For large PRs and/or those requiring extensive testing, the C+ should comment on the issue to propose $250. These apply when there were no regression.
    • PR review from an internal GH issue that does require testing $250. This includes reviewing PRs for a project/feature.
  16. mountiny commented on Jun 5, 2026

    @mountiny
    ContributorAuthor

    Seems like thi sis fixed in Sentry, I think we can go with $250

  17. mallenexpensify commented on Jun 5, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @gijoe0295 due $250 via NewDot

  18. JmillsExpensify commented on Jun 16, 2026

    @JmillsExpensify
    Contributor

    $250 approved for @gijoe0295

  19. moved this from Done to SUBISSUE in [#whatsnext] #qualityon Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions