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() and encodeURIComponent() in JavaScript
  • urllib.parse.quote() in Python
  • URLEncoder.encode() with explicit space handling in Java
  • rawurlencode() 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-urlencoded parsing
  • URLSearchParams.toString() in JavaScript
  • urllib.parse.urlencode() in Python
  • urlencode() in PHP
  • Older web frameworks

The Core Distinction

EncodingStandardUsed By
%20RFC 3986 (URL standard)encodeURIComponent, quote, rawurlencode
+application/x-www-form-urlencodedHTML 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++ or 1.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)YesYes
Express.js (params)NoYes
DjangoYesYes
ASP.NET CoreYes (configurable)Yes
Spring BootYes (depends on config)Yes
FlaskYesYes
Ruby on RailsYesYes
Nginx $arg_*NoYes

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:


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.