How does WebMvcLinkBuilder resolve the base URI and controller-relative paths, and what request context does it require?
answer
- class mapping + method mapping + path vars
- base from current request (ServletUriComponentsBuilder)
- needs request-bound thread — fails in @Scheduled/@Async/plain test
- reverse proxy → ForwardedHeaderFilter + X-Forwarded-*
- .slash(), .toUri(), WebFluxLinkBuilder for reactive
basics
~20 slinkTo reads the controller's @RequestMapping to form the path and prepends the current request's base URL (scheme, host, port, context path) obtained from ServletUriComponentsBuilder. It needs an active request thread; there is no request-independent default.
solid answer
~40 sWebMvcLinkBuilder builds a path from the controller class mapping plus the invoked method's mapping (linkTo(ControllerClass.class) uses just the class-level @RequestMapping; linkTo(methodOn(...).method()) adds the method mapping and substitutes @PathVariable/@RequestParam values). It then resolves the absolute base URI — scheme, host, port, and servlet context path — from the current request via ServletUriComponentsBuilder.fromCurrentRequest/RequestContextHolder. Because of that, calls must run on a request-bound thread; invoking them from a scheduled job, a @Async thread that lost the context, or a plain unit test without a mock request throws (no current request attributes). Behind a reverse proxy, correct host/scheme comes from X-Forwarded-* headers, which Spring only trusts when ForwardedHeaderFilter (or equivalent) is registered. You can also chain .slash(...) to append segments and .toUri() to get a raw URI without a rel.
code
java · 13 lines// Path comes from mappings; base comes from the live request.
Link self = linkTo(methodOn(ItemController.class).getItem(1L)).withSelfRel();
// -> https://api.example.com/ctx/items/1 (public host, IF proxy headers trusted)
// Class-relative link (no method): uses only class-level @RequestMapping
Link list = linkTo(ItemController.class).slash("active").withRel("active");
// -> .../items/active
// Plain URI without a rel:
java.net.URI uri = linkTo(methodOn(ItemController.class).getItem(1L)).toUri();
// Boot config to honor a reverse proxy's forwarded headers:
// application.yml -> server.forward-headers-strategy: frameworkgo deeper
Know that the href is absolute and built from mappings plus the current request.
Explain class-only vs. method-level linkTo and path-variable substitution.
Detail request-context dependence and the reverse-proxy/ForwardedHeaderFilter requirement.
Weigh operational concerns: proxy trust boundaries, header spoofing risk, and context propagation across async boundaries.
A link href produced by Spring HATEOAS has two parts: a **path** derived from your controller mappings, and an **absolute base** derived from the incoming request. Understanding both explains why links are correct in prod and why they sometimes fail outside a web request. **Path resolution (controller-relative):** - `linkTo(SomeController.class)` uses only the **class-level** `@RequestMapping` value, e.g. `/items`. - `linkTo(methodOn(SomeController.class).getItem(id))` combines the class mapping with the **method** mapping (`@GetMapping("/{id}")`) and substitutes template variables — the recorded argument `id` fills `{id}`. `@RequestParam` arguments become query parameters. - You can post-process the builder: `.slash("sub")` appends a path segment; `.withSelfRel()`/`.withRel(...)` finalize into a `Link`; `.toUri()` yields a plain `java.net.URI` (no rel). **Base URI resolution:** `WebMvcLinkBuilder` obtains the base from the **current request** using `ServletUriComponentsBuilder` / `RequestContextHolder.getRequestAttributes()`. It reads scheme, server name, port, and the servlet **context path**, so the href is absolute and environment-correct without configuration. This is why you never hardcode `http://localhost:8080`. **The request-context requirement (major gotcha):** because it reads thread-bound request attributes, `linkTo`/`methodOn` must execute on the **request-handling thread**. If you call them: - from a `@Scheduled` task or message listener → **no request** → `IllegalStateException`/error. - from an `@Async` method whose executor didn't propagate request attributes → same problem (fix by propagating context or building URIs explicitly). - from a unit test with no mocked request → fails; use `MockMvc`/integration tests, or set up `RequestContextHolder`, or use `UriComponentsBuilder` manually. WebFlux uses a different builder, `WebFluxLinkBuilder`, which threads context reactively. **Reverse proxies / load balancers:** externally clients hit `https://api.example.com` but the app sees `http://internal-host:8080`. To emit the public URL, the proxy must send `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-Port`/`Forwarded`, and Spring must **trust** them by registering `ForwardedHeaderFilter` (a bean of it, or `server.forward-headers-strategy=framework/native` in Boot). Without that, generated links leak internal scheme/host — a classic HATEOAS-behind-proxy bug. **Determinism note:** because base URI depends on the request, the same endpoint can legitimately produce different hrefs for different callers (different Host header). Tests should assert on path suffixes or use a fixed mock request.
- Your generated links show the internal host http://10.0.0.5:8080 instead of https://api.example.com. Why, and how do you fix it?The app isn't trusting proxy forwarded headers. Have the proxy send X-Forwarded-Proto/Host/Port (or Forwarded) and register ForwardedHeaderFilter — in Spring Boot set server.forward-headers-strategy=framework (or native).
- Why does linkTo(methodOn(...)) throw when called from a @Scheduled job?It reads the current request via RequestContextHolder; a scheduler thread has no bound request, so there's no base URI to resolve. Build the URI explicitly or use a configured base path there.
saying these in an interview costs you the question
- Believing generated hrefs are relative — they are absolute, built from the current request.
- Thinking Spring trusts X-Forwarded-* by default without ForwardedHeaderFilter/forward-headers-strategy.
- Assuming linkTo works on any thread regardless of request context.