From 9af92f5d8cb318fd16739fb0afd1997356f4da83 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Sun, 6 Sep 2026 07:16:16 -0400 Subject: [PATCH] fix(url_decode): keep %2B as a literal plus when decoding (#939) url_decode decoded the percent-encoding first and only then replaced "+" with a space, so a "%2B" became "+" and was immediately turned into a space. Any literal "+" was therefore lost when round-tripped through url_encode. I now replace "+" with a space before decodeURIComponent, which lines up with Ruby's CGI.unescape used by Shopify. --- src/filters/url.ts | 2 +- test/integration/filters/url.spec.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/filters/url.ts b/src/filters/url.ts index 77a9cab1c..ddfbd88ff 100644 --- a/src/filters/url.ts +++ b/src/filters/url.ts @@ -1,6 +1,6 @@ import { stringify } from '../util/underscore' -export const url_decode = (x: string) => decodeURIComponent(stringify(x)).replace(/\+/g, ' ') +export const url_decode = (x: string) => decodeURIComponent(stringify(x).replace(/\+/g, ' ')) export const url_encode = (x: string) => encodeURIComponent(stringify(x)).replace(/%20/g, '+') export const cgi_escape = (x: string) => encodeURIComponent(stringify(x)) .replace(/%20/g, '+') diff --git a/test/integration/filters/url.spec.ts b/test/integration/filters/url.spec.ts index b18649a49..eef829b86 100644 --- a/test/integration/filters/url.spec.ts +++ b/test/integration/filters/url.spec.ts @@ -7,6 +7,14 @@ describe('filters/url', () => { const html = liquid.parseAndRenderSync('{{ "%27Stop%21%27+said+Fred" | url_decode }}') expect(html).toEqual("'Stop!' said Fred") }) + it('should decode %2B to a literal plus', () => { + const html = liquid.parseAndRenderSync('{{ "1%2B1" | url_decode }}') + expect(html).toEqual('1+1') + }) + it('should keep a literal plus when round-tripped through url_encode', () => { + const html = liquid.parseAndRenderSync('{{ "a+b c" | url_encode | url_decode }}') + expect(html).toEqual('a+b c') + }) }) describe('url_encode', () => {