From 2ad9a20cc52aaf27885651a0d685df5aa16f0d52 Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Tue, 17 Mar 2026 11:50:20 +0000 Subject: [PATCH] fix(twenty-shared): stop decoding URI components in URL normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/15564?type=bug The `lowercaseUrlOriginAndRemoveTrailingSlash` function applies `decodeURIComponent` to URL pathname and search components, converting intentionally encoded characters like `%2F` to `/`, which breaks URLs that depend on encoding (e.g., Google Maps). Fix: Removed `safeDecodeURIComponent()` wrapping from `url.pathname` and `url.search` in `lowercaseUrlOriginAndRemoveTrailingSlash`. This function was added in commit `1119e3d77e` to handle accented characters, but `new URL()` already normalizes non-ASCII characters in pathnames (encoding `é` as `%C3%A9`). The `decodeURIComponent` call was incorrectly decoding structurally significant percent-encoded characters like `%2F` (encoded slash) back to `/`, breaking URLs that depend on encoding (e.g., Google Maps `data=` parameter). The fix restores the original `url.pathname + url.search + url.hash` concatenation, which correctly preserves all percent-encoding as provided by the URL constructor. The `safeDecodeURIComponent` utility itself is kept since it's still used by the IMAP message text extractor service. Updated test expectations to match correct URL normalization behavior: - Percent-encoded characters like `%2F`, `%20`, `%2520` are preserved as-is - Non-ASCII characters are encoded by the URL constructor (e.g., `é` → `%C3%A9`) - Added a Google Maps URL test case to prevent regression --- ...aseUrlOriginAndRemoveTrailingSlash.test.ts | 33 ++++++++++++------- ...owercaseUrlOriginAndRemoveTrailingSlash.ts | 6 +--- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts b/packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts index 7156ee94c85..220a8e1c24c 100644 --- a/packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts +++ b/packages/twenty-shared/src/utils/url/__tests__/lowercaseUrlOriginAndRemoveTrailingSlash.test.ts @@ -39,19 +39,19 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => { expected: 'https://www.example.com/TEST#Hash', }, { - title: 'should preserve special characters in path', + title: 'should encode special characters in path via URL normalization', input: 'https://test.test/edouard-ménard-22219837', - expected: 'https://test.test/edouard-ménard-22219837', + expected: 'https://test.test/edouard-m%C3%A9nard-22219837', }, { - title: 'should decode already encoded special characters in path', + title: 'should preserve already encoded special characters in path', input: 'https://test.test/edouard-m%C3%A9nard-22219837', - expected: 'https://test.test/edouard-ménard-22219837', + expected: 'https://test.test/edouard-m%C3%A9nard-22219837', }, { - title: 'should preserve special characters in query params', + title: 'should encode special characters in query params', input: 'https://example.com/path?name=José', - expected: 'https://example.com/path?name=José', + expected: 'https://example.com/path?name=Jos%C3%A9', }, { title: @@ -60,13 +60,12 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => { expected: 'https://example.com/test%E0%A4%A', }, { - title: - 'should preserve double-encoded URLs (encoded percent signs stay encoded once)', + title: 'should preserve double-encoded URLs without decoding them', input: 'https://example.com/test%2520name', - expected: 'https://example.com/test%20name', + expected: 'https://example.com/test%2520name', }, { - title: 'should preserve special characters in hash fragments', + title: 'should encode special characters in hash fragments', input: 'https://example.com/path#frédéric', expected: 'https://example.com/path#fr%C3%A9d%C3%A9ric', }, @@ -76,9 +75,19 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => { expected: 'https://example.com/path#fr%C3%A9d%C3%A9ric', }, { - title: 'should handle mixed encoded and non-encoded in same URL', + title: + 'should preserve percent-encoded slashes and query params without decoding', input: 'https://example.com/path%2Fwith%2Fslashes?query=hello%20world', - expected: 'https://example.com/path/with/slashes?query=hello world', + expected: + 'https://example.com/path%2Fwith%2Fslashes?query=hello%20world', + }, + { + title: + 'should preserve percent-encoded characters in Google Maps URLs', + input: + 'https://www.google.com/maps/place/Test/data=!16s%2Fg%2F1ptwh8096', + expected: + 'https://www.google.com/maps/place/Test/data=!16s%2Fg%2F1ptwh8096', }, ])('$title', ({ input, expected }) => { expect(lowercaseUrlOriginAndRemoveTrailingSlash(input)).toBe(expected); diff --git a/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts b/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts index 26a5bf48297..b21769c7124 100644 --- a/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts +++ b/packages/twenty-shared/src/utils/url/lowercaseUrlOriginAndRemoveTrailingSlash.ts @@ -1,6 +1,5 @@ import { getURLSafely } from '@/utils/getURLSafely'; import { isDefined } from '@/utils/validation'; -import { safeDecodeURIComponent } from './safeDecodeURIComponent'; export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => { const url = getURLSafely(rawUrl); @@ -10,10 +9,7 @@ export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => { } const lowercaseOrigin = url.origin.toLowerCase(); - const path = - safeDecodeURIComponent(url.pathname) + - safeDecodeURIComponent(url.search) + - url.hash; + const path = url.pathname + url.search + url.hash; return (lowercaseOrigin + path).replace(/\/$/, ''); };