skip to content

How does encoding interact with URI template expansion in UriComponentsBuilder, and why does the order matter?

level: seniorimportance: should knowfreq 28%

answer

  1. template literals vs variable values
  2. encode() on builder = TEMPLATE_AND_VALUES
  3. expand-then-encode leaves reserved '&' ambiguous
  4. RestTemplate/WebClient default strict value encoding
  5. build(true) = already-encoded, don't re-encode

basics

~20 s

You must encode after (or as part of) expansion, because the values you substitute may contain reserved characters like & or /. The safest approach is UriComponentsBuilder.encode() with EncodingMode.TEMPLATE_AND_VALUES, which encodes the template literals and then strictly encodes each expanded variable.

solid answer

~50 s

Encoding and expansion order is subtle. Two strategies exist. The classic 'encode after expand': build().expand(values).encode() first substitutes variables, then percent-encodes the whole resulting URI — but this can't tell whether a character came from your fixed template or from a variable, so a value containing a reserved char like '&' or '/' may be interpreted as structure. The preferred modern approach is UriComponentsBuilder.encode() (EncodingMode.TEMPLATE_AND_VALUES): it encodes only the illegal characters in the fixed template first, then, when you expand, it strictly encodes each variable value including reserved characters. So a query value 'a&b' becomes 'a%26b' rather than splitting into two params. In short: call encode() on the builder before expanding, or use buildAndExpand which does not encode variables unless the builder was set to encode. RestTemplate and WebClient default to strict value encoding for exactly this reason.

code

java · 18 lines
java
import org.springframework.web.util.UriComponentsBuilder;

String q = "a&b c"; // reserved '&' plus a space

// PREFERRED: encode template first, then strictly encode the value on expand
String good = UriComponentsBuilder.fromUriString("https://api/search?q={q}")
        .encode()                 // EncodingMode.TEMPLATE_AND_VALUES
        .buildAndExpand(q)
        .toUriString();
// -> https://api/search?q=a%26b%20c   (one param, value preserved)

// LEGACY / WRONG for reserved chars: expand then encode
String bad = UriComponentsBuilder.fromUriString("https://api/search?q={q}")
        .build()
        .expand(q)
        .encode()
        .toUriString();
// -> https://api/search?q=a&b%20c   ('&' stays a delimiter -> ambiguous)

go deeper

for a junior

Know that values are percent-encoded and you shouldn't pre-concatenate encoded strings yourself.

for a middle

Know buildAndExpand encodes variables and that you generally let Spring/RestTemplate handle encoding rather than doing it manually.

for a senior

Explain TEMPLATE_AND_VALUES vs expand-then-encode and why reserved chars in a value need strict per-value encoding.

for a principal

Set org-wide conventions (EncodingMode, when build(true) is allowed, avoiding double-encoding) and reason about encoding as an injection-safety boundary.

## Why this is tricky A URI has structure — `/`, `?`, `&`, `=`, `#`, `:` are **reserved delimiters**. When you expand a template variable, the *value* might itself contain those characters. Should `queryParam("q", "a&b")` produce `?q=a&b` (two params — wrong) or `?q=a%26b` (one param with value `a&b` — right)? Encoding order decides. ## The two components: template literals vs. variable values - **Template literals** = the fixed parts you wrote (`/users`, `?q=`). These may contain characters that are illegal for their URI component and need encoding, but their reserved delimiters are *intentional structure*. - **Variable values** = whatever fills `{q}`. Here every reserved character should be treated as data and percent-encoded. Mixing these up is the whole problem. ## Strategy A — encode AFTER expand (legacy) ```java UriComponents uc = UriComponentsBuilder.fromUriString("/s?q={q}") .build() // not encoded .expand("a&b") // -> /s?q=a&b (already ambiguous!) .encode(); // encodes illegal chars, but '&' is a legal delimiter -> stays // result: /s?q=a&b (WRONG: looks like two params) ``` After expansion the builder can no longer distinguish the injected `&` from a real delimiter, so `.encode()` leaves it. Result is ambiguous/incorrect. ## Strategy B — encode the TEMPLATE, then strictly encode values (preferred) Call `.encode()` on the **builder** (before expanding). Internally this sets `EncodingMode.TEMPLATE_AND_VALUES`: ```java UriComponents uc = UriComponentsBuilder.fromUriString("/s?q={q}") .encode() // encode template literals now (mode = TEMPLATE_AND_VALUES) .buildAndExpand("a&b"); // each value strictly encoded on expansion // result: /s?q=a%26b (CORRECT: one param, value a&b) ``` Here the template's own `?` and `=` are preserved as structure, but the value `a&b` is strictly encoded to `a%26b`. ## EncodingMode enum (`UriComponentsBuilder.EncodingMode`) - `TEMPLATE_AND_VALUES` — encode illegal chars in the template first, then strictly encode expanded variable values. Set by calling `builder.encode()`. **Recommended.** - `VALUES_ONLY` — don't encode the template; strictly encode variable values at expansion via `UriUtils`/`DefaultUriBuilderFactory`. Assumes the template is already valid. - `URI_COMPONENT` — encode after expanding as a whole (the legacy `UriComponents.encode()` behavior). - `NONE` — no encoding. `DefaultUriBuilderFactory` (used by `RestTemplate` and `WebClient`) defaults to `TEMPLATE_AND_VALUES`, which is why passing a value with reserved chars to those clients Just Works. ## build(true) — 'already encoded' If your components are *already* percent-encoded and you call `build(true)`, Spring will not encode again — it just parses. Passing a not-yet-encoded string here leaves illegal characters in place. Use only when you truly hold pre-encoded input. ## Practical rules of thumb 1. Prefer `builder.encode().buildAndExpand(values)` (or let RestTemplate/WebClient do it) so values are strictly encoded. 2. Never `expand()` an unencoded value and hope a later `encode()` fixes reserved chars — it won't. 3. Only use `build(true)` when the input is genuinely pre-encoded. 4. Remember encoding rules differ per component: a `+` is a space in a query but literal in a path; `/` is a delimiter in a path but data in a query value. ## Edge cases / gotchas - Non-ASCII (e.g. `naïve`) -> UTF-8 percent-encoding (`na%C3%AFve`). - A `{` in a literal that you did NOT intend as a template variable will be treated as a variable and can throw on expansion — escape or avoid. - Double encoding: applying `.encode()` twice turns `%20` into `%2520`. Encode exactly once.

  • What does build(true) mean and when is it dangerous?
    It tells the builder the supplied components are already percent-encoded, so it skips encoding and just parses. Dangerous when the input isn't actually encoded — illegal/reserved chars pass through unescaped, producing malformed or injectable URIs.
  • Why do RestTemplate and WebClient correctly encode a query value containing '&' by default?
    Their DefaultUriBuilderFactory defaults to EncodingMode.TEMPLATE_AND_VALUES, which strictly encodes each expanded URI variable, turning '&' into %26 so it can't be mistaken for a delimiter.

saying these in an interview costs you the question

  • Claiming .expand(value).encode() safely handles values with reserved characters like '&' — it does not.
  • Thinking encoding the whole final URI string is equivalent to strict per-value encoding.
  • Encoding twice (%20 -> %2520) by calling encode() and also relying on the client to encode.
  • Using build(true) on input that isn't actually pre-encoded.

context