Skip to content
learn

Spring Cloud Gateway

Spring Cloud Gateway is the reactive counterpart of YARP: predicates select a route, filters transform the exchange, and a target URI decides where it goes.

The course gateway exposes /api/scales/**, removes the public /api prefix and adds one traceable header:

@Bean
RouteLocator courseRoutes(RouteLocatorBuilder routes,
@Value("${course.scales-uri}") String scalesUri) {
return routes.routes()
.route("scales", route -> route.path("/api/scales/**")
.filters(filters -> filters.stripPrefix(1)
.addRequestHeader("X-Course", "spring-cloud-gateway"))
.uri(scalesUri))
.build();
}

The route owns transport policy, not music-domain decisions. Authentication, rate limits and correlation are reasonable edge concerns; chord construction is not. Every filter also changes failure semantics, so keep the chain short and observable.

The integration test binds both processes to OS-assigned ports. A tiny Reactor Netty upstream echoes its path and header; the gateway starts through @SpringBootTest and receives the upstream URI through @DynamicPropertySource.

upstream = HttpServer.create().port(0)
.route(routes -> routes.get("/scales/C", (request, response) -> response
.sendString(Mono.just(request.uri() + "|"
+ request.requestHeaders().get("X-Course")))))
.bindNow();

The assertion is deterministic:

GET /api/scales/C -> /scales/C|spring-cloud-gateway
GET /other -> 404

Run it with:

mvn -B -pl l06-gateway -am test

This covers predicate matching, filter order, forwarding and an unmatched path. It does not prove production latency, TLS termination or proxy-header trust; those require deployment-specific tests.

  1. Add a route for /api/progressions/** that preserves the path after removing /api.
Solution

Add a second route with a distinct ID, a path("/api/progressions/**") predicate and stripPrefix(1). Test the upstream URI rather than inspecting the route object.

  1. Add a response header containing the matched route ID.
Solution

Use addResponseHeader("X-Route", "scales") and assert it with WebTestClient.expectHeader(). Do not expose internal host names.

  1. Why should a retry filter not cover every request automatically?
Solution

Retries can duplicate non-idempotent effects and multiply load during an outage. Restrict them by method and failure class, bound attempts, and measure retry amplification. Lesson 9 adds resilience deliberately.