URL parser · Guide

How to Parse URL Parameters Without Losing Values

Parsing URL parameters means separating the query from the rest of the URL, splitting it into key-value pairs, and percent-decoding each component once. Standard URL APIs already know the order; manual `split('&')` code does not handle repeated keys, missing values, plus-for-space rules or malformed input consistently. Parse the full URL with a platform library, then validate the resulting strings for your application's expected types.

Separate the URL components first

The query begins after ? and ends before #. A fragment is client-side data and is not sent in an HTTP request. Splitting an arbitrary URL on ? or # by hand becomes fragile once encoded delimiters or relative references appear.

JavaScript
const url = new URL(
  'https://example.com/search?q=red%20blue&page=2#results'
);
console.log(url.pathname); // /search
console.log(url.search);   // ?q=red%20blue&page=2
console.log(url.hash);     // #results

When input may be relative, provide a trusted base: new URL('/search?q=x', 'https://example.com'). Do not use an attacker-controlled base when later validating origins, because resolution changes what host the URL refers to.

For a raw query string, URLSearchParams accepts the part with or without a leading question mark. It does not parse a complete URL; passing a full URL makes the URL text part of the first key.

Read values with URLSearchParams

Use get for the first value, getAll for repeated keys, has to distinguish absence, and iteration when unknown keys are allowed. Values are returned decoded, including Unicode and form-style plus signs.

JavaScript
const p = new URLSearchParams('?tag=red&tag=blue&empty=&flag');
console.log(p.get('tag'));      // red
console.log(p.getAll('tag'));   // ['red', 'blue']
console.log(p.has('empty'));    // true
console.log(p.get('empty'));    // ''
console.log(p.get('missing'));  // null
console.log(p.get('flag'));     // ''

Calling Object.fromEntries(p) silently keeps only the last occurrence of a duplicate key. That may be correct for your contract, but it must be a conscious choice. Access-control checks are especially dangerous if one component validates the first value and another uses the last.

Parameter names are decoded too. Treat them as untrusted strings and allow-list expected names rather than copying arbitrary keys onto an object, where names such as __proto__ have caused prototype-manipulation bugs in unsafe code.

Understand decoding and plus signs

Percent escapes represent bytes; query parsers decode their UTF-8 representation. Under form-style rules, + becomes a space before values are returned, while %2B becomes a literal plus. This is why hand-written calls to decodeURIComponent do not exactly match query parsing.

JavaScript
const p = new URLSearchParams('q=C%2B%2B+language');
console.log(p.get('q')); // C++ language

console.log(decodeURIComponent('C%2B%2B+language'));
// C+++language: decodeURIComponent does not convert + to space

Do not decode a value returned by URLSearchParams again. A legitimate value %2F may become / on the second pass, changing later path validation. One parser should own one decoding pass.

If legacy input uses a non-UTF-8 encoding, require an explicit compatibility path. Guessing character sets after decoding produces mojibake and inconsistent security checks across services.

Parse on the server with standard libraries

Server frameworks usually expose an already parsed query collection. When working below the framework, use a URL or query library so malformed sequences and repeated values have defined behaviour. Python's parse_qs retains lists; PHP's parse_str follows PHP-specific bracket conventions.

Python
from urllib.parse import urlsplit, parse_qs

u = urlsplit('https://e.test/?tag=red&tag=blue&empty=')
params = parse_qs(u.query, keep_blank_values=True)
print(params)
# {'tag': ['red', 'blue'], 'empty': ['']}
PHP
$query = 'filter[color]=blue&page=2';
parse_str($query, $params);
// ['filter' => ['color' => 'blue'], 'page' => '2']

// Bracket nesting is a PHP convention, not a URL standard.

Never call parse_str($query) without its result argument in modern code. More broadly, do not let parsed query keys become local variables. Keep them in a dedicated map and copy only expected fields into typed application input.

Convert strings to application types explicitly

A query parser returns strings, not trusted booleans or numbers. In JavaScript, Boolean('false') is true, and parseInt('10items') returns 10. Validate the entire string before converting, then enforce domain ranges.

JavaScript
const rawPage = p.get('page');
if (rawPage !== null && !/^[1-9]\d*$/.test(rawPage)) {
  throw new Error('page must be a positive integer');
}
const page = rawPage === null ? 1 : Number(rawPage);
if (!Number.isSafeInteger(page) || page > 10000) {
  throw new Error('page is out of range');
}

Define boolean syntax, for example exactly true and false, instead of relying on truthiness. Parse dates only in the promised format and preserve identifiers such as postal codes as strings when leading zeroes matter.

Apply defaults only to absent keys unless the contract says empty means absent. This distinction prevents ?limit= from unexpectedly receiving a privileged or expensive default.

Set limits and reject ambiguous input

A query string can contain thousands of keys, huge values or deep bracket nesting. Apply a total URL length limit at the proxy and application, cap parameter count and nesting, and reject inputs beyond the feature's needs. Limits prevent memory and CPU abuse before business validation runs.

Text
accepted keys: q, page, tag
maximum query bytes: 4096
maximum parameters: 50
maximum tag occurrences: 20
unknown keys: reject or ignore consistently
duplicate scalar keys: reject

Choose a duplicate-key rule for scalar settings. Rejecting role=user&role=admin is safer than allowing separate layers to choose first or last. Preserve duplicates only for fields explicitly modelled as lists.

Malformed % escapes should lead to a clear 400 response rather than partial interpretation. Different proxies and frameworks may normalise malformed input differently, so reject ambiguity as close to the edge as possible.

Debug the raw and parsed forms together

When a parameter is missing or changed, capture the raw request target and the parser output in a safe test environment. The browser address bar may display decoded characters, and DevTools may present a friendly view, so inspect the actual request representation as well.

Shell
curl -G 'https://example.com/search' \
  --data-urlencode 'q=C++ & Rust' \
  --data-urlencode 'tag=red' \
  --data-urlencode 'tag=blue'

# curl encodes each supplied name/value pair.

Log parameter names and validation errors, but redact tokens and sensitive values. A URL commonly leaks into access logs, browser history, analytics and referrer data; credentials do not belong in query parameters when headers or bodies are available.

Test empty values, repeated keys, literal plus signs, encoded ampersands, Unicode, fragments and malformed escapes. Those cases expose differences between a real parser and the simple happy path that manual splitting appears to handle.

Frequently asked questions

How do I parse URL parameters in JavaScript?

Construct a URL for the full URL, then read its searchParams. Use get() for a scalar, getAll() for repeated keys, and has() when an empty value must be distinguished from an absent parameter.

How do I parse a query string without a full URL?

Pass the raw query portion to URLSearchParams, with or without its leading ?. Do not pass a complete URL to that constructor; use new URL(fullUrl).searchParams instead.

Why does a plus sign become a space in URL parameters?

Query parsers commonly follow form rules, where + represents a space. A literal plus must arrive as %2B; standard query builders perform that encoding automatically.

How should duplicate query parameters be handled?

Use all values only for fields explicitly defined as lists. For scalar or security-sensitive fields, reject duplicates so two application layers cannot disagree about whether the first or last value wins.

Does URLSearchParams decode values automatically?

Yes. It percent-decodes keys and values and converts form-style plus signs to spaces. Do not call decodeURIComponent on the returned value again.

What is the difference between an empty and missing parameter?

?name= contains the key with an empty string, while no name pair is absent. Use has() or the server library's presence check before applying defaults, because treating both alike can change application behaviour.

Ready to try it?

Open the free browser-based URL parser and apply what you just read — no sign-up, runs locally.

Open the URL parser tool