Did properties-based (from configuration server) override/replace java-based routes config? - spring

I use server with defining of some routes in yml configuration, which stored in Consul Key/Value. When I'm trying to define route using Fluent API (Java based config), gateway doesn't work properly and doens't process this routes.
Example of server based config:
cloud:
gateway:
discovery:
locator:
enabled: false
routes:
- id: foo
predicates:
- Path=/foo/**
uri: lb:https://bar
And defining routes in Fluent style:
#Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p
.path("/testing_route")
.filters(f -> f.addRequestHeader("Hello", "World"))
.uri("http://httpbin.org:80"))
.build();
}
As result gateway return 404 status code for all requests to the /testing_route path, which mean this route is not working.
In case of my problem i want to modify request body using ModifyRequestBodyFilter which based on DSL configuration, that means - I need to use both ways to configure context.
In reality this code does nothing.
Can we combine RouteLocatorBuilder with property-based config in yml?
Spring Boot 2.2.5 RELEASE
Spring Cloud Hoxton.SR3

Answered in issue thread
https://github.com/spring-cloud/spring-cloud-gateway/issues/1953#issuecomment-705081934
TL;DR
Need to enable CachingRouteLocator
#Bean
#Primary
#ConditionalOnMissingBean(name = "cachedCompositeRouteLocator")
// TODO: property to disable composite?
public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}

Related

Is it possible to change the service endpoint path on gateway using spring cloud

I have a running api on my local machine with url http://localhost:8080/gnk-debt/service/taxpayer-debt.
And I would like this api to be available through gateway with url http://localhost:8243/gnk/service/phystaxpayer/debt/v1.
To do so, first I set the port in property file. But I am not able set custom url for the api endpoint. I am trying to write a simple gateway using spring cloud, configuration of which as following:
#Configuration
public class SpringCloudConfig {
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder routeLocatorBuilder)
{
return routeLocatorBuilder.routes()
.route("gnkTaxpayerDebt", rt -> rt.path("/gnk-debt/**")
.uri("http://localhost:8080/"))
.build();
}
}
But at the end, gateway api endpoint is available at: http://localhost:8243/gnk-debt/service/taxpayer-debt.
The question that I am curios about is that if it is possible to change gateway api endpoint to:
http://localhost:8243/gnk/service/phystaxpayer/debt/v1
EDIT
As spencergibb mentioned that there are some options to do that. I started with RewritePath, subsequently my config has been changed as following:
#Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder routeLocatorBuilder)
{
return routeLocatorBuilder.routes()
.route("gnkTaxpayerDebt", rt -> rt.path("/gnk/**")
.filters(f->f.rewritePath("/gnk/service/phystaxpayer/debt/v1(?<remains>.*)","/${remains}"))
.uri("http://localhost:8080"))
.build();
}
At the end I am able to access my gateway endpoint at
http://localhost:8243/gnk/service/phystaxpayer/debt/v1/gnk-physicaltaxpayer-debt/gnk/service/physical-taxpayer-debt
How can I change the final endpoint to: http://localhost:8243/gnk/service/phystaxpayer/debt/v1
One of the ways of setting your own endpoint is to create a GlobalFilter where you change the attribute "GATEWAY_REQUEST_URL_ATTR" that comes with ServerWebExchangeUtils. You just have to set your endpoint as below
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, yourEndpoint);

Wrong "Generated server url" in springdoc-openapi-ui (Swagger UI) deployed behind proxy

Spring Boot 2.2 application with springdoc-openapi-ui (Swagger UI) runs HTTP port.
The application is deployed to Kubernetes with Ingress routing HTTPS requests from outside the cluster to the service.
In this case Swagger UI available at https://example.com/api/swagger-ui.html has wrong "Generated server url" - http://example.com/api. While it should be https://example.com/api.
While Swagger UI is accessed by HTTPS, the generated server URL still uses HTTP.
I had same problem. Below worked for me.
#OpenAPIDefinition(
servers = {
#Server(url = "/", description = "Default Server URL")
}
)
#SpringBootApplication
public class App {
// ...
}
If the accepted solution doesn't work for you then you can always set the url manually by defining a bean.
#Bean
public OpenAPI customOpenAPI() {
Server server = new Server();
server.setUrl("https://example.com/api");
return new OpenAPI().servers(List.of(server));
}
And the url can be defined via a property and injected here.
springdoc-openapi FAQ has a section How can I deploy the Doploy springdoc-openapi-ui, behind a reverse proxy?.
The FAQ section can be extended.
Make sure X-Forwarded headers are sent by your proxy (X-Forwarded-For, X-Forwarded-Proto and others).
If you are using Undertow (spring-boot-starter-undertow), set property server.forward-headers-strategy=NATIVE to make a Web server natively handle X-Forwarded headers. Also, consider switching to Undertow if you are not using it.
If you are using Tomcat (spring-boot-starter-tomcat), set property server.forward-headers-strategy=NATIVE and make sure to list IP addresses of all internal proxies to trust in the property server.tomcat.internal-proxies=192\\.168\\.\\d{1,3}\\.\\d{1,3}. By default, IP addresses in 10/8, 192.168/16, 169.254/16 and 127/8 are trusted.
Alternatively, for Tomcat set property server.forward-headers-strategy=FRAMEWORK.
Useful links:
Running Behind a Front-end Proxy Server
Customize Tomcat’s Proxy Configuration
ServerProperties.ForwardHeadersStrategy
In case you have non-default context path
#Configuration
public class SwaggerConfig {
#Bean
public OpenAPI openAPI(ServletContext servletContext) {
Server server = new Server().url(servletContext.getContextPath());
return new OpenAPI()
.servers(List.of(server))
// ...
}
}
Below worked for me.
#OpenAPIDefinition(servers = {#server(url = "/", description = "Default Server URL")})
#SpringBootApplication
class App{
// ...
}
or
#OpenAPIDefinition(servers = {#server(url = "/", description = "Default Server URL")})
#Configuration
public class OpenAPIConfig {
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("App name")
.termsOfService("http://swagger.io/terms/")
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
}
}
Generated server url is HHTP - issue

SpringBootTest with Zuul Proxy - Empty response

Introduction:
I'm currently writing integration tests for my API Gateway with Zuul Proxy using SpringBootTest.
I've already created a working mock microservice on port 8089 returning some json data, to which the gateway application should forward incoming requests.
Problem:
Zuul manages to match the routes correctly, however it fails somehow to forward the request, because the response is always an empty HTTP 200, whereas it should contain the json data returned by the mocked microservice.
This problem occures only in test . It works fine in production.
Recent observations: While debugging I've figured out that in test, the FilterLoader does not provide any filters of type route, while in production it provides a list of three filters which are then used by the route() method of the ZuulServlet.
After the route() method is processed, the response is populated with data obtained from the microservice. It does not happen in test.
I've also tried to replace the mock server with a real one - result was exactly the same.
Question:
I would appreciate any advice on this problem ;)
Zuul Config:
logging:
level:
org:
springframework:
cloud:
netflix: trace
ribbon:
eureka:
enabled: false
eureka:
client:
enabled: false
zuul:
routes:
restuarant:
path: /restaurant/**
url: http://localhost:8089
spring:
cloud:
discovery:
enabled: false
...
Test Class Annotations:
#ExtendWith(SpringExtension.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#DirtiesContext(classMode = ClassMode.AFTER_CLASS)
#AutoConfigureWebTestClient(timeout = "30000")
Calling the tested endpoint
//the mocked microservice has an endpoint http://localhost:8089/restaurants
private WebTestClient.ResponseSpec getResource(String accessToken) {
return webClient.get()
.uri("/restaurant/restaurants")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, TokenType.BEARER.getValue() + " " + accessToken)
.exchange();
}

How to retry an external service when a call to an internal service fails using spring cloud gateway?

I'm implementing a service that mocks another service available on the web.
I'm trying to configure the spring cloud gateway to reroute requests to the public service when a call to my implementation fails.
I tried using the Hystrix filter the following way:
spring:
cloud:
gateway:
routes:
- id: foo
uri: http://my-internal-service/
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: https://the.public.service/
Unfortunately, like the documentation says:
Currently, only forward: schemed URIs are supported. If the fallback is called, the request will be forwarded to the controller matched by the URI.
Therefore, I can't use fallbackUri: https://....
Is there any plan to support this feature soon?
Otherwise, what are my options for this particular use case?
I ended up with a kind of hacky workaround that seems to work for my particular use case (i.e. a GET request):
Create my own fallback controller in the Gateway application
Configure the hystrix fallback to point to that controller
Use the WebClient to call my public service
This is what the end result looks like:
application.yml
spring:
cloud:
gateway:
default-filters:
- name: AddResponseHeader
args:
name: X-Data-Origin
value: My internal service
routes:
- id: foo
uri: http://my-internal-service/
filters:
- name: Hystrix
args:
name: local-service-fallback
fallbackUri: forward:/fallback/foo
FallbackController.java
#RestController
#RequestMapping(path = "/fallback")
public class FallbackController {
private static final String fallbackUri = "https://the.public.service";
WebClient webClient;
public FallbackController() {
webClient = WebClient.create(fallbackUri);
}
#GetMapping("/foo")
Mono<MyResponse> foo(ServerWebExchange failedExchange) {
failedExchange.getResponse().getHeaders().remove("X-Data-Origin");
failedExchange.getResponse().getHeaders().add("X-Data-Origin", "The public service");
// Now call the public service using the same GET request
UriComponents uriComponents = UriComponentsBuilder.newInstance()
.uri(URI.create(fallbackUri))
.path("/path/to/service")
.queryParams(failedExchange.getRequest().getQueryParams())
.build();
return WebClient.create(uriComponents.toUriString())
.get()
.accept(MediaType.TEXT_XML)
.exchange()
.doOnSuccess(clientResponse -> {
// Copy the headers from the public service's response back to our exchange's response
failedExchange.getResponse().getHeaders()
.addAll(clientResponse.headers().asHttpHeaders());
})
.flatMap(clientResponse -> {
log.info("Data origin: {}",
failedExchange.getResponse().getHeaders().get("X-Data-Origin"));
return clientResponse.bodyToMono(MyResponse.class);
});
}
}
I had similar problem to solve.
I added new route for fallback and it worked.
.route(p -> p .path("/fallback/foo").uri("https://example.com"))

Get the hostname/IP:port of the server to which the request is to be forwarded from Zuul+ribbon

I'm using Eureka for service discovery and Zuul+ribbon as reverse proxy and load balancer.
I have 2 instances registered under Eureka as follows:
MYSERVICE n/a (2) (2) UP (2) - MYHOST:MyService:8888 , MYHOST:MyService:9999
Below is my zuul config:
#EnableZuulProxy
#EnableDiscoveryClient
zuul:
debug.request: true
sensitiveHeaders:
routes:
ecm:
path: /myservice/**
serviceId: MYSERVICE
stripPrefix: false
host:
maxTotalConnections: 200
maxPerRouteConnections: 30
RibbonRoutingFilter:
route.disable: false
I want a filter or interceptor which would help me log my request URL, my request params and the server chosen by Zuul.
I tried extending the following:
#Component
public class RibbonInterceptor extends ZoneAvoidanceRule {
#Override
public Server choose(Object key) {
Server choose = super.choose(key);
System.out.println(choose);
return choose;
}
But, this just gave me the server info from Ribbon, and here ribbon was just choosing a server. I wanted this info from Zuul along with the request details.
Please help!!
For the request URL and the server chosen by Zuul, you can set the loglevel of the LoadBalancerContext to DEBUG in application.properties.
#logging load balancing information
logging.level.com.netflix.loadbalancer.LoadBalancerContext=DEBUG
This will create a log statement like:
2017-09-11T12:59:09.746-07:00: [DEBUG] hystrix-myserviceV3-2 com.netflix.loadbalancer.LoadBalancerContext - myserviceV3 using LB returned Server: myservice-2.abc.com:8080 for request http:///myservice/auth/users
Not sure though, how you can handle the request params.
Assuming you use Apache HttpClient, there are many ways to do this but I think the most simple is to add an HttpRequestInterceptor to the CloseableHttpClient used by Ribbon. You can customize the client by providing a bean of type CloseableHttpClient as mentioned in the documentation [1]. You then have the request actually used by HttpClient so you log the details.
#Bean
public HttpClient delegate(IClientConfig clientConfig)
{
HttpClientBuilder builder = HttpClientBuilder.create();
//set connection pool configuration
HttpRequestInterceptor interceptor = (request, context) -> logger.info("Server : {}, headers : {}", request.getRequestLine().getUri(), request.getAllHeaders());
builder.addInterceptorFirst(interceptor);
return builder.build();
}
You could also extend HttpClientRibbonCommand and override the run() method to print what you want. You would use your new class by providing a bean of type RibbonCommandFactory<YourExtendedRibbonCommand> and it should wire automatically to the RibbonRoutingFilter.
Finally, if you're using the semaphore isolation strategy in hystrix, you could use your RibbonInterceptor like you indeed in addition to com.netflix.zuul.context.RequestContext. In the RequestContext, you'll find the original HttpServletRequest along with parsed parameters and headers that were processed in the pre filters.
[1] https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#_zuul_http_client

Resources