URL Encoding the Space Character — + vs %20 Explained Once and For All
If you have debugged a query string issue, you have seen this:
hello world → hello%20world
But sometimes the same input becomes:
hello world → hello+world
And that is where the confusion begins.
Which one is correct? Are they interchangeable? Why does JavaScript use %20 but browsers send +? Why does your backend handle one but not the other?
This article settles the + vs %20 debate with concrete examples, language-specific behavior, and production debugging advice.
Where %20 Comes From
%20 is the standard percent-encoding of the space character.
The ASCII value for space is 32 decimal, which is 20 in hexadecimal. URL percent-encoding converts any byte to %XX format, where XX is the hex value.
Space → ASCII 32 → 0x20 → %20
This encoding is defined in RFC 3986 (Uniform Resource Identifier) and is the universal standard for encoding special characters in URLs.
Systems that use %20 for spaces include:
- RFC 3986 compliant URL parsers
encodeURI()andencodeURIComponent()in JavaScripturllib.parse.quote()in PythonURLEncoder.encode()with explicit space handling in Javarawurlencode()in PHP- Most modern web frameworks and API gateways
Where + Comes From
The + convention predates modern URL standards. It comes from the application/x-www-form-urlencoded content type used by HTML forms.
When the early web needed to submit form data, the specification chose + to represent spaces — likely because it was shorter than %20 and already visually familiar.
<form action="/search" method="GET">
<input name="q" value="hello world">
<input type="submit">
</form>
Submitting this form produces:
/search?q=hello+world
The browser converts the space to +, not %20.
Systems that use + for spaces include:
- HTML form submissions (GET and POST)
application/x-www-form-urlencodedparsingURLSearchParams.toString()in JavaScripturllib.parse.urlencode()in Pythonurlencode()in PHP- Older web frameworks
The Core Distinction
| Encoding | Standard | Used By |
|---|---|---|
%20 | RFC 3986 (URL standard) | encodeURIComponent, quote, rawurlencode |
+ | application/x-www-form-urlencoded | HTML forms, URLSearchParams, urlencode |
Both represent a space character. But they are not universally interchangeable.
When + Fails
The + convention breaks down in contexts where a literal + is valid data.
Example: C++ Search
const query = "C++";
const params = new URLSearchParams({ q: query });
console.log(params.toString());
Output:
q=C++
A naive server may decode this as "C" (dropping everything after the +) or treat the + as a space and return results for "C".
The correct encoding for a literal plus sign is %2B.
console.log(encodeURIComponent("C++")); // C%2B%2B
Where This Hurts
- Email addresses containing
+(Gmail's plus addressing) - Version strings like
C++or1.0+beta - Mathematical expressions
- Phone numbers with
+prefix - Base64 encoded data in URLs
Not all frameworks decode + as a space in path segments. Some only do it in query strings. Some never do it at all.
Language-by-Language Behavior
JavaScript
encodeURIComponent("hello world"); // hello%20world
new URLSearchParams({q: "hello world"}).toString(); // q=hello+world
new URL("https://example.com?q=hello+world").searchParams.get("q"); // "hello world"
JavaScript decodes both %20 and + as spaces when parsing search params. But encodeURIComponent only produces %20.
Python
from urllib.parse import quote, urlencode
quote("hello world") # hello%20world
urlencode({"q": "hello world"}) # q=hello+world
Python's quote() gives %20. Its urlencode() gives +. This creates cross-module inconsistency.
Java
import java.net.URLEncoder;
import java.net.URI;
URLEncoder.encode("hello world", "UTF-8"); // hello+world
new URI("http", null, "host", -1, "/path", "q=hello world", null).toString();
Java's URLEncoder.encode() produces + by default — it follows form-urlencoded rules. The URI class follows RFC 3986.
URLEncoder.encode("hello world", "UTF-8").replace("+", "%20");
This workaround is common in Java codebases.
C#
using System.Web;
using System.Net;
HttpUtility.UrlEncode("hello world"); // hello%2bworld (encodes + as %2b, space as +)
WebUtility.UrlEncode("hello world"); // hello%20world
The two built-in C# encoding methods behave differently for spaces. See the C# URL encoding guide for details.
PHP
urlencode("hello world"); // hello+world
rawurlencode("hello world"); // hello%20world
PHP explicitly provides two functions for the two conventions. urlencode() follows form rules, rawurlencode() follows URL rules.
Curl and Bash
curl "https://example.com/search?q=hello+world"
The shell does not interpret + specially. The server receives the literal +. Whether it decodes it as a space depends on server-side logic.
How Different Frameworks Decode Spaces
| Framework | + Decoded as Space? | %20 Decoded as Space? |
|---|---|---|
| Express.js (query) | Yes | Yes |
| Express.js (params) | No | Yes |
| Django | Yes | Yes |
| ASP.NET Core | Yes (configurable) | Yes |
| Spring Boot | Yes (depends on config) | Yes |
| Flask | Yes | Yes |
| Ruby on Rails | Yes | Yes |
| Nginx $arg_* | No | Yes |
The inconsistency across path segments vs query strings is a common source of bugs.
A Real Bug: OAuth with Signed URLs
Consider a signed URL system that generates a token based on the exact request path:
Signing input: /api/user?name=John+Doe
The backend signs this path. The frontend sends a request. If the frontend encodes the space as %20:
Request path: /api/user?name=John%20Doe
The signature validation fails because the signed input and request path do not match — one has +, the other has %20.
This is not a hypothetical edge case. It affects:
- CloudFront signed URLs
- OAuth state parameters
- Payment gateway webhooks
- API request signing
- CDN token authentication
Best Practices for Space Encoding
Use %20 for Path Segments
/products/hello%20world # correct
/products/hello+world # incorrect — + is literal here
Path segments should always use %20 for spaces. Most web servers and frameworks do not decode + as a space in paths.
Use %20 for Redirect URLs
OAuth providers, payment gateways, and SSO systems expect %20 in redirect URIs. Using + breaks signature validation.
Expect + in Form Data
Server-side form parsers expect + for spaces. This is part of the application/x-www-form-urlencoded spec.
Encode Literal Plus Signs
If your data contains a literal +, always encode it as %2B:
encodeURIComponent("C++"); // C%2B%2B
Normalize Early
If your system receives data that may contain either format, normalize spaces early:
# Python example: normalize incoming query params
query = query.replace("+", " ") # form-encoded spaces
query = unquote(query) # percent-encoded spaces
Testing for Space Encoding Bugs
Add these to your test suite:
// JavaScript test cases
const testCases = [
"hello world",
"C++",
"john+doe@example.com",
"path/to/resource",
"50% off",
"東京",
];
testCases.forEach(value => {
it(`encodes "${value}" safely`, () => {
const encoded = encodeURIComponent(value);
const decoded = decodeURIComponent(encoded);
expect(decoded).toBe(value);
});
});
Round-trip tests catch encoding mismatches early.
Debugging Space Encoding Issues
1. Inspect Raw Network Traffic
Do not rely on browser DevTools formatted view or Postman's parameter display. Use the raw request view to see the actual bytes sent.
2. Log the Encoded URL Server-Side
// Express.js
app.use((req, res, next) => {
console.log("Raw URL:", req.originalUrl);
next();
});
3. Try Both Formats
If a request fails, manually test with both + and %20:
curl "https://api.example.com/search?q=hello+world"
curl "https://api.example.com/search?q=hello%20world"
If one works and the other does not, you have found the issue.
4. Check Framework Space Handling
Research how your specific framework handles + in query strings. Some frameworks have configuration options to toggle this behavior.
Related Resources
If you are dealing with encoding mismatches across language boundaries, these guides provide language-specific details:
-
How to Encode a URL in JavaScript JavaScript URL Encoding Guide
-
How to URL Encode Data for curl curl URL Encoding
-
URL Encoding in C# C# URL Encoding Guide
-
URL Encoding Explained with Real API Examples URL Encoding with Real API Examples
FAQ
Why do spaces become %20 in some places and + in others?
%20 is standard RFC 3986 URL encoding. + comes from the older application/x-www-form-urlencoded form standard. Different systems follow different specifications.
Are %20 and + interchangeable?
Not always. They are both decoded as spaces in query strings by most frameworks, but + is NOT decoded as a space in path segments or in some signature-validation contexts.
How do I encode a literal plus sign?
Use %2B. For example, encodeURIComponent("C++") produces C%2B%2B.
Why does URLSearchParams use +?
URLSearchParams follows the application/x-www-form-urlencoded specification, which uses + for spaces internally.
Should my API accept both + and %20?
Ideally yes. If you control the API, normalize incoming query strings by decoding + to space and then decoding percent-encoding. If you consume an API, test which format it expects.
Does the + vs %20 issue affect SEO?
Yes. Search engines and crawlers handle both formats, but inconsistencies can cause duplicate content issues if the same page is accessible via both + and %20 variants.
How do different programming languages handle spaces in encoding?
JavaScript's encodeURIComponent uses %20. Python's quote uses %20 but urlencode uses +. Java's URLEncoder uses +. PHP's urlencode uses + but rawurlencode uses %20.
Final Thoughts
The %20 vs + distinction is small but consequential. It looks like a formatting detail until your signed URL fails, your OAuth callback breaks, or your payment webhook silently drops data.
Remember the rule: %20 is the URL standard; + is the form standard. Use %20 for APIs, redirects, paths, and signed URLs. Expect + in form submissions and handle it accordingly.
If you need to quickly check how a specific value gets encoded, the URL Encoder/Decoder tool lets you test both formats side by side.