URL Parsing Corruption: When Multiple ? Breaks Tracking
Multiple question marks corrupt URL parameter parsing. Learn how browsers handle malformed URLs and why GA4 misses campaign data.
Your URL has multiple question marks. Your browser's URL parser is confused. GA4 receives corrupted data.
This is the technical deep-dive into URL parsing corruption and why RFC 3986 allows only ONE question mark per URL.
Table of contents
- How URL Parsers Work
- What Happens with Multiple Question Marks
- Example 1: Two Question Marks
- Example 2: Three Question Marks
- Impact on GA4 Tracking
- With Multiple Question Marks
- Server-Side vs Client-Side Parsing
- Server-Side (What Your Server Receives)
- Client-Side (JavaScript)
- RFC 3986: The URL Standard
- How Browsers Handle Malformed URLs
- Chrome/Edge (Chromium)
- Firefox
- Safari
- Debugging URL Parsing Issues
- Method 1: Browser DevTools
- Method 2: URL Validator Tool
- Real-World Corruption Scenarios
- Scenario 1: Dynamic URL Building
- Scenario 2: Server-Side Redirect
- FAQ
- Can I URL-encode the second question mark?
- Do all URL parsers behave the same way?
- What if I need a question mark in a parameter value?
- Will fixing this break historical data?
- How do I find URLs with multiple question marks?
- Conclusion
🚨 Not sure what's breaking your tracking?
Run a free 60-second audit to check all 40+ ways UTM tracking can fail.
Scan Your Campaigns Free✓ No credit card ✓ See results instantly
How URL Parsers Work
Modern browsers use standardized URL parsers based on WHATWG URL Standard:
const url = new URL('https://shop.com?param1=value1¶m2=value2');
console.log(url.origin); // https://shop.com
console.log(url.pathname); // /
console.log(url.search); // ?param1=value1¶m2=value2
console.log(url.searchParams.get('param1')); // value1The parser expects:
- ONE question mark
?starting the query string - Parameters separated by ampersands
& - Key-value pairs using equal signs
=
What Happens with Multiple Question Marks
Example 1: Two Question Marks
const url = new URL('https://shop.com?page=1?utm_source=facebook');
console.log(url.search); // ?page=1?utm_source=facebook
console.log(url.searchParams.get('page')); // "1?utm_source=facebook"
console.log(url.searchParams.get('utm_source')); // nullWhat happened:
- Parser found first
?at position after domain - Everything after first
?= query string - Second
?treated as part of the value forpage utm_sourcenever parsed as a separate parameter
Example 2: Three Question Marks
const url = new URL('https://shop.com?a=1?b=2?c=3');
console.log(url.searchParams.get('a')); // "1?b=2?c=3"
console.log(url.searchParams.get('b')); // null
console.log(url.searchParams.get('c')); // nullOnly the first parameter is parsed. Everything after becomes part of its value.
😰 Is this your only tracking issue?
This is just 1 of 40+ ways UTM tracking breaks. Most marketing teams have 8-12 critical issues they don't know about.
• 94% of sites have UTM errors
• Average: $8,400/month in wasted ad spend
• Fix time: 15 minutes with our report
✓ Connects directly to GA4 (read-only, secure)
✓ Scans 90 days of data in 2 minutes
✓ Prioritizes issues by revenue impact
✓ Shows exact sessions affected
Impact on GA4 Tracking
GA4's measurement protocol expects clean parameter parsing:
// GA4 extracts UTMs like this
const params = new URLSearchParams(window.location.search);
const source = params.get('utm_source');
const medium = params.get('utm_medium');
const campaign = params.get('utm_campaign');With Multiple Question Marks
// URL: site.com?page=home?utm_source=facebook&utm_medium=cpc
const params = new URLSearchParams(window.location.search);
// search = "?page=home?utm_source=facebook&utm_medium=cpc"
params.get('page'); // "home?utm_source=facebook&utm_medium=cpc"
params.get('utm_source'); // null
params.get('utm_medium'); // nullResult: GA4 attributes traffic as "Direct" (no campaign parameters found).
Server-Side vs Client-Side Parsing
Server-Side (What Your Server Receives)
# Flask example
from flask import request
@app.route('/page')
def page():
# URL: site.com?a=1?b=2
print(request.args.get('a')) # "1?b=2"
print(request.args.get('b')) // NoneServers follow the same parsing rules: Only one ? marks the query string start.
Client-Side (JavaScript)
// Same behavior in browser
const urlParams = new URLSearchParams(window.location.search);
urlParams.get('a'); // "1?b=2"
urlParams.get('b'); // nullBoth client and server see corrupted data.
RFC 3986: The URL Standard
The Internet Engineering Task Force (IETF) defines URL structure in RFC 3986:
URI = scheme:[//authority]path[?query][#fragment]
Key rules:
1. Query component BEGINS with "?" delimiter
2. Query component ENDS with "#" (fragment) or end of URI
3. Only ONE "?" character delimits start of query
4. Within query, "&" separates parameters
From RFC 3986 Section 3.4:
The query component is indicated by the first question mark ("?") character and terminated by a number sign ("#") character or by the end of the URI.
Implication: The FIRST ? starts the query. Any additional ? characters are part of the query string data, not delimiters.
How Browsers Handle Malformed URLs
Different browsers may handle edge cases slightly differently, but all follow WHATWG URL Standard:
Chrome/Edge (Chromium)
const url = new URL('https://site.com?a=1?b=2');
console.log(url.href); // https://site.com?a=1?b=2
console.log(url.search); // ?a=1?b=2
// Second ? preserved as-is in query stringFirefox
const url = new URL('https://site.com?a=1?b=2');
console.log(url.href); // https://site.com?a=1?b=2
console.log(url.search); // ?a=1?b=2
// Same behavior as ChromiumSafari
const url = new URL('https://site.com?a=1?b=2');
console.log(url.href); // https://site.com?a=1?b=2
console.log(url.search); // ?a=1?b=2
// Consistent with other browsersAll modern browsers follow the same standard.
Debugging URL Parsing Issues
Method 1: Browser DevTools
// Open console on your page
const current = window.location.href;
console.log('Full URL:', current);
console.log('Query string:', window.location.search);
console.log('Parsed params:', Object.fromEntries(new URLSearchParams(window.location.search)));Expected output (healthy):
Full URL: https://site.com?utm_source=facebook&utm_medium=cpc
Query string: ?utm_source=facebook&utm_medium=cpc
Parsed params: {utm_source: "facebook", utm_medium: "cpc"}
Corrupted output (multiple ?):
Full URL: https://site.com?page=1?utm_source=facebook
Query string: ?page=1?utm_source=facebook
Parsed params: {page: "1?utm_source=facebook"}
Method 2: URL Validator Tool
function validateURL(urlString) {
try {
const url = new URL(urlString);
// Count question marks in full URL
const questionMarkCount = (urlString.match(/\?/g) || []).length;
if (questionMarkCount > 1) {
console.error(`ERROR: ${"{"}{"{"}questionMarkCount{"}"}{"}"}} question marks found (maximum: 1)`);
console.error(`Query string: ${url.search}`);
console.error(`This will corrupt parameter parsing`);
return false;
}
console.log('✅ URL structure is valid');
return true;
} catch (error) {
console.error('Invalid URL:', error.message);
return false;
}
}
// Test
validateURL('https://site.com?a=1?b=2');
// ERROR: 2 question marks found (maximum: 1)Real-World Corruption Scenarios
Scenario 1: Dynamic URL Building
// E-commerce filter system
function buildProductURL(filters, utmParams) {
let url = 'https://shop.com/products';
// Add filters
if (filters.category) {
url += '?category=' + filters.category;
}
if (filters.price) {
url += '?price=' + filters.price; // ❌ BUG: Should be &
}
// Add UTMs
if (utmParams) {
url += '?' + utmParams; // ❌ BUG: Should check for existing ?
}
return url;
}
// Result: shop.com/products?category=shoes?price=50?utm_source=email
// Three question marks!Scenario 2: Server-Side Redirect
# Flask redirect with UTMs
@app.route('/old-page')
def redirect_with_utm():
base_url = request.args.get('redirect_url', '/home')
# base_url = "https://site.com?welcome=true"
utm_params = "utm_source=redirect&utm_medium=internal"
# ❌ BUG: Doesn't check if base_url has parameters
redirect_url = f"`{"{"}{"{"}base_url{"}"}{"}"}}`?`{"{"}{"{"}utm_params{"}"}{"}"}}`"
# Result: https://site.com?welcome=true?utm_source=redirect
return redirect(redirect_url)✅ Fixed this issue? Great! Now check the other 39...
You just fixed one tracking issue. But are your Google Ads doubling sessions? Is Facebook attribution broken? Are internal links overwriting campaigns?
• Connects to GA4 (read-only, OAuth secured)
• Scans 90 days of traffic in 2 minutes
• Prioritizes by revenue impact
• Free forever for monthly audits
Join 2,847 marketers fixing their tracking daily
FAQ
Can I URL-encode the second question mark?
Yes: ?param1=value%3Fparam2=value2. But this is confusing. Better to use & as the separator.
Do all URL parsers behave the same way?
Yes. All modern browsers and server frameworks follow RFC 3986 and WHATWG URL Standard.
What if I need a question mark in a parameter value?
URL-encode it: ?message=hello%3Fworld (%3F is the encoded ?).
Will fixing this break historical data?
Historical data with multiple ? will remain corrupted. The fix only affects future traffic.
How do I find URLs with multiple question marks?
Search your codebase for patterns like url + '?' or regex \?.*\?.
Conclusion
Multiple question marks corrupt URL parameter parsing because:
- RFC 3986 allows only ONE
?to start query strings - Browsers treat subsequent
?as part of parameter values - GA4 can't extract UTM parameters from corrupted query strings
Fix: Use ? for the first parameter, & for all subsequent parameters.
❌ CORRUPTED: site.com?a=1?b=2?c=3
✅ CORRECT: site.com?a=1&b=2&c=3
Technical Reference: Multiple Question Marks Validation Rule