Safeguard
AppSec

URL Encoding and Decoding in Java: URLEncoder and URLDecoder

How URLEncoder.encode in Java actually behaves, why it turns spaces into plus signs, the Charset overload you should be using, and where hand-rolled encoding turns into an injection bug.

Yukti Singhal
Platform Engineer
6 min read

URLEncoder.encode in Java converts a string into the application/x-www-form-urlencoded format — it percent-encodes reserved characters using the charset you pass, and, notoriously, turns spaces into + rather than %20. That last clause is the source of most real-world encoding bugs, because form encoding and generic URL encoding are similar but not identical, and URLEncoder only ever does the former. Used per-component with an explicit UTF-8 charset it is exactly the right tool; used on a whole URL, or with the deprecated no-charset overload, it produces links that are subtly broken or, worse, injectable.

Here is how the pieces fit together, what changed in modern JDKs, and the failure modes worth a code-review checklist entry.

The correct modern call

Since Java 10 there is an overload that takes a Charset directly, and it is the one to use:

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

String q = URLEncoder.encode("cafe & crème brûlée", StandardCharsets.UTF_8);
// -> cafe+%26+cr%C3%A8me+br%C3%BBl%C3%A9e

Three variants exist and only one belongs in new code:

  • encode(String) — deprecated since Java 1.4; uses the platform default charset, which historically differed across machines. On very old runtimes this was a genuine works-on-my-laptop generator (note that recent JDKs made UTF-8 the default charset, which blunts but does not excuse it).
  • encode(String, String charsetName) — takes the charset as a string and throws checked UnsupportedEncodingException, forcing pointless try-catch ceremony.
  • encode(String, Charset) — the Java 10+ form. No checked exception, no typo-prone string.

The decoding mirror image is URLDecoder.decode(String, StandardCharsets.UTF_8), which reverses percent-escapes and converts + back into a space.

Why spaces become plus signs

URLEncoder predates clean terminology: despite the class name, it implements HTML form encoding, where a space in a submitted field is transmitted as +. In the path portion of a URL, however, + is just a literal plus character — only %20 means space there. Encode a filename into a path with URLEncoder and my report.pdf becomes my+report.pdf, which a correct server will treat as a file whose name contains a plus sign.

The standard fix when you need %20 semantics is a targeted replace after encoding:

String pathSegment = URLEncoder.encode(name, StandardCharsets.UTF_8)
        .replace("+", "%20");

That two-line idiom is how most production Java code performs a general url encode of a string, and it is fine — just keep it in one utility method instead of scattered copies, because the day someone forgets the .replace is the day report links with spaces stop resolving.

Encode components, never whole URLs

The most common misuse of the java URLEncoder is feeding it a complete URL:

// WRONG: destroys the URL's own structure
URLEncoder.encode("https://api.example.com/v1/search?q=a&b", StandardCharsets.UTF_8);
// -> https%3A%2F%2Fapi.example.com%2Fv1%2Fsearch%3Fq%3Da%26b

Every delimiter that gives the URL meaning — ://, /, ?, &, = — gets escaped along with the data. Encoding is a per-component operation: build the skeleton yourself and encode only the untrusted values you interpolate into it.

String url = "https://api.example.com/v1/search?q="
        + URLEncoder.encode(userQuery, StandardCharsets.UTF_8)
        + "&page=" + page;

For anything beyond a query parameter or two, let a builder own the composition. java.net.URI's multi-argument constructors encode each component with the right rules for that component, and every mainstream HTTP client (Spring's UriComponentsBuilder, OkHttp's HttpUrl.Builder) does the same with a friendlier API. The rule of thumb: if your code concatenates strings into a URL, each concatenated variable should pass through exactly one encoding step appropriate to where it lands.

URL decode in Java: the double-decoding trap

Decoding looks trivial and usually is:

String value = URLDecoder.decode(raw, StandardCharsets.UTF_8);

The trap is decoding something your framework already decoded. Servlet containers hand you request.getParameter values fully decoded; running them through a url decode in Java a second time means an attacker can smuggle characters past your validation by encoding them twice. %2527 survives the container's decode as %27, passes a naive quote filter, and then your redundant decode turns it into a live single quote right before it reaches a query. Double decoding is a recurring root cause in path traversal and injection advisories across many ecosystems, which is why filter-then-decode ordering shows up in code review checklists.

Two rules keep you safe:

  1. Decode exactly once, at the boundary, and know which layer does it.
  2. Validate and canonicalize after the final decode, never before.

Where encoding bugs become security bugs

Encoding failures cluster into a few exploitable patterns:

  • Open-redirect and SSRF assists. Un-encoded user input concatenated into a redirect or callback URL lets attackers inject extra query parameters or fragments (&next=//evil.example), steering flows that assumed a fixed structure.
  • Header and log injection. A raw CR/LF that should have been percent-encoded splits a Location header or forges log lines.
  • Filter bypass via mixed encodings. Validation applied pre-decode, or applied to + but not %20 (or vice versa), gives two spellings for every payload character.

These are exactly the classes of issue that show up when you exercise a running application with hostile inputs rather than reading the code — a DAST scan that mutates parameter encodings will flag a double-decode or redirect-injection path in minutes. And when the bug is not in your code but in a URL-handling library you depend on, dependency-level visibility from an SCA tool tells you whether the vulnerable version is actually on your classpath.

A compact reference

// Query/form value (spaces as +)
URLEncoder.encode(s, StandardCharsets.UTF_8);

// Path segment (spaces as %20)
URLEncoder.encode(s, StandardCharsets.UTF_8).replace("+", "%20");

// Decode once, UTF-8, at the boundary
URLDecoder.decode(s, StandardCharsets.UTF_8);

// Structured building — prefer this over concatenation
URI uri = new URI("https", "api.example.com", "/v1/search", "q=" + q, null);

If you remember nothing else: encode per component, always name the charset, spaces mean + only in form data, and decode exactly once.

FAQ

What does URLEncoder.encode actually do?

It converts a string to application/x-www-form-urlencoded form: unreserved characters (letters, digits, .-*_) pass through, spaces become +, and everything else becomes percent-escaped bytes in the charset you supply. It is designed for form fields and query values, not whole URLs.

Why does URLEncoder use + instead of %20?

Because it implements HTML form encoding, where + is the defined representation of a space. In URL path segments only %20 means space, so append .replace("+", "%20") when encoding path components.

Which encode overload should I use?

URLEncoder.encode(String, Charset) with StandardCharsets.UTF_8, available since Java 10. The single-argument overload is deprecated and the string-charset overload forces you to handle a checked exception for a typo that cannot happen with the Charset constant.

Is it safe to call URLDecoder on request parameters?

Usually not, because your web framework already decoded them — decoding again enables double-encoding bypasses of your validation. Decode exactly once at the boundary, and run validation after the final decode.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.