SpringBoot controller redirection doesn't work - spring-boot

Hitting the / directory of my Rest app doesn't redirect to what I what, but just print the redirection directive on the screen: "redirect:swagger-ui.html"
My controller:
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
#RestController
class HomeController(val info: InfoProperties) {
#RequestMapping("/")
fun home(): String {
return "redirect:/swagger-ui.html"
}
}

Using curl, we see that the answer is text (Content-Type: text/plain;charset=UTF-8), hence the simple unexpected text output:
> curl -v "http://localhost:8080/"
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: * / *
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 25
< Date: Fri, 15 Mar 2019 17:52:32 GMT
<
* Connection #0 to host localhost left intact
redirect:/swagger-ui.html
The #RestController annotation is a specialized version of the controller. It includes the #Controller and #ResponseBody annotations, and #ResponseBody is the cause of our problem.
So to fix this, replace the #RestController annotation with the more generic #Controller one:
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.stereotype.Controller
#Controller
class HomeController(val info: InfoProperties) {
#RequestMapping("/")
fun home(): String {
return "redirect:/swagger-ui.html"
}
}
The redirection now works properly.

You can actually achieve this without changing #RestController to #Controller. What you need to do is return a RedirectView instead of a string. This is how I have it working in java:
#RestController
#ApiIgnore
public class ApiDocsRedirectController {
#RequestMapping(value = {"/","/api-docs","/v3/api-docs"})
public RedirectView redirect() {
return new RedirectView("/swagger-ui.html");
}
}

Related

Spring webflux: ServerResponse redirection

This is my related code:
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ServerResponse> gicar(#RequestHeader("GICAR_ID") String gicarId) {
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
Issue arises when I'm calling to _/login endpoint:
$ curl -i -X POST localhost:8080/login -H "GICAR_ID: tre"
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1 ; mode=block
Referrer-Policy: no-referrer
curl: (18) transfer closed with outstanding read data remaining
Why am I getting an 200 http code response?
On spring boot logging I'm getting this exception:
022-06-27 13:11:19.931 ERROR 79654 --- [or-http-epoll-2] r.n.http.server.HttpServerOperations : [9750a9d8-1, L:/127.0.0.1:8080 - R:/127.0.0.1:33150] Error finishing response. Closing connection
org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.springframework.web.reactive.function.server.DefaultServerResponseBuilder$WriterFunctionResponse and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
Why above exception is reaised?
Any ideas?
According to Spring documentation ServerResponse
Represents a typed server-side HTTP response, as returned by a handler function or filter function.
and it supposed to be used in Functional Endpoints
#Configuration
public class GicarConfiguration {
#Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions
.route(POST("/login"), this::loginHandler);
}
private Mono<ServerResponse> loginHandler(ServerRequest request) {
var gicarId = request.headers().firstHeader("GICAR_ID");
return ServerResponse.temporaryRedirect(URI.create("/me")).build();
}
}
If you still want to use Annotated Controllers, use ResponseEntity instead
#RestController
public class GicarController {
#PostMapping("/login")
public Mono<ResponseEntity<Void>> gicar() {
return Mono.just(ResponseEntity
.status(HttpStatus.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, "/me")
.build()
);
}
}

Spring Boot Test with Mockito : #Validated annotation is being ignored during unit tests

I'm using Spring Boot 2.1.1, JUnit 5, Mockito 2.23.4.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
Here's my controller :
#RestController
#Validated
public class AramaController {
#ResponseStatus(value = HttpStatus.OK)
#GetMapping("/arama")
public List<Arama> arama(#RequestParam #NotEmpty #Size(min = 4, max = 20) String query) {
return aramaService.arama(query);
}
}
This controller works as expected.
curl with no "query" parameter returns Bad Request 400 :
~$ curl http://localhost:8080/arama -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /arama HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Wed, 12 Dec 2018 21:47:11 GMT
< Connection: close
<
* Closing connection 0
curl with "query=a" as parameter returns Bad Request 400 as well :
~$ curl http://localhost:8080/arama?query=a -v
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /arama?query=a HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 400
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 12 Dec 2018 21:47:33 GMT
< Connection: close
<
* Closing connection 0
{"message":"Input error","details":["size must be between 4 and 20"]}
This controller and validation works flawlessly when running on a server.
During unit tests the #Validated annotation doesn't seem to have any effect.
Here my test code :
#ExtendWith(MockitoExtension.class)
class AramaControllerTest {
#Mock
private AramaService aramaService;
#InjectMocks
private AramaController aramaController;
private MockMvc mockMvc;
#BeforeEach
private void setUp() {
mockMvc = MockMvcBuilders
.standaloneSetup(aramaCcontroller)
.setControllerAdvice(new RestResponseEntityExceptionHandler())
.build();
}
#Test
void aramaValidationError() throws Exception {
mockMvc
.perform(
get("/arama").param("query", "a")
)
.andExpect(status().isBadRequest());
verifyNoMoreInteractions(aramaService);
}
}
This test results in failure :
java.lang.AssertionError: Status expected:<400> but was:<200>
Expected :400
Actual :200
Since the #Valid annotations pass my other test cases, and they work without loading the Spring context, is there a way to make the #Validated annotation work as well with Mockito (again, without loading the Spring context) ?
I got the answer elsewhere and wanted to share :
Without starting up the context, you won't have #Validator getting
tested because validator instances are Spring beans. However, #Valid
will work as it is a JSR-303 standard.
As of now, what I can suggest is.
#SpringBootTest
#ExtendWith(SpringExtension.class)
maybe you can try using #WebMvcTest and add SpringExtension
#ExtendWith({SpringExtension.class, MockitoExtension.class})
#WebMvcTest(AramaController.class)
class AramaControllerTest {
#Mock
private AramaService aramaService;
#InjectMocks
private AramaController aramaController;
#Autowired
private MockMvc mockMvc;
#Test
void aramaValidationError() throws Exception {
mockMvc
.perform(
get("/arama").param("query", "a")
)
.andExpect(status().isBadRequest());
verifyNoMoreInteractions(aramaService);
}
}

In spring, how about if PathVariable contains RequestMapping value

If I want to create a basic controller with RequestMapping = "/{content}" to handle the general case. But for some specific contents, I want to create a concrete controller for this special case, and inherit from that basic controller.
For example:
#RequestMapping(value = "/{content}")
class ContentController {
public ContentController(#PathVariable String content) { ... }
}
#RequestMapping(value = "/specialContent")
class SpecialContentController extends ContentController {
public SpecialContentController() { super("specialContent"); }
// overwrite sth
....
}
Is this legal? Or some other better implementation?
#PathVariable should not be used in constructor.
You seem confused about how controllers in spring work.
A controller is a singleton which is created on application upstart and whose methods are invoked to handle incoming requests.
Because your controller isn't created for each request but created before any requests are handled you can't use path variables in the constructor - both because there's no information about it's value when the instance is created but also because you'll want it to reflect the current request being handled and since controllers can handle many multiple requests simultaneously you can't store it as a class attribute or multiple requests would interfere with each other.
To achieve what you want you should use methods and compose them, something like this:
#RestController
public class ContentController {
#GetMapping("/specialContent")
public Map<String, String> handleSpecialContent() {
Map<String, String> map = handleContent("specialContent");
map.put("special", "true");
return map;
}
#GetMapping("/{content}")
public Map<String, String> handleContent(#PathVariable String content) {
HashMap<String, String> map = new HashMap<>();
map.put("content", content);
return map;
}
}
Note the regular expression in {content:^(?!specialContent$).*$} to ensure that Spring never routes specialContent there. You can get an explanation of the regular expression here and toy around with it here.
You can see that it works if we put it to the test:
$ http localhost:8080/test
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 01 Feb 2018 08:18:11 GMT
Transfer-Encoding: chunked
{
"content": "test"
}
$ http localhost:8080/specialContent
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 01 Feb 2018 08:18:15 GMT
Transfer-Encoding: chunked
{
"content": "specialContent",
"special": "true"
}

RequestMapping GET and POST methods handling in Spring REST

I have such code:
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#RequestMapping(value = "/foo", method = RequestMethod.POST)
String testPost(#RequestParam("param1") String param1, #RequestParam("param2") String param2) {
return param1 + param2;
}
#RequestMapping("/foo")
String testGet(#RequestParam String param1) {
return param1;
}
}
And I execute such curl expressions:
curl --verbose http://localhost:9000/foo?param1=Sergei
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> GET /foo?param1=Sergei HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
>
< HTTP/1.1 404 Not Found
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 29 Dec 2015 08:55:56 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1451379356514,"status":404,"error":"Not Found","message":"No message available","path":"/foo"}
and,
curl --verbose --data "param1=value1&param2=value2" http://localhost:9000/foo
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> POST /foo HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:9000
> Accept: */*
> Content-Length: 27
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 27 out of 27 bytes
< HTTP/1.1 405 Method Not Allowed
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Allow: HEAD, GET
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 29 Dec 2015 09:01:46 GMT
<
* Connection #0 to host localhost left intact
{"timestamp":1451379706832,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/foo"}
Help me please get both my methods work.
Add a RestController or Controller stereotype annotation to your Application class like this:
#RestController
#Configuration
#ComponentScan
#EnableAutoConfiguration
public class Application {
...
}
Note: You can use SpringBootApplication meta annotation instead of those three, so you would have:
#RestController
#SpringBootApplication
public class Application {
....
}
You should change the scope level of the two controller methods to public. Right now, they don't have any, so the methods are package local by default.
public String testPost(#RequestParam("param1") String param1, #RequestParam("param2") String param2) {
public String testGet(#RequestParam String param1) {

Spring/Eureka/Feign - FeignClient setting Content-Type header to application/x-www-form-urlencoded

When I use a FeignClient it is setting the Content-Type to application/x-www-form-urlencoded instead of application/json;charset=UTF-8.
If I use a RestTemplate to send the same message the message header Content-Type is correctly set to application/json;charset=UTF-8.
Both the FeignClient and RestTemplate are using Eureka for service discovery, and I discovered this problem by debugging the HTTP message received by the server.
The controller on the server side looks like this:
#RestController
#RequestMapping("/site/alarm")
public class SiteAlarmController {
#RequestMapping(method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<RaiseAlarmResponseDto> raiseAlarm(#RequestBody RaiseSiteAlarmRequestDto requestDto) {
...
}
My FeignClient interface in the service that calls the alarm looks like this:
#FeignClient("alarm-service")
public interface AlarmFeignService {
#RequestMapping(method = RequestMethod.POST, value = "/site/alarm")
RaiseAlarmResponseDto raiseAlarm(#RequestBody RaiseSiteAlarmRequestDto requestDto);
}
The HTTP message headers from the FeignClient are:
Accept: */*
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.7.0_60
Host: smit005s-MacBook-Pro.local:9120
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 323
The alarm service doesn't like the Content-Type and throws the following exception:
2015-04-22 12:12:28.580 thread="qtp1774842986-25" class="org.eclipse.jetty.servlet.ServletHandler" level="WARN"
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is feign.FeignException: status 415 reading AlarmFeignService#raiseAlarm(RaiseSiteAlarmRequestDto); content:
{"timestamp":1429701148576,"status":415,"error":"Unsupported Media Type","exception":"org.springframework.web.HttpMediaTypeNotSupportedException","message":"Unsupported Media Type","path":"/site/alarm"}
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:978) ~[spring-webmvc-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:857) ~[spring-webmvc-4.1.5.RELEASE.jar:4.1.5.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:618) ~[tomcat-embed-core-8.0.20.jar:8.0.20]
...
... /* commented rest of stack out */
...
If I change the client side code to use a RestTemplate as follows:
#Service
public class AlarmService {
#Autowired
private RestTemplate restTemplate;
...
public void send(RaiseSiteAlarmRequestDto alarm) {
RaiseAlarmResponseDto result = restTemplate.postForObject("http://alarm-service/site/alarm",
raiseSiteAlarmRequestDto, RaiseAlarmResponseDto.class);
}
}
It works with the RestTemplate, the alarm-service receives the message and processes it successfully. The message headers sent by the RestTemplate are:
Accept: application/json, application/*+json
Content-Type: application/json;charset=UTF-8
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.7.0_60
Host: smit005s-MacBook-Pro.local:9120
Connection: keep-alive
Content-Length: 323
The answer was to do as #spencergibb suggests; use the consumes directive in the #RequestMapping annotation on the FeignClient interface. This Spring/Netflix documentaition also has an example.
So for example the #FeignClient interface declaration in the client is now:
#FeignClient("alarm-service")
public interface AlarmFeignService {
#RequestMapping(method = RequestMethod.POST, value = "/site/alarm", consumes = "application/json"))
RaiseAlarmResponseDto raiseAlarm(RaiseSiteAlarmRequestDto requestDto);
}
Note this is only necessary on the client side and the server side controller does not need to have this change.
Would be nice if this was done by default on the #FeignClient and then it would be the consistent with RestTemplate and the server side controller #RequestMapping annotation. Maybe that can be done in a future release of spring-cloud.

Resources