SpringBootTest with Zuul Proxy - Empty response - spring

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();
}

Related

Spring Boot: Connection timed out when trying to call a service from a service

I have 2 microservices + an Eureka Server in which they are registerd.
I made really everything I could think of, yet when I try to call the login service from the manager service, I always get "Connection timed out".
POST http://localhost:9903/login
{
"username":"adm4",
"password":"adm4adm4"
}
I have tried to work with Spring RestTemplate and WebClient and also Apache HttpClient.
All the times, the flow reaches the post method, and I get the same result.
I guess it must be some configuration issue.
I am working on localhost with all modules.
It really drives me crzay!
Please advise. I appreciate it.
The relevant info is as follows. Please tell me if you need more info.
First of all you can see that the services are registered and up:
Next the code:
Manager (calling) Service:
(I left inside all my previous attempts commented)
#PostMapping("/login")
public void login(#RequestBody LoginRequest loginRequest) throws Exception {
String url = getBaseUrl("bbsim-login-service") + "/api/auth/signin";
/* CloseableHttpClient httpclient = HttpClients.createDefault();
try {
HttpPost httpPost = new HttpPost(getBaseUrl("bbsim-login-service") + "/api/auth/signin");
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("username", loginRequest.getUsername()));
params.add(new BasicNameValuePair("password", loginRequest.getPassword()));
httpPost.setEntity(new UrlEncodedFormEntity(params));
CloseableHttpResponse response = httpclient.execute(httpPost);
System.out.println(response.getStatusLine().getStatusCode());
} finally {
httpclient.close();
}
*/
/* HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
// Connect timeout: time is in milliseconds
clientHttpRequestFactory.setConnectTimeout(30000);
// Read timeout: time is in milliseconds
clientHttpRequestFactory.setReadTimeout(30000);
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);
HttpEntity<LoginRequest> request = new HttpEntity<>(loginRequest);
JwtResponse res = restTemplate.postForObject(url, request, JwtResponse.class);
System.out.println(res);
*/
localApiClient
.post()
.uri(url)
.body(Mono.just(loginRequest), LoginRequest.class)
.retrieve()
.bodyToMono(JwtResponse.class)
.block();
}
private String getBaseUrl(String serviceName) {
Application application = eurekaClient.getApplication(serviceName);
InstanceInfo instanceInfo = application.getInstances().get(0);
String hostname = instanceInfo.getHostName();
int port = instanceInfo.getPort();
return "http://" + hostname + ":" + port;
}
application.yml:
server.port: 9903
spring:
application.name: bbsim-manager-service
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:8088/eureka}
registryFetchIntervalSeconds: 1
# register-with-eureka: true
# fetch-registry: true
instance:
leaseRenewalIntervalInSeconds: 1
If I understand well, the request does not reach the login service at all.
Login (called) service:
#PostMapping("/signin")
public ResponseEntity<?> authenticateUser(#Valid #RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok().body(new JwtResponse(jwt,
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
application.yml file:
server.port: 9902
spring:
application:
name: bbsim-login-service
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8088/eureka/
registryFetchIntervalSeconds: 1
instance:
leaseRenewalIntervalInSeconds: 1
I addition, I tried the following - giving me the same results:
curl -d "#data.json" -H "Content-Type: application/json" -X POST http://localhost:9903/login
where data.json has the body contents.
This will not be a complete answer but I hope it helps you with your issue.
I think your problem could be related with a mix of the different IP address of your machine.
First, I think Eureka is exposing your services like host.docker.internal, as indicated, the logical name that references the host machine through the different docker containers, for the reason explained in this SO question.
Basically, it seems that the docker software is updating your hosts file with entries for host.docker.internal and gateway.docker.internal and Eureka probably is taking that alias as the one for the machine IP that is being advertised. Please, see the accepted answer in the aforementioned question.
When you run Spring Boot normally the underlying server (Tomcat, Jetty, Undertow) will listen for connections in the 0.0.0.0 address, i.e., in all the network interfaces available, including localhost. This is what troubles me, because as indicated, the service should be accessible through all the IPs in the machine.
In any way, I think you can try several things to solve your issue.
Probably the best approach to solve the problem will be to configure the hostname of your Eureka server and/or your Eureka clients to a common one.
For example, you can configure your server and clients to be exposed as localhost.
For that purpose, you need to include the following configuration property in their respective config files:
eureka:
instance:
hostname: localhost
Looks like you are using Docker. You are trying to connect to localhost but other services are running in other container hence localhost won’t work. Would you please try 0.0.0.0 or host.docker.internal in your YAML file and see if that will work.
In other words you will need to edit following.
server.port: 9903
spring:
application.name: bbsim-manager-service
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://host.docker.internal:8088/eureka}
registryFetchIntervalSeconds: 1
# register-with-eureka: true
# fetch-registry: true
instance:
leaseRenewalIntervalInSeconds: 1
or change EUREKA_URI env variable to reflect that. Also in your service YAML
server.port: 9902
spring:
application:
name: bbsim-login-service
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://host.docker.internal:8088/eureka/}
registryFetchIntervalSeconds: 1
instance:
leaseRenewalIntervalInSeconds: 1

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

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)));
}

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

zuul eureka service id redirection

I am having few spring boot microservices, which are deployed to JBoss over a cloud environment. These boot services are Eureka clients which register itself in to the Eureka server. Following is an example:
eureka:
client:
healthcheck:
enabled: true
serviceUrl:
defaultZone: ${DISCOVERY_URL:http://localhost:8761}/eureka/
instance:
ip-address: 127.0.0.1
appname: user-regn-service-app
home-page-url-path: /user-regn-service-app
It registers the app with Eureka with the name user-regn-service-app
Eureka Homepage
The wildfly server is running at 8080 and the user-regn-service-app is deployed at the context path /user-regn-service-app.
So the rest api is as below
localhost:8080/user-regn-service-app/regnUser
When I am using zuul as api gateway, the config is as below
zuul:
prefix: /api
routes:
test:
path: /test/**
service-id: USER-REGN-SERVICE-APP
strip-prefix: true
ribbon:
eureka:
enabled: true
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
registerWithEureka: false
But whenever I am making call to zuul api gateway it is unable to recognize the context path and redirects to localhost:8080 instead of localhost:8080/user-regn-service-app.
http://localhost:8765/api/ -> 404 not found
http://localhost:8765/api/user-regn-service-app/ -> Wildfly default homepage
http://localhost:8765/api/user-regn-service-app/user-regn-service-app/regnUser -> Redirects to user registration.
Expected behavior: http://localhost:8765/api/test/regnUser should redirect to the user registration.
I have pretty much tried all combinations that I got from blogs between Zuul and Eureka to get the following done but no luck. Kindly advise if I am missing something.
I have tried using custom zuul custom filter as below but it doesn't forward to the Required Context path. Code is as below
#Component
public class ZuulApiFilter extends ZuulFilter{
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
System.out.println("original"+ ctx.get("requestURI"));
HttpServletRequest request = ctx.getRequest();
String requestURI = request.getRequestURI();
String contextAwareURI=requestURI.concat("user-regn-service-app/");
ctx.set("requestURI", contextAwareURI);
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public int filterOrder() {
return 1;
}
#Override
public String filterType() {
return "pre";
}
}
The requestURI doesn't changes after setting the new URI as well
ctx.set("requestURI", contextAwareURI);
request.getRequestURI(); shows the old Request URI only.
Could you remove ZuulApiFilter and try setting:
strip-prefix: false
and try sending the request to:
http://<zuul ip>:<zuul port>/api/test/....
Edited:
#Tanmay Ghosh
Here is a sample code I'm using for Zuul-related blog post (still in draft) that I'll publish in the next couple of days:
Zuul's application.yml:
...
eureka:
client:
registerWithEureka: false
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8000/eureka/
# ribbon.eureka.enabled: false
zuul:
ignoredServices: "*"
routes:
zuulDemo1:
path: /zuul1/**
# serviceId as registed with Eureka. Enabled and used when ribbon.eureka.enabled is true.
serviceId: demo-zuul-api1
# zuul.routes.<the route>.url used when ribbon.eureka.enabled is false, serviceId is disabled.
# url: http://localhost:8600/
# stripPrefix set to true if context path is set to /
stripPrefix: true
...
And actually my Zuul server repo is public and available at: https://bitbucket.org/asimio/zuulserver and a recent blog post at http://tech.asimio.net/2017/10/10/Routing-requests-and-dynamically-refreshing-routes-using-Spring-Cloud-Zuul-Server.html
Another thing, Does the Zuul service also uses an app context other than / ? If so, Could you try sending the request via Zuul at: http://<zuul host>:<zuul port>/<zuul app context>/api/test/.... ?

Resources