URL Encoding in Python
Python's standard library splits URL encoding across four functions in urllib.parse, and the differences between them cause real bugs when the wrong one is chosen. quote percent-encodes a string using RFC 3986 rules1, leaving letters, digits, and _.-~ untouched by default, and it also treats / as safe unless you override the safe parameter. quote_plus behaves like quote but additionally converts a space into +, matching application/x-www-form-urlencoded2 conventions rather than %20. For building a whole query string from a dictionary or list of pairs, urlencode handles both the encoding and the joining with & and = in one call3. Decoding mirrors this split: unquote reverses quote, and unquote_plus additionally converts + back to a space. Consequently, picking quote when you meant quote_plus, or the reverse, is the most common source of stray plus signs or literal %20 sequences in Python-generated URLs.
quote versus quote_plus: the space and slash difference
The two functions diverge on exactly two points, and both matter. urllib.parse.quote(s) encodes a space as %20 and, by default, leaves a forward slash / unescaped because its safe parameter defaults to '/'3. quote_plus(s) encodes a space as + instead, and it does not treat / as safe, so a slash becomes %2F unless you pass a custom safe argument.
What both functions leave unchanged
Beneath those two differences, quote and quote_plus share the same unreserved character set1: letters, digits, hyphen, underscore, period, and tilde. That common base means most alphanumeric queries produce identical results from both functions unless the input contains spaces or slashes, which is why the bug pattern usually appears only when those characters are present.
Consequently, quote is the natural choice for encoding a path segment where slashes might be intentional structure, while quote_plus is the natural choice for a form-style query value where a + for space is expected by the receiving system. Passing safe='' to quote removes its slash exception entirely, which is worth doing whenever the value is data rather than a path fragment. Mixing the two without checking which convention the other end of your integration expects is the most frequent cause of spaces surviving as literal plus signs downstream.
Building a query string with urlencode
Constructing a full query string by hand, one quote_plus call at a time, invites the exact ordering mistake that produces a broken URL. urllib.parse.urlencode(query, doseq=False) accepts a dictionary or a sequence of two-item tuples and returns the fully assembled, correctly encoded query string in one step, encoding each key and value with quote_plus internally and joining them with & and `=2.
Handling repeated keys
When a key needs multiple values, such as tag=python&tag=web, pass doseq=True along with a dictionary whose values are lists, and urlencode expands each list into repeated key-value pairs automatically. Furthermore, urlencode accepts a quote_via argument, letting you swap in quote instead of the default quote_plus if you need %20 for spaces rather than +. Building the query string this way removes the chance of applying encoding after the delimiters are already assembled.
Decoding with unquote and unquote_plus
Reversing the process requires matching the decoder to the encoder that produced the string. urllib.parse.unquote(s) reverses percent-escapes back into their original bytes and characters but leaves any literal + untouched4, treating it as a real plus sign rather than a space. That distinction between a data character and a space is exactly why choosing the right decoder at the start of a debugging session matters: using plain unquote on form-encoded data produces output that looks decoded yet is still wrong.
Choosing between the two decoders
Because the decoder set mirrors the encoder set exactly, the choice is not a matter of preference but of matching the encoding that was actually used. If the string came from quote_plus or from an HTML form submission, unquote_plus is the correct decoder because it handles the +-for-space convention. If the string came from quote or from JavaScript's encodeURIComponent5, unquote is correct because it will not convert any literal + signs that are part of your data.
Conversely, unquote_plus(s) performs the same percent-decoding and additionally converts every + into a space, which is the correct choice for a string produced by quote_plus or by an HTML form submission. Passing a quote_plus-encoded string to plain unquote leaves stray plus signs in your decoded output, a bug that looks identical to the plus-sign confusion you would see with JavaScript's decodeURIComponent. Because the decoder set mirrors the encoder set exactly, the rule is simple: pair unquote with anything produced by quote, and pair unquote_plus with anything produced by quote_plus or by form submissions. Testing the round trip in the CapyToolkit decoder confirms whether a given encoded string needs the plus-aware decoder before you commit to one in your Python code.
When to use this
Reach for quote when encoding a path segment or any value where / should remain a literal structural character, and reach for quote_plus or urlencode when building a form-style query string where + for space is the expected convention. Use urlencode with a dictionary whenever you are assembling more than one parameter, since it removes the ordering mistakes that come from hand-joining encoded pieces.
Notes
All four functions live in urllib.parse, which is part of the standard library, so no external package is required for standard URL encoding in Python. quote and quote_plus both accept a safe keyword argument listing additional characters to leave unescaped, and both accept an encoding argument if you need a byte encoding other than the UTF-8 default. Requests library users should note that requests calls urlencode internally for the params argument, so manually pre-encoding values before passing them to requests.get(url, params=...) usually causes double encoding.
Examples
Encoding a path segment
urllib.parse.quote('notes/2026') Returns notes/2026 unchanged because quote treats / as safe by default; pass safe='' to escape it to %2F.
Encoding a form value
urllib.parse.quote_plus('hello world') Returns hello+world, using + for the space per application/x-www-form-urlencoded conventions.
Building a full query string
urllib.parse.urlencode({'q': 'cats & dogs', 'page': 2}) Returns q=cats+%26+dogs&page=2, correctly encoding and joining both parameters.
Verify with the URL Encoder / Decoder tool.
Encoding a path segment
urllib.parse.quote('notes/2026') Returns notes/2026 unchanged because quote treats / as safe by default; pass safe='' to escape it to %2F.
- 1.
IETF, "Unreserved Characters," RFC 3986 Section 2.3, rfc-editor.org, January 2005. https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3
- 2.
WHATWG, "application/x-www-form-urlencoded encoding algorithm," HTML Standard, html.spec.whatwg.org, accessed July 2026. https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
- 3.
Python Software Foundation, "urllib.parse," Python 3 documentation, docs.python.org, accessed July 2026. https://docs.python.org/3/library/urllib.parse.html
- 4.
WHATWG, "URL Standard," url.spec.whatwg.org, accessed July 2026. https://url.spec.whatwg.org/
- 5.
Mozilla Developer Network, "encodeURIComponent()," developer.mozilla.org, accessed July 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
quote encodes a space as %20 and leaves / unescaped by default. quote_plus encodes a space as + and always escapes / unless you pass a custom safe argument. Use quote for path segments where slashes matter, and quote_plus for form-style query values.
Use urllib.parse.urlencode(your_dict), which encodes every key and value and joins them with & and = in one call. For repeated keys from list values, pass doseq=True. This avoids the common mistake of encoding an already-assembled query string, which escapes the delimiters you meant to keep literal.
You likely decoded a quote_plus-encoded string with plain unquote, which does not convert + back to a space. Use unquote_plus instead whenever the source used form encoding. CapyToolkit's decoder converts %20 back to a space but leaves a literal + as is, mirroring unquote behavior.
If you manually percent-encode a value before passing it in the params dictionary, requests encodes it again internally via urlencode, producing double encoding. Pass the raw, un-encoded value in params and let requests handle encoding once. CapyToolkit's decoder can confirm whether a URL was double-encoded by decoding it twice.
Use urllib.parse.unquote(s) for standard percent-encoding, or unquote_plus(s) if the string also uses + for spaces. Both are in the standard library and require no installation. You can verify the expected output before committing to one in your code.
URL Encoding in PHP
PHP ships two URL-encoding functions that look interchangeable but follow different specifications, and mixing them up is a persistent source of bugs. rawurlencode follows RFC 39861, encoding a space as %20 and leaving the unreserved characters, including the tilde ~, untouched. urlencode follows the older application/x-www-form-urlencoded convention2 used by HTML forms, encoding a space as + instead of %20. Consequently, rawurlencode is the correct choice for a URL path segment3, a username, or any RFC 3986 context, while urlencode is the correct choice for building an HTML form-style query string that a typical $_GET or $_POST handler expects. The matching decode functions, rawurldecode and urldecode, follow the same split: only urldecode converts a + back into a space, so pairing the wrong decoder with an encoded string leaves stray plus signs in your output.
Why two functions exist for one job
PHP's two encoders trace back to two different eras of the web, and each era left a durable trace in PHP's own source code that still affects which function you should choose. urlencode predates widespread RFC 3986 adoption and was written to match how HTML forms serialize data4, which explains its space-as-plus behavior and its historical treatment of the tilde as a character needing escape.
rawurlencode was added later specifically to follow RFC 3986 precisely1, which is why it uses %20 for spaces and treats ~ as unreserved and therefore safe. Consequently, code written against form submissions, particularly anything reading $_GET or $_POST, tends to use urlencode-family functions because PHP itself decodes incoming form data using the form convention. Code that constructs a path segment, a REST API URL component, or anything meant to comply strictly with RFC 3986 should use rawurlencode instead, since it will not introduce a + that means something different in that context.
Why the split persists today
Because both functions remain in active use, the confusion survives in newer codebases even though the underlying specification dispute is settled. Any shared library that builds URLs for external APIs or that generates links consumed by JavaScript clients should choose one convention explicitly rather than inheriting whichever function happened to be convenient. Documenting that choice prevents future reviewers from silently swapping one function for the other and reintroducing encoding bugs.
In practice, this decision is easy to overlook during bug fixes or refactors, because both functions look like generic URL cleanup utilities rather than spec-specific encoders. A quick grep for urlencode and rawurlencode across a legacy codebase often reveals whether the team standardized on one Convention or mixed them depending on who wrote each file, and the inconsistency is a leading indicator of encoding bugs waiting to surface in production.
The tilde and other historical quirks
The tilde character has a small but real history in PHP's two encoders worth knowing before you audit older code. Very old PHP versions had rawurlencode escape the tilde, deviating from RFC 3986, until the behavior was corrected to leave ~ unescaped, matching the standard's unreserved set3. Because that correction happened inside PHP itself rather than in a library, projects upgrade their runtime without changing their source code and suddenly see different output from the same rawurlencode call, which can break string comparisons or cache keys if they were written against the old behavior. Documenting which PHP version introduced the fix makes regressions easier to trace.
What this means for current PHP
On a current PHP release, rawurlencode('~user') returns ~user unchanged, matching RFC 3986 exactly. The practical takeaway is that if %7E ever appears in a modern PHP application, the most likely cause is that the value was encoded with urlencode rather than rawurlencode, or that it was already percent-encoded before it reached PHP and was then double-escaped. If you maintain code written against a much older PHP version, or if you see %7E appearing where you expect a literal tilde, that is a legacy quirk worth flagging during a migration review. Furthermore, urlencode still encodes the tilde as part of its broader form-encoding character set, so the two functions continue to disagree on this one character even on modern PHP, which is one more reason to pick the function matching your actual target format rather than assuming they are equivalent.
Choosing the right function for the context
The decision is straightforward once you separate the two contexts these functions were built for. Building a URL path, encoding a filename for a Content-Disposition header, or producing an RFC 3986 compliant identifier calls for rawurlencode. Building a query string that mimics an HTML form submission2, or encoding a value you will decode later with urldecode, calls for urlencode.
Switching http_build_query to RFC 3986 mode
Consequently, mixing them, encoding with one and decoding with the other, is the fastest way to end up with a stray plus sign or an unexpected %20 in your output. PHP's http_build_query, the query-string equivalent of Python's urlencode, defaults to urlencode-style encoding but accepts a enc_type parameter (PHP_QUERY_RFC3986)5 to switch it to rawurlencode behavior when you need strict RFC 3986 compliance for the whole query string at once.
When to use this
Use rawurlencode for URL path segments, RFC 3986 compliant identifiers, and any value where a + should never be misread as a space. Use urlencode for form-style query values that a typical PHP request handler will decode with urldecode4. When building a full query string, prefer http_build_query with the PHP_QUERY_RFC3986 flag5 if you need rawurlencode semantics throughout.
Notes
Both function families are built into PHP core and require no extension or Composer package. rawurlencode and urlencode both operate on bytes, so multi-byte UTF-8 characters encode correctly as long as the input string is already valid UTF-8. http_build_query($data, '', '&', PHP_QUERY_RFC3986) is the closest PHP equivalent to combining rawurlencode with automatic key-value joining, useful when you want RFC 3986 output for an entire array of parameters at once.
Examples
RFC 3986 encoding for a path
rawurlencode('my file.txt') Returns my%20file.txt, using %20 for the space and following RFC 3986 exactly.
Form-style encoding for a query value
urlencode('my file.txt') Returns my+file.txt, using + for the space to match application/x-www-form-urlencoded.
Building an RFC 3986 query string
http_build_query($params, '', '&', PHP_QUERY_RFC3986)
Encodes every value with %20 for spaces instead of the default + behavior.
Verify with the URL Encoder / Decoder tool.
RFC 3986 encoding for a path
rawurlencode('my file.txt') Returns my%20file.txt, using %20 for the space and following RFC 3986 exactly.
- 1.
IETF, "Unreserved Characters," RFC 3986 Section 2.3, rfc-editor.org, January 2005. https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3
- 2.
WHATWG, "application/x-www-form-urlencoded encoding algorithm," HTML Standard, html.spec.whatwg.org, accessed July 2026. https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
- 3.
PHP Manual, "rawurlencode," developer.php.net, accessed July 2026. https://www.php.net/manual/en/function.rawurlencode.php
- 4.
PHP Manual, "urlencode," developer.php.net, accessed July 2026. https://www.php.net/manual/en/function.urlencode.php
- 5.
WHATWG, "URL Standard," url.spec.whatwg.org, accessed July 2026. https://url.spec.whatwg.org/
rawurlencode follows RFC 3986, encoding a space as %20. urlencode follows the application/x-www-form-urlencoded format, encoding a space as +. Use rawurlencode for path segments and RFC 3986 contexts, and urlencode for form-style query values that a typical request handler expects.
The string was likely encoded with urlencode and you decoded it with rawurldecode, which does not convert + back to a space. Use urldecode instead whenever the source used form-style encoding. CapyToolkit's decoder mirrors rawurldecode behavior and leaves a literal + unchanged.
No, not on current PHP versions. ~ is part of RFC 3986's unreserved set, so rawurlencode leaves it untouched, matching the standard. Very old PHP releases escaped it incorrectly; this was fixed to align with RFC 3986. urlencode still treats the tilde differently as part of its broader form-encoding character set.
Use http_build_query($array) to encode and join an associative array into a query string in one call. Pass PHP_QUERY_RFC3986 as the fourth argument if you need rawurlencode-style %20 spaces instead of the default urlencode-style +. This avoids manually joining encoded pieces with & and =.
rawurlencode is the closer match, since both follow RFC 3986 style percent-encoding and use %20 for spaces. urlencode diverges because it uses + for spaces, matching the older form-encoding convention rather than encodeURIComponent's RFC 3986 behavior.
URL Encoding in Java
java.net.URLEncoder is built for one specific job, and using it outside that job is the most common Java URL-encoding mistake. Its encode method converts a string into application/x-www-form-urlencoded MIME format1, the same convention HTML forms use, which means it encodes a space as + rather than %20 and leaves only letters, digits, and the four characters . - * _ unescaped. The current recommended call is URLEncoder.encode(s, StandardCharsets.UTF_8), which takes a Charset argument directly rather than the older, exception-throwing String charset name overload. Consequently, URLEncoder is the right tool for encoding an HTML form field or a form-style query value, and the wrong tool for encoding a URL path segment, since it was never designed for RFC 3986 compliance2 and does not know about path structure at all. For a complete, correctly structured URL, Java's java.net.URI class is the appropriate alternative3.
The exact character set URLEncoder preserves
URLEncoder's safe set is smaller and different from JavaScript's encodeURIComponent4, and understanding that difference matters the moment you cross from one language to another. It leaves unchanged only the alphanumeric characters and four punctuation marks: period, hyphen, asterisk, and underscore. Every other character, including the tilde, becomes a percent-escape, and a space specifically becomes a plus sign rather than %20.
What this means for cross-language porting
Because the two functions target different formats, copying an encoding call from JavaScript to Java without checking the safe set is a reliable way to introduce subtle bugs. A value that decoded correctly when produced by encodeURIComponent may arrive at the server looking correct while actually containing different escape sequences than the Java-generated version, which makes direct string comparison fail even when the original readable text was the same.
Consequently, comparing its output directly against encodeURIComponent reveals two differences: the space handling and the treatment of the tilde and other marks outside its narrow safe set. Because the encoding target is application/x-www-form-urlencoded5, this behavior is intentional and matches what a browser sends when a form is submitted with the GET or POST method, not an oversight in the Java standard library. Developers porting encoding logic from JavaScript to Java often assume the two behave identically, and the tilde is usually the first character where that assumption quietly fails.
Why URLEncoder is wrong for path segments
Path components need RFC 3986 rules2, and URLEncoder was never built to provide them. Passing a path segment through URLEncoder.encode converts a legitimate segment slash into a form-encoded space substitute in exactly the cases where you least want that, and it offers no way to tell it that / should remain literal or that %20, not +, is the correct space encoding for that context.
Building a full URI correctly
For assembling a complete, valid URL from parts, java.net.URI's multi-argument constructor, such as new URI(scheme, userInfo, host, port, path, query, fragment), handles the percent-encoding of each component according to its actual role in the URI grammar, correctly distinguishing a path's rules from a query's rules3. This is the appropriate class when you are not dealing with a single form-encoded value but with a structured URL that has multiple distinct parts, each governed by different escaping rules. Reaching for URI instead of URLEncoder also removes the need to manually track which characters are safe in which component, since the constructor already encodes that knowledge.
Using the Charset overload correctly
Two overloads of encode exist, and only one is current in modern Java. The older URLEncoder.encode(String s, String enc) takes a charset name as a string and declares a checked UnsupportedEncodingException1, forcing every call site to handle an exception that in practice never fires for a valid charset name like "UTF-8".
Why the String overload is legacy
The checked exception is not merely inconvenient; it also encourages wrapping encode in try-catch blocks that throw away useful context. If the encoding is always UTF-8 in practice, catching UnsupportedEncodingException does not recover anything meaningful, so the clutter remains for the life of the codebase. Switching to the Charset overload removes both the exception and the wrapper.
The newer URLEncoder.encode(String s, Charset charset), added in Java 10, takes a Charset object directly, most commonly StandardCharsets.UTF_8, and throws no checked exception, since a Charset object is guaranteed valid at compile time. Consequently, any Java codebase targeting Java 10 or later should prefer the Charset overload, both for cleaner code and because it removes a try-catch block that served no real purpose beyond satisfying the compiler.
When to use this
Use URLEncoder.encode(s, StandardCharsets.UTF_8) when encoding a value for application/x-www-form-urlencoded output4, such as a form field or a query parameter you know the receiving server decodes with form-encoding rules. Use java.net.URI's constructor when assembling a complete URL from structured parts, since it applies RFC 3986 rules appropriate to each component rather than the form-encoding rules URLEncoder always applies.
Notes
URLEncoder and its counterpart URLDecoder are part of the Java standard library in java.net, requiring no external dependency. Both operate correctly on any valid Charset, though UTF-8 is the practical default for web-facing code. URLDecoder.decode(s, StandardCharsets.UTF_8) reverses URLEncoder output and converts a + back into a space, matching the form-encoding convention; it will throw an IllegalArgumentException on a malformed percent-escape sequence.
Examples
Encoding a form value
URLEncoder.encode("hello world", StandardCharsets.UTF_8) Returns hello+world, using + for the space per application/x-www-form-urlencoded.
Decoding a form-encoded value
URLDecoder.decode("hello+world", StandardCharsets.UTF_8) Returns hello world, converting the plus sign back to a space.
Building a structured URI instead
new URI("https", "example.com", "/search", "q=cats & dogs", null) The URI constructor applies the correct RFC 3986 escaping rules to the path and query separately.
Verify with the URL Encoder / Decoder tool.
Encoding a form value
URLEncoder.encode("hello world", StandardCharsets.UTF_8) Returns hello+world, using + for the space per application/x-www-form-urlencoded.
- 1.
Oracle, "URLEncoder," Java SE 11 Documentation, docs.oracle.com, accessed July 2026. https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URLEncoder.html
- 2.
IETF, "Unreserved Characters," RFC 3986 Section 2.3, rfc-editor.org, January 2005. https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3
- 3.
Oracle, "URI," Java SE 11 Documentation, docs.oracle.com, accessed July 2026. https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URI.html
- 4.
WHATWG, "URL Standard," url.spec.whatwg.org, accessed July 2026. https://url.spec.whatwg.org/
- 5.
WHATWG, "application/x-www-form-urlencoded encoding algorithm," HTML Standard, html.spec.whatwg.org, accessed July 2026. https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
URLEncoder targets the application/x-www-form-urlencoded format, the same convention HTML forms use, which specifies a plus sign for spaces rather than RFC 3986's %20. This is intentional and matches the format Java's servlet APIs expect when decoding form submissions, not a bug.
You can, but it will produce incorrect results for path use, since it applies form-encoding rules like plus-for-space rather than RFC 3986 path rules. Use java.net.URI's constructor instead when building a structured URL, since it applies encoding rules appropriate to each component, including the path.
The older overload takes a charset name as a String and declares a checked UnsupportedEncodingException. The newer overload, added in Java 10, takes a Charset object directly and throws no checked exception. Prefer URLEncoder.encode(s, StandardCharsets.UTF_8) in any codebase targeting Java 10 or later.
Only letters, digits, and the four characters period, hyphen, asterisk, and underscore. This is a smaller safe set than encodeURIComponent's, which additionally preserves the tilde, exclamation mark, and parentheses. Every character outside URLEncoder's narrow set becomes a percent-escape, and space specifically becomes a plus sign.
Use URLDecoder.decode(s, StandardCharsets.UTF_8), which reverses the percent-escapes and also converts + back to a space. Pasting the same string into CapyToolkit's DECODE mode will reverse the percent-escapes but leave a literal + unchanged, since it does not assume form encoding by default.
URL Encoding in Node.js
Node.js gives you three overlapping ways to encode URL data, and they were not all designed to agree with each other. The global encodeURIComponent and encodeURI functions, inherited from the same ECMAScript specification browsers use, percent-encode a space as %201. URLSearchParams, part of the WHATWG URL Standard implementation built into Node2, instead percent-encodes a space as +, matching application/x-www-form-urlencoded conventions3, because it represents form-style query data specifically. Consequently, building a query string with URLSearchParams and then comparing it against a manually encodeURIComponent-encoded string shows spaces encoded two different ways, even though both outputs are valid for their respective contexts. The legacy querystring module, still present for backward compatibility4, percent-encodes a space as %20 rather than +, adding a third convention to keep straight when reading older Node.js codebases.
URLSearchParams and its plus-for-space behavior
URLSearchParams is the modern, WHATWG-standard way to build and parse query strings in Node.js and in browsers. Constructing one from an object or array of pairs and calling .toString() percent-encodes every key and value according to the application/x-www-form-urlencoded percent-encode set3, which specifically converts a space to + rather than %20.
Why the same value can look different elsewhere
Because URLSearchParams targets form-style encoding, its output only matches other encoders when the receiving system also treats + as a space. Copying a query string built by URLSearchParams into a decoder that assumes straight percent-encoding is one common place where this mismatch shows up: the string looks similar at a glance, but the plus sign will not be decoded as a space unless the parser knows the form-encoding convention.
Furthermore, the constructor itself interprets an incoming + in a query string as a space when parsing, which is correct for form data but can surprise you if you feed it a value containing a legitimate literal plus sign, such as a phone number or a base64 fragment, since that plus will be read back as a space. This dual behavior, encoding spaces to + and decoding + to spaces, is internally consistent but differs from encodeURIComponent5, which never touches an existing + during encoding and never converts + to a space during decoding. Keeping track of which of the two behaviors a given piece of code relies on becomes important the moment a value migrates between a component-based system and a form-based one.
The URL class and full-URL query encoding
Setting .search or .searchParams on a Node.js URL object routes through the same URLSearchParams logic internally2, so building a query through url.searchParams.set(key, value) gives you plus-for-space encoding consistent with form data. Directly assigning a raw string to url.search, however, behaves differently and percent-encodes a space as %201, since it treats the string as a query component under general URL percent-encoding rather than as form-encoded parameters.
Why this distinction matters
This means the exact same logical value can encode to a + or a %20 for its space characters depending on which property of the URL object you used to set it, a subtlety that is easy to miss when refactoring code between the two approaches. You can verify which convention your Node server is producing by pasting the raw output into CapyToolkit's ENCODE mode and comparing it against the plain encodeURIComponent row to see whether the space survives as %20, and testing the actual output your code produces, rather than assuming based on which API "feels" more standard, avoids surprises when a downstream service expects one specific space convention.
Migrating away from the legacy querystring module
Older Node.js code frequently uses the querystring module's stringify and parse functions4, which predate URLSearchParams in the Node.js standard library. querystring.escape, used internally by stringify, percent-encodes a space as %20, differing from URLSearchParams's + convention, which means migrating a codebase from querystring to URLSearchParams can silently change how spaces are represented in generated URLs.
Why the visible difference is not cosmetic
A downstream API that expects form-encoded POST bodies may accept either %20 or + for spaces, but some systems enforce one convention explicitly in validation logic or in tests that compare exact query strings. Switching the encoding style without changing the downstream contract is therefore a behavioral change, not merely a stylistic one.
Consequently, the Node.js documentation itself recommends new code use URLSearchParams rather than querystring, but a direct swap without testing can break any downstream consumer that was written to expect %20-encoded spaces specifically. Comparing the output of both functions on your actual data in CapyToolkit's ENCODE mode, alongside the plain encodeURIComponent row, is a fast way to confirm which convention a piece of legacy code is currently producing before you change it.
When to use this
Use URLSearchParams for building and parsing form-style query strings in new Node.js code, since it is the WHATWG-standard approach and handles encoding and joining together. Use plain encodeURIComponent when encoding a single value for a path segment or any context where %20, not +, is the expected space representation5. Avoid introducing new code that depends on the legacy querystring module.
Notes
URLSearchParams and the URL class are globally available in Node.js without any import, matching their browser API surface. The querystring module requires require('node:querystring') or require('querystring') and remains supported for backward compatibility, though Node.js documentation steers new code toward URLSearchParams. All three approaches use UTF-8 for encoding non-ASCII characters, so Unicode handling is consistent regardless of which one you choose.
Examples
Building a query string with URLSearchParams
new URLSearchParams({q: 'cats dogs'}).toString() Returns q=cats+dogs, using + for the space per form-encoding conventions.
Encoding a single value directly
encodeURIComponent('cats dogs') Returns cats%20dogs, using %20 for the space, differing from URLSearchParams output.
Parsing a query string back
new URLSearchParams('q=cats+dogs').get('q') Returns "cats dogs" because URLSearchParams interprets + as a space when parsing.
Verify with the URL Encoder / Decoder tool.
Building a query string with URLSearchParams
new URLSearchParams({q: 'cats dogs'}).toString() Returns q=cats+dogs, using + for the space per form-encoding conventions.
- 1.
ECMA-262, "encodeURIComponent ( uriComponent )," ECMAScript 2027 Language Specification, tc39.es, accessed July 2026. https://tc39.es/ecma262/#sec-encodeuricomponent
- 2.
WHATWG, "URL Standard," url.spec.whatwg.org, accessed July 2026. https://url.spec.whatwg.org/
- 3.
WHATWG, "application/x-www-form-urlencoded encoding algorithm," HTML Standard, html.spec.whatwg.org, accessed July 2026. https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
- 4.
Node.js, "URL," Node.js Documentation, nodejs.org, accessed July 2026. https://nodejs.org/api/url.html
- 5.
Mozilla Developer Network, "encodeURIComponent()," developer.mozilla.org, accessed July 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
URLSearchParams implements the WHATWG URL Standard's application/x-www-form-urlencoded serialization, which specifies + for spaces, matching HTML form conventions. This differs from encodeURIComponent, which always uses %20. Both are correct for their respective contexts, so check which convention the receiving system expects.
Use URLSearchParams when building a complete query string from multiple key-value pairs, since it handles both encoding and joining correctly. Use encodeURIComponent for a single value, especially one destined for a path segment, where %20 rather than + is the expected space representation.
It is not formally deprecated, but Node.js documentation recommends URLSearchParams for new code. The main practical difference is that querystring.stringify encodes spaces as %20 while URLSearchParams uses +, so migrating existing code between the two can change the output format for any consumer expecting one specific convention.
You likely parsed the value with URLSearchParams, which interprets any + in the input as a space per form-encoding rules. If the plus sign was meant literally, such as in a phone number, encode it as %2B before it reaches URLSearchParams, or parse the raw query string manually with decodeURIComponent instead.
No. Assigning a raw string to url.search percent-encodes spaces as %20, while url.searchParams.set routes through URLSearchParams and uses +. The same logical value can end up encoded differently depending on which property you use, so test the actual output before relying on either. Comparing both encodings in CapyToolkit's encoder is often the fastest way to see which convention your code is currently producing.
URL Encoding in Go
Go's standard library net/url package deliberately splits URL encoding into two functions, and the split exists because a path and a query follow different rules under RFC 39861. url.QueryEscape percent-encodes a string for safe use inside a query string, converting a space to + and escaping a plus sign to %2B, matching application/x-www-form-urlencoded conventions. url.PathEscape percent-encodes a string for safe use inside a path segment, converting a space to %20 and leaving a literal + unescaped, since a plus is not ambiguous in path context the way it is in a query. Consequently, running the same string through both functions produces genuinely different output, and the matching decoders, url.QueryUnescape and url.PathUnescape, mirror that split: only QueryUnescape converts a + back into a space during decoding.
Why Go has two escape functions instead of one
The RFC 3986 grammar treats path segments and query components as separate productions with separate rules for which characters are safe, and Go's API mirrors that structural distinction directly rather than offering one general-purpose escaper. QueryEscape2 assumes the string will sit inside a query string, where a + traditionally represents a space thanks to legacy form-encoding conventions3, so it escapes any literal + in your data to %2B to avoid ambiguity.
Why the mismatch corrupts a path segment
PathEscape assumes the string will sit inside a path segment, where no such legacy convention exists, so it leaves a literal + alone and uses %20 for a space, matching straightforward RFC 3986 percent-encoding1. Consequently, calling QueryEscape on a value you intend to place in a path produces a + for any space in that value, which a path parser reads as a literal plus character rather than a space, silently corrupting the segment.
Building query strings with url.Values
For assembling a complete query string from multiple parameters, url.Values4, a map[string][]string type, offers an Encode() method that handles both the per-value escaping and the joining with & and = in one call, using QueryEscape semantics internally for every key and value2. Because both the escaping and the joining happen inside the standard library, the two error-prone steps are handled in one operation rather than left to every caller: forgetting to encode individual values, and forgetting to place literal delimiters between them.
Handling repeated keys
Because url.Values maps each key to a slice of strings rather than a single string, repeated keys such as multiple tag= parameters are represented naturally: call Add multiple times with the same key, and Encode() emits each value with its own key=value pair in the output, sorted alphabetically by key.
A second practical consequence is that tests built against hand-joined strings often fail when the same logic is replaced with url.Values.Encode(). The output is semantically equivalent but may list parameters in a different order, so assertions should compare decoded key-value sets rather than raw query strings unless alphabetical ordering is part of the contract. This built-in sorting is worth knowing if you are comparing generated query strings against a fixed expected string in a test, since the order of parameters in the output is deterministic but not necessarily the insertion order. Relying on Encode() instead of hand-building the string also means any future addition of a parameter automatically gets the same escaping treatment, with no risk of forgetting a call to QueryEscape.
Choosing the right unescape function
Decoding correctly requires matching the unescape function to whichever escape function produced the string in the first place, because the two unescape functions differ on exactly one behavior that changes the meaning of the output. url.QueryUnescape reverses percent-escapes and additionally converts any + into a space, which is correct for a string that came from a query string or from url.Values.Encode().
Why the plus-sign difference matters in practice
Choosing the wrong unescape function does not merely leave a character unchanged; it actively changes the meaning of your data. A phone number such as +1-555-2368 that arrives from a path segment will have its leading plus turned into a space if decoded with QueryUnescape, turning a valid identifier into nonsense. Verifying the round trip in the CapyToolkit decoder before committing to one decoder in your Go code catches this class of bug quickly.
url.PathUnescape reverses percent-escapes but leaves any + as a literal plus character, which is correct for a string that came from a path segment via PathEscape. Using QueryUnescape on a path-derived string converts a legitimate literal plus into a space incorrectly, while using PathUnescape on a query-derived string leaves a form-encoded space3 as a stray plus sign in your decoded output. The Go documentation explicitly states that PathUnescape is identical to QueryUnescape except for this one difference in plus-sign handling4.
When to use this
Use url.PathEscape when encoding a value destined for a URL path segment, and url.QueryEscape or url.Values.Encode() when encoding a value destined for a query string. Matching the unescape function to the escape function that produced the string, PathUnescape for paths and QueryUnescape for queries, avoids the plus-sign-versus-space mismatch that is the most common bug in Go URL-handling code.
Notes
Both QueryEscape and PathEscape, along with their unescape counterparts, live in the net/url package in the Go standard library, requiring no external module. url.Values.Encode() always sorts keys alphabetically in its output, which is useful to know when writing tests that compare against a literal expected string. The net/url package operates on UTF-8 encoded Go strings by default, consistent with Go's native string encoding.
Examples
Encoding a query value
url.QueryEscape("cats & dogs") Returns cats+%26+dogs, using + for the space and escaping the literal ampersand.
Encoding a path segment
url.PathEscape("cats & dogs") Returns cats%20%26%20dogs, using %20 for the space, matching RFC 3986 path rules.
Building a query string with url.Values
v := url.Values{}; v.Add("q", "cats & dogs"); v.Encode() Returns q=cats+%26+dogs, encoding and joining in one call.
Verify with the URL Encoder / Decoder tool.
Encoding a query value
url.QueryEscape("cats & dogs") Returns cats+%26+dogs, using + for the space and escaping the literal ampersand.
- 1.
IETF, "Unreserved Characters," RFC 3986 Section 2.3, rfc-editor.org, January 2005. https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3
- 2.
Go, "url," Package Documentation, pkg.go.dev, accessed July 2026. https://pkg.go.dev/net/url
- 3.
WHATWG, "application/x-www-form-urlencoded encoding algorithm," HTML Standard, html.spec.whatwg.org, accessed July 2026. https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
- 4.
Go, "url.Values," Package Documentation, pkg.go.dev, accessed July 2026. https://pkg.go.dev/net/url#Values
QueryEscape encodes a space as + and escapes a literal + to %2B, matching form-encoding conventions for query strings. PathEscape encodes a space as %20 and leaves a literal + unescaped, matching plain RFC 3986 rules for path segments. Use each for the context its name describes.
You used QueryEscape, which follows application/x-www-form-urlencoded conventions and represents a space as +. If you need %20 instead, such as for a path segment, use PathEscape instead. Both are valid encodings; the correct one depends on whether the value is going into a path or a query.
Use url.Values, a map[string][]string, and call Add(key, value) for each parameter, then call .Encode() to get the fully assembled, correctly escaped query string with keys sorted alphabetically. This avoids manually joining QueryEscape output with & and =, which risks encoding-order mistakes.
Match it to the escape function used originally. Use url.QueryUnescape for strings produced by QueryEscape or url.Values.Encode(), since it converts + back to a space. Use url.PathUnescape for strings produced by PathEscape, since it leaves a literal + unchanged rather than converting it to a space. CapyToolkit's DECODE mode can show you the expected result before you commit to one decoder in your Go code.
Yes, QueryEscape escapes a literal + to %2B precisely because + already has a reserved meaning, representing a space, within query strings. This prevents ambiguity: a decoder can always tell a real plus sign, which arrives as %2B, from an encoded space, which arrives as a bare +.
URL Encoding with curl
curl does not percent-encode data for you unless you explicitly ask it to,1 which surprises anyone who assumes -d handles encoding automatically. Passing raw text with -d or --data sends it exactly as typed, so any space, ampersand, or special character in the value goes onto the wire unencoded and can corrupt the request or get silently mangled by the server.
--data-urlencode solves this by URL-encoding the value before curl sends it, and it accepts four distinct forms: plain content to encode a whole string, =content to encode content while dropping a leading equals sign, name=content to encode only the content portion while leaving the name as-is, and name@filename (or @filename alone) to read and encode the contents of a file. Combined with -G, which redirects data onto the URL as a query string instead of a POST body, --data-urlencode becomes the standard way to build a correctly encoded GET request from the command line.
Why raw -d does not encode your data
curl treats -d as a literal pass-through by design, sending exactly the bytes you provide as the request body without inspecting or transforming them. This matches curl's general philosophy of not guessing at your intent, but it means a space, an ampersand, or a plus sign in a -d value travels unencoded unless you have manually escaped it in your shell command.
How the ampersand breaks a raw request
Consequently, a command like curl -d "name=Fish & Chips" https://api.example.com sends a literal, unencoded ampersand in the body, which most servers will misparse as two separate form fields rather than one value containing an ampersand. The fix is not to hand-escape the string yourself in the shell, which is fragile and easy to get wrong, but to let --data-urlencode perform RFC 3986 style percent-encoding on the value before curl transmits it.
The four syntax forms of --data-urlencode
Each of the four forms --data-urlencode accepts1 targets a slightly different situation, and picking the wrong one skews what curl actually sends. The bare content form treats the entire option value as data, making it unsuitable when you also need to preserve a parameter name. The name=content form leaves the name literal while encoding the value, which is the safest default when you know the exact parameter name. The @filename form reads and encodes a file's contents, which is the right choice when your payload is too long or too awkward to escape on the command line. Choosing the correct form avoids surprising results from curl's parsing rules.
Matching the form to the value
Use bare content when the whole string is a value with no = or @ characters of its own, since either character would otherwise be interpreted as part of the option's special syntax. Use name=content when you have a known parameter name that should stay literal and only the content needs encoding, which is the most common form for building form-style POST bodies. Use @filename when the data to encode lives in a file rather than inline in the command, useful for large payloads or values containing characters that are awkward to escape in a shell. Each of these percent-encodes its content portion using the same rules regardless of which form you choose.
The =content form deserves special mention because it behaves like bare content but silently strips any leading equals sign from your data before encoding it, which is useful when the payload begins with = but you want the request to carry no explicit parameter name. Every form ultimately produces the same RFC 3986 percent-encoded output,2 so none of the forms affects which characters get percent-encoded. That decision is made after curl parses the option syntax.
Combining -G with --data-urlencode for GET requests
By itself, --data-urlencode still sends a POST request, encoding the body but not changing the HTTP method. Adding -G (or its long form --get)3 tells curl to take everything specified with -d, --data-binary, or --data-urlencode and append it to the URL as a query string instead, issuing a GET request rather than a POST.
Consequently, curl -G --data-urlencode "q=cats & dogs" https://api.example.com/search correctly builds the URL https://api.example.com/search?q=cats%20%26%20dogs, encoding the space and ampersand in the value while leaving the base URL and the ? and = structural characters intact. This is the standard curl idiom for testing a GET endpoint with a query value that contains characters a shell or a raw URL would otherwise mishandle, and it avoids the common mistake of manually building an encoded query string and appending it to the URL yourself.
What -G does not change
-G only redirects the data payload to the URL and does not alter how the value was encoded. The %20 sequences produced by --data-urlencode remain %203 in the query string rather than being converted to +. This matters when the target API expects one convention over the other, which is common enough that you should confirm the expected encoding before testing a live endpoint.
When to use this
Use --data-urlencode any time a curl command sends a value that might contain spaces, ampersands, or other characters with special meaning in a URL or in form data. Add -G when you need that value to become part of a GET request's query string rather than a POST body. This combination is a reliable way to reproduce, from the command line, exactly what a browser would send when submitting a form.
Notes
--data-urlencode is built into curl itself and requires no separate installation or flag beyond what a standard curl binary provides. The encoding it performs matches RFC 3986 percent-encoding, using %20 for a space rather than the form-style +. Multiple --data-urlencode flags can be combined in one command to encode several parameters at once, each joined automatically with & in the resulting body or query string.
Examples
Encoding a POST body value
curl -d "" --data-urlencode "name=Fish & Chips" https://api.example.com
Sends name=Fish%20%26%20Chips as the encoded body, keeping the ampersand as literal data.
Building a GET query string
curl -G --data-urlencode "q=cats & dogs" https://api.example.com/search
Requests https://api.example.com/search?q=cats%20%26%20dogs with the value correctly encoded.
Encoding file contents
curl -G --data-urlencode "[email protected]" https://api.example.com
Reads notes.txt, URL-encodes its contents, and appends it as the body parameter in the query string.
Verify with the URL Encoder / Decoder tool.
Encoding a POST body value
curl -d "" --data-urlencode "name=Fish & Chips" https://api.example.com
Sends name=Fish%20%26%20Chips as the encoded body, keeping the ampersand as literal data.
- 1.
curl, "--data-urlencode," curl.se, accessed July 2026. https://curl.se/docs/manpage.html
- 2.
IETF, "RFC 3986," rfc-editor.org, accessed July 2026. https://www.rfc-editor.org/rfc/rfc3986.html
- 3.
curl, "URL Syntax," curl.se, accessed July 2026. https://curl.se/docs/url-syntax.html
No. Plain -d or --data sends your value exactly as typed, with no encoding applied. Use --data-urlencode instead, which percent-encodes the value before sending it. This matters whenever your data contains spaces, ampersands, or other characters that have special meaning in a URL or form body.
Combine -G with --data-urlencode. The -G flag tells curl to append the data as a query string and issue a GET request instead of a POST. For example, curl -G --data-urlencode "q=cats & dogs" https://api.example.com/search correctly encodes the space and ampersand in the query value.
Plain content encodes the whole string. =content encodes the content while dropping a leading equals sign. name=content encodes only the content, leaving the name literal. name@filename reads a file, encodes its contents, and pairs it with the name. Choose the form based on whether you have a fixed parameter name and where your data originates.
A raw -d value containing an unencoded & is read by the server as two separate parameters, splitting your intended value. Switch to --data-urlencode, which percent-encodes the ampersand to %26, keeping the value intact. Test the exact string in CapyToolkit first if you want to preview the encoded output before running the command.
It uses %20, following RFC 3986 percent-encoding rather than the form-style + convention. If the API you are calling specifically expects + for spaces in the body, you may need to encode the value yourself before passing it to curl, since --data-urlencode does not offer a +-for-space mode.