MockMVC multipart returns 404 with context-path - spring

I am using MockMvc multipart in spring boot to test a RestController which accepts a multipart file.
The test runs fine when I don't set context-path in application.yml but as soon as I set it to something the mockMvc returns 404.
Following config works:
Application.yml
server:
port: 8080
servlet:
context-path: /
use-forward-headers: true
forward-headers-strategy: FRAMEWORK
Test.java
// setup
String templateUploadUrl = "http://localhost:8080/v1/file/upload?name=%s";
byte[] templateContent = DataFactory.getFileAsByteArray();
String contentParameterName = "file";
String fileName = "fileName";
MockMultipartFile multipartFile = new MockMultipartFile(contentParameterName, templateContent);
templateUploadUrl = String.format(templateUploadUrl, fileName);
this.mvc.perform(multipart(templateUploadUrl).file(multipartFile));
But following config gives 404:
Application.yml
server:
port: 8080
servlet:
context-path: /context
use-forward-headers: true
forward-headers-strategy: FRAMEWORK
Test.java
// setup
String templateUploadUrl = "http://localhost:8080/context/v1/file/upload?name=%s";
byte[] templateContent = DataFactory.getFileAsByteArray();
String contentParameterName = "file";
String fileName = "fileName";
MockMultipartFile multipartFile = new MockMultipartFile(contentParameterName, templateContent);
templateUploadUrl = String.format(templateUploadUrl, fileName);
this.mvc.perform(multipart(templateUploadUrl).file(multipartFile));

Related

Retaining the Request's Path During Spring Cloud Gateway Failover

Is there a way to externally configure Spring Cloud Gateway to failover to another data center? I'm thinking of something like this:
spring:
cloud:
gateway:
routes:
- id: test-service
uri: lb://test-service:8085/
predicates:
- Path=/test-service/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: fallback
fallbackUri: forward:/fallback
#fallbackUri: forward:/fallback/test-service
- id: fallback
uri: http://${fallback_data_center}
predicates:
- Path=/fallback/**
---
spring:
config:
activate:
on-profile: data_center_1
fallback_data_center: dc2.com
---
spring:
config:
activate:
on-profile: data_center_2
fallback_data_center: dc1.com
The problem I run into is that the CircuitBreaker filter's fallbackUri parameter only supports forward schemed URIs. However, the path part of the request URL is overridden with the path in the forward URL. So there does not appear to be a way to failover with the path from the original request such as if this configuration had received a request of http://dc1.com/test-service/some/path without creating a configuration for every possible path.
At the time of writing this answer there is still now official way of doing a failover to another host.
What we are trying to achieve in our team is to have routes with Retry and CircuitBreaker filters which can fallback to another host keeping the original request unmodified ( request payload, header, query params and most importantly the API context path ) and just replacing the host so we can fallback to another datacenter.
We archived this by using the default Gateway Retry and CircuitBreaker filters and developing a custom FallbackController which just replaces the host with a configured property and keeps the rest of the request unmodified including the request context path:
#RestController
#RequestMapping("/fallback")
#ConditionalOnProperty(value="gateway.fallback.enabled", havingValue = "true")
public class FallbackController {
private final GatewayFallbackConfig gatewayFallbackConfig;
private final WebClient webClient;
public FallbackController(GatewayFallbackConfig gatewayFallbackConfig) {
this.gatewayFallbackConfig = gatewayFallbackConfig;
this.webClient = WebClient.create();
}
#PostMapping
Mono<ResponseEntity<String>> postFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
#GetMapping
Mono<ResponseEntity<String>> getFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
#PatchMapping
Mono<ResponseEntity<String>> patchFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
#DeleteMapping
Mono<ResponseEntity<String>> deleteFallback(#RequestBody(required = false) String body,
ServerWebExchangeDecorator serverWebExchangeDecorator) {
return fallback(body, serverWebExchangeDecorator);
}
private Mono<ResponseEntity<String>> fallback(String body, ServerWebExchangeDecorator serverWebExchangeDecorator) {
ServerHttpRequest originalRequest = serverWebExchangeDecorator.getDelegate().getRequest();
WebClient.RequestBodySpec request = webClient.method(originalRequest.getMethod())
.uri(buildFallbackURI(originalRequest));
Optional.ofNullable(body)
.ifPresent(request::bodyValue);
return request.exchangeToMono(response -> response.toEntity(String.class));
}
private URI buildFallbackURI(ServerHttpRequest originalRequest) {
return UriComponentsBuilder.fromHttpRequest(originalRequest)
.scheme(gatewayFallbackConfig.getScheme())
.host(gatewayFallbackConfig.getHost())
.port(gatewayFallbackConfig.getPort())
.build(ServerWebExchangeUtils.containsEncodedParts(originalRequest.getURI()))
.toUri();
}
With an additional property configuration holder:
#Getter
#Component
#RefreshScope
#ConditionalOnProperty(value="gateway.fallback.enabled", havingValue = "true")
public class GatewayFallbackConfig {
private final String scheme;
private final String host;
private final String port;
private final Set<String> excludedHeaders;
public GatewayFallbackConfig(
#Value("${gateway.fallback.scheme:https}") String scheme,
#Value("${gateway.fallback.host}") String host,
#Value("${gateway.fallback.port:#{null}}") String port,
#Value("${gateway.fallback.headers.exclude}") Set<String> excludedHeaders) {
this.scheme = scheme;
this.host = host;
this.port = port;
this.excludedHeaders = excludedHeaders;
}
And we are using it with a route configuration like that:
- id: example-route
uri: http://localhost:8080
predicates:
- Path=/foo/bar/**
filters:
- name: CircuitBreaker
args:
name: exampleCircuitBreaker
fallbackUri: forward:/fallback
statusCodes:
- INTERNAL_SERVER_ERROR
- BAD_GATEWAY
- SERVICE_UNAVAILABLE
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT
series: SERVER_ERROR
methods: GET,POST,PUT,DELETE
exceptions: org.springframework.cloud.gateway.support.NotFoundException,javax.security.auth.login.LoginException
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
gateway:
fallback:
scheme: https
host: some.other.host.com
enabled: true

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

How to resolve "SSLV3_ALERT_BAD_CERTIFICATE" error in Spring Boot

I have the following rest end point exposed protected by SSL (Spring Boot)
#RestController
public class TestController {
#RequestMapping(value = "/data", method = RequestMethod.GET)
public String getData() {
return "Hello World";
}
In YML I have the following properties
server:
ssl:
enabled: true
client-auth: need
key-store: {keystore-path}
key-store-password: {keystore-password}
key-alias: alias-name
key-store-type: JKS
Now I am trying to call the above rest end point from another app with the following code
URL obj = new URL(GET_URL);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
int responseCode = con.getResponseCode();
System.out.println("GET Response Code :: " + responseCode);
But I am getting the following error :
Error: write EPROTO 2771201016:error:10000410:SSL routines:OPENSSL_internal:SSLV3_ALERT_BAD_CERTIFICATE:../../third_party/boringssl/src/ssl/tls_record.cc:587:SSL alert number 42`
How to resolve this error?

Required request part 'file' is not present in Spring Boot

I checked all of the simular posts and still couldnt find the solution.
Problem is Required request part 'file' is not present in test class.
I want to upload a file and save it to the database. Here is my rest controller #RestController:
#PostMapping(value = "/upload")
public ResponseEntity<LogoDto> uploadLogo(#RequestParam("file") MultipartFile multipartFile) {
return ResponseEntity.ok(logoService.createLogo(multipartFile));
}
and my test class:
#Test
public void createLogo2() throws Exception {
String toJsonLogoDto = new Gson().toJson(logoDto);
MockMultipartFile file = new MockMultipartFile("path", "url", MediaType.APPLICATION_JSON_VALUE, image);
LogoDto response = LogoDataTest.validLogoDto();
Mockito.when(logoServiceMock.createLogo(Mockito.any(MultipartFile.class))).thenReturn(response);
mockMvc.perform(MockMvcRequestBuilders.multipart("/brand-icon/upload")
.file(file)
.content(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.characterEncoding(CharEncoding.UTF_8))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk());
}
and my application.yml looks like this:
spring:
servlet:
multipart:
enabled: true
max-file-size: 2MB
max-request-size: 10MB
I tried to add consumes in my #PostMapping;
try to set literally every MediaTypes.. still get an error.
I appreciate all of your answer.
issue is in declaration of MockMultipartFile, first parameter should match controller #RequestParam param. So, in your case, should be:
MockMultipartFile file = new MockMultipartFile("file", "url", MediaType.APPLICATION_JSON_VALUE, image);
Also, I recommend to update your controller method to the following one:
#PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<LogoDto> uploadLogo(#RequestPart("file") MultipartFile multipartFile) {
...
}

Feign Client ignoring request params

I created Feign Client:
#FeignClient(name = "yandex",url="${yandex.ribbon.listOfServers}")
public interface YandexMapsRestApiServiceClient {
#RequestMapping(method = RequestMethod.GET, value = "{geoParam}")
String getCountryInfo(#Param("geoParam") String geoParam);
}
In controller I have been wrote:
#Autowired
private YandexMapsRestApiServiceClient client;
#RequestMapping(value = "/", method = RequestMethod.GET)
public String test() {
return client.getCountryInfo("Moscow");
}
My Applicaton.yml look this:
yandex:
ribbon:
listOfServers: https://geocode-maps.yandex.ru/1.x/?format=json&geocode=
ConnectTimeout: 20000
ReadTimeout: 20000
IsSecure: true
hystrix.command.default.execution:
timeout.enabled: true
isolation.thread.timeoutInMilliseconds: 50000
When I try to get some result, in return I get 404 error:
feign.FeignException: status 404 reading YandexMapsRestApiServiceClient#getCountryInfo(String); content:
In this case, I see in the debugger that he feign not set my geoParam:
Why does this happen and how to solve this problem?
As Musaddique has stated, you are mixing Feign and Spring annotations. When using Spring Cloud Feign(OpenFeign), you must use the Spring annotation RequestParam. Feign annotations will not be processed.
Update
To achieve what you are looking for, you will need to change your configuration. The of url should be a url or service name only. Using query string or other extensions to the url will have unexpected results.
Move the path information to the RequestMapping annotation and specify the query parameter there.
#FeignClient(name = "yandex", url="${yandex.ribbon.listOfServers}")
public interface YandexMapsRestApiServiceClient {
#RequestMapping(method = RequestMethod.GET, value = "/1.x?format=json&geocode={geoParam}")
String getCountryInfo(#RequestParam("geoParam") String geoParam);
}
Where your ribbon configuration looks like this:
yandex:
ribbon:
listOfServers: "https://geocode-maps.yandex.ru"
ConnectTimeout: 20000
ReadTimeout: 20000
IsSecure: true
Now, using your example of client.getCountryInfo("moscow") will result in the final url of https://geocode-maps.yandex.ru/1.x?format=json&geocode=moscow.

Resources