The context
I am currently working on an educational project. This implies two Spring Boot REST servers. One is an actual server, which does some processing.
The one I'm interested in is the other. It is a proxy which will redirect all calls to the first one. So that when I call http://localhost:8080/foo, my proxy server will in turn call http://localhost:8090/foo. And if the first server returns A, the proxy will return {"proxied": A, "someInformationAboutThisCall": B}.
I managed to get to this point with some probably inelegant but functioning code of which I give an excerpt below. The key here is that I use #RequestMapping("**") to achieve this. The next step is to design an interface that will make my additional information immediately legible, which is basically the point of this project. If I remove all #RequestMapping("**"), it works just fine.
The question
Now my problem is the following: having used #RequestMapping("**"), I cannot serve static content (the calls get redirect to the other REST server, which does not serve static content). How could I configure Spring Boot/Spring MVC to ignore resources available as static content when mapping the requests, or make the PathResourceResolver prioritary over my controller?` Or should I serve my static content from yet another JVM/server?
Thanks in advance for your help!
Edit of interest: while doing some tests, I discovered that the static content is served, with some restrictions, if I use #RequestMapping("*").
/index.html generates an error page (as does more generally any static content directly in public)
/itf/index.html works (as does more generally any file in public/itf or any other subdirectory of public)
/itf does not work: Spring Boot seems unaware of an index file in it. I must specify a full URI, down to the specific file I want to display.
This however does not work at all with #RequestMapping("**"), which I need.
The tentatives
I tried using a WebMvcConfigurerAdapter with an HandlerInterceptorAdapter (found on SO, SO again and many other places on the Internet), but could not start my project anymore because Spring boot then does not find the InterceptorRegistry bean (has there been recent changes in Spring Boot? I'm using the version 1.5.3.RELEASE).
I also tried some anti-matching but not only does it not work, it also feels very very dirty (and this whole project is probably not optimal, so that's saying a lot).
The code samples for the curious
My "proxy" controller
Note: you can suggest better ways to realize this in comments. Please keep in mind that, though I'm always open to enhancement suggestions, this was not my question.
#RestController
public class ProxyController {
#Value("${monitored.url.base}") // "http://localhost:8090"
private String redirectBase;
#RequestMapping(value = "**", method = {RequestMethod.POST, RequestMethod.PUT})
public ProxiedResponse proxifyRequestsWithBody(HttpServletRequest request, #RequestHeader HttpHeaders headers, #RequestBody Object body) throws URISyntaxException {
return proxifyRequest(request, headers, body);
}
#RequestMapping(value = "**")
public ProxiedResponse proxifyRequestsWithoutBody(HttpServletRequest request, #RequestHeader HttpHeaders headers) throws URISyntaxException {
return proxifyRequest(request, headers, null);
}
private ProxiedResponse proxifyRequest(HttpServletRequest request, #RequestHeader HttpHeaders headers, #RequestBody Object body) throws URISyntaxException {
final RequestEntity<Object> requestEntity = convertToRequestEntity(request, headers, body);
// call remote service
final ResponseEntity<Object> proxied = restTemplate.exchange(requestEntity, Object.class);
// Return service result + monitoring information
final ProxiedResponse response = new ProxiedResponse();
response.setProxied(proxied.getBody());
// set additional information
return response;
}
// Won't work properly for POST yet
private <T> RequestEntity<T> convertToRequestEntity(HttpServletRequest request, HttpHeaders headers, T body) throws URISyntaxException {
// Build proxied URL
final StringBuilder redirectUrl = new StringBuilder(redirectBase).append(request.getRequestURI());
final String queryString = request.getQueryString();
if (queryString != null) {
redirectUrl.append("?").append(queryString);
}
// TODO enhancement: transmit headers and request body to make this a real proxy
final HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
return new RequestEntity<>(body, headers, httpMethod, new URI(redirectUrl.toString()));
}
}
My dirty attempt at excluding static resources URLs
#Configuration // adding #EnableWebMvc did not solve the problem
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
private static class StaticResourcesHandlerInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
final String requestURI = request.getRequestURI();
if (requestURI == null || "/".equals(requestURI) || "/index.html".equals(requestURI) || requestURI.startsWith("/assets")) {
return super.preHandle(request, response, null);
}
return super.preHandle(request, response, handler);
}
}
#Autowired
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new StaticResourcesHandlerInterceptor()).addPathPatterns("/**");
}
}
You can split the path into a wild-card, and a named path variable which must match a negative lookahead regular expression.
#RequestMapping("/{variable:(?!static).*}/**")
You can then use #PathVariable String variable as an argument of your controller method to obtain the value of variable if you need to pass it.
(Would rather have written a comment but I have insufficient reputation)
Try to add the #EnableWebMvc annotation to your configuration:
#Configuration
#EnableWebMvc
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
...
}
Related
I am trying to access httpServletRequest inside a component class. I tried it in several ways.
#Component
public class MyService{
#Resource
WebServiceContext wsCtxt;
public void myWebMethod(){
MessageContext msgCtxt = wsCtxt.getMessageContext();
HttpServletRequest req = (
(HttpServletRequest)msgCtxt.get(MessageContext.SERVLET_REQUEST);
String clientIP = req.getRemoteAddr();
}
This didn't work for me. because WebServiceContext is always null. Then I tried same code inside Web service class. Then that code is working. My Requirement it to get HttpServletRequest inside component class. (ultimately What i am trying to do it get client host from request header).
It this possible to do ?. Are there any alternatives for this ?
Method #1
Have you tried passing the request object into your component by passing it in as an argument to your service method, and from your service to your component method?
// in your controller... Spring provides the request object
public String myController(HttpServletRequest request, ...) {
//...
myService.myServiceMethod(request,...);
}
// in your service...
public void myServiceMethod(HttpServletRequest request, ...) {
//...
myComponent.myWebMethod(request,...);
}
// in your component
public String myWebMethod(HttpServletRequest request, ...) {
// use the raw request object
}
Method #2
Also, DispatcherServlet exposes the request object by wrapping it in a ServletRequestAttributes object, which in turn is stored in a ThreadLocal variable. The actual storing takes place in RequestContextHolder and its static methods. You can access it as follows:
public void myWebMethod(){
//...
RequestAttributes reqAttr = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servlReqAttr = (ServletRequestAttributes)reqAttr;
HttpServletRequest req = servlReqAttr.getRequest();
//...
}
Although a little verbose, you can see what's going on.
You could also condense it:
((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
I hope this helps!
I have a basic SpringBoot app. using Spring Initializer, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR file.
I have this controller:
#Controller
#RequestMapping("/deviceevent")
public class DeviceEventController {
#RequestMapping(value={ "/list"}, method = { RequestMethod.GET})
public String deviceeventList() {
return "tdk/deviceEvent/DeviceEventList";
}
}
and this other test class. Tests using Spring's MockMVC framework. This drives an MVC application in a test, as if it was running in a container,
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#WebMvcTest
public class MockMvcTests {
// Pull in the application context created by #ContextConfiguration
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setup() {
// Setup MockMVC to use our Spring Configuration
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
#Test
public void getDeviceEventsTest() throws Exception {
this.mockMvc
.perform(get("/deviceevent/list") //
.accept(MediaType.parseMediaType("text/html;charset=UTF-8")))
.andExpect(status().isOk()) //
.andExpect(model().size(1)) //
.andExpect(forwardedUrl("tdk/deviceEvent/DeviceEventList"));
}
But I got this error in the forwarded URL. I always used this method in JSP, never in Thymeleaf, but I guess that It is the same:
java.lang.AssertionError: Forwarded URL expected:</tdk/deviceEvent/DeviceEventList> but was:<null>
Assuming a standard Thymeleaf/Spring setup, it looks like there is a misunderstanding with what the controller is doing - when the controller returns that string "tdk/deviceEvent/DeviceEventList" it is not forwarding the HTTP request somewhere, but returning a view name.
With a normal Spring-thymeleaf setup, that string corresponds to the name of a thymeleaf view that will be rendered on hitting that endpoint (I assume the controller is just serving a normal webpage - so that path probably corresponds to some file path most likely in src/main/resources - but again, this depends a lot on your spring config) - at this point the HTTP request has not been returned to the user, and Spring is still processing it - and will attempt to render the HTML view before returning to the user.
The forwarded URL is used if Spring is not rendering anything but instead returning a HTTP response to the user to forward them to another URL (which will start a different Spring request-response process) using a 301/302 mechanism.
Note the difference in the following methods:
#RequestMapping( value="/document", method=RequestMethod.GET )
public String newDocumentSettings( Model model ){
model.addAllAttributes( contentManagementService.documentToJson() );
return "pages/document-settings";
}
#RequestMapping( value="/document", method=RequestMethod.POST )
public String createNewDocument( #RequestParam String title, #RequestParam String overview, #RequestParam String tags ){
Document doc = documentService.createDocument( title, overview, tags );
return "redirect:/document/${doc.url}/1?getting-started";
}
The first renders the template at the given filepath, the second returns a redirect command to the browser to make another HTTP request to the given URL.
In any case, the forwardedUrl in your test case is because hte HTTP Response doesn't have a URL to forward to (because its returning the HTML). If you do want forwarding behaviour (e.g. you actually want to complete the response and the browser to make a second HTTP request) then you would likely need to update the controller as per example, however, if you are happy with the rendered html page, then the test is invalid (look at the Thymeleaf testing framework to see how to test templating).
Caveat: This is based on the assumption of default Spring-Boot config - if you have other config whereby that string does result in a forwarded HTTP request then this doesnt apply!
Taking a guess here, but the URL tdk/deviceEvent/DeviceEventList is probably not defined. Try replacing it with the URL associated with your context (edit as necessary):
#Test
public void getDeviceEventsTest() throws Exception {
this.mockMvc
.perform(get("/deviceevent/list")
.accept(MediaType.parseMediaType("text/html;charset=UTF-8")))
.andExpect(status().isOk())
.andExpect(model().size(1))
.andExpect(forwardedUrl("/WEB-INF/tdk/deviceEvent/DeviceEventList.html"));
}
Aside, instead of:
#RequestMapping(value={ "/list"}, method = { RequestMethod.GET})
you can use the shorthand:
#GetMapping("/list")
I am using Zuul to proxy a strange client that sends a body as part of a GET request. There is unfortunately no way I can change the client.
With curl such a request can be sent as:
curl -XGET 'localhost:8765/kibana/index.html' -d' {"key": "value"}'
And the data is really sent in the body. On zuul side, however, when I try to read the body it is empty. Here is my prototype zuul code:
#Configuration
#ComponentScan
#EnableAutoConfiguration
#Controller
#EnableZuulProxy
public class ZuulServerApplication {
#Bean
public ZuulFilter myFilter() {
return new ZuulFilter(){
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request=(HttpServletRequest)ctx.getRequest();
try {
InputStream is=request.getInputStream();
String content=IOUtils.toString(is);
System.out.println("Request content:"+content);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public int filterOrder() {
return 10;
}
#Override
public String filterType() {
return "pre";
}};
}
public static void main(String[] args) {
new SpringApplicationBuilder(ZuulServerApplication.class).web(true).run(args);
}
}
If I send a POST request, the this code prints the request body without problem. However, if I send the above GET request, the body is not printed. Anything I can do to actually get the body sent as part of a GET request?
It seems that some underlying machinery[0], e.g. some built-in Zuul filter with lesser filter order, replaces default "raw" HttpServletRequest with HttpServletRequestWrapper which, under standard circumstances (i.e. not GET method with body), is able to handle multiple acquisition of input stream. But in the case of GET method with body HttpServletRequestWrapper seems to not proxy input stream at all.
Thus solution could be to change filterOrder e.g. to -10.
Then it works for the filter since HttpServletRequest is used - the mentioned machinery did not get to its turn and thus didn't replace HttpServletRequest with HttpServletRequestWrapper yet. But potential issue with this solution is that the filter might exhaust input stream for something else, e.g. filter with higher filter order. But since GET with body is not a good practice anyway, it might be good enough solution after all :)
[0] I've debug into this longer time ago, but did not get to exact point - thus vague definition of "the machinery".
#RequestMapping(value = "/servers/{domain}", method = RequestMethod.GET)
public Server getMailServer(#PathVariable("domain") String domain)
Server server = null;
try {
server = getServerByDomain(domain);
}
catch(Exception e){
}
return server;
}
When I call "http://localhost:8080/server/hotmail.com" with HttpClient Get method,the value of variable domain is "hotmail", not "hotmail.com".And I got the error:
HttpMediaTypeNotAcceptableException: Could not find acceptable representation.
But if I call "http://localhost:8080/server/hotmail", it works well.
I hope someone can see what is causing this issue.
This might be the same issue I had (and also this guy: https://stick2code.blogspot.co.at/2014/03/solved-orgspringframeworkwebhttpmediaty.html)
My service offers operations on files, i.e., /files/check/foo.txt
I always got a HttpMediaTypeNotAcceptableException and my rest handler was never actually called.
The problem is, that Spring has a feature where it tries to detect the requested content type by the path extension. So .com could mean a COM File. (See http://docs.spring.io/spring/docs/4.3.3.RELEASE/spring-framework-reference/htmlsingle/#mvc-config-content-negotiation and http://docs.spring.io/spring/docs/4.3.3.RELEASE/spring-framework-reference/htmlsingle/#mvc-config-path-matching)
My minimal #Config fix:
#Configuration
#EnableWebMvc
public class PathDispatchConfig extends WebMvcConfigurerAdapter {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
configurer.setUseRegisteredSuffixPatternMatch(false);
}
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
}
}
Change your #RequestMapping to following:)
#RequestMapping(value = "/servers/{domain:.+}", method = RequestMethod.GET)
I had the same problem because I had usernames (=mail adresses) in my request url. Thanks to the post of Benjamin Maurer I found the problem. However, I fixed it in a different way because I was unsure of the consequences regarding security of his answer.
I added the usernames as request parameters in the request. In your case, this would look something like:
#RequestMapping(value = "/servers", method = RequestMethod.GET)
public Server getMailServer(#RequestParam("domain") String domain) {
// do stuff
}
The request url would then look something like:
http://localhost:8080/server?domain=hotmail.com
I have a simple controller test that looks like this
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = CuponzaApiApplication.class)
#WebAppConfiguration
public class UserControllerTest {
private MockMvc mockMvc;
#Autowired
protected WebApplicationContext wac;
#Autowired
UserRepository userRepository;
#Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
#Test
public void createUser() throws Exception{
CuponzaUser user = new CuponzaUser("some#test.com", "firstName", "lastName");
ObjectWriter jackson = new ObjectMapper().writer().withDefaultPrettyPrinter();
mockMvc.perform(post("/user/add").content(jackson.writeValueAsString(user)).contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"));
}
however it fails saying the following
java.lang.AssertionError: Content type not set
here is my controller
#RestController
public class UserController {
#Autowired
UserRepository userRepository;
#RequestMapping(value = "/user/add",method = RequestMethod.POST,produces={MediaType.APPLICATION_JSON_VALUE})
public void AddUser(#RequestBody CuponzaUser user, HttpServletResponse response){
if(user ==null){
response.setStatus(HttpStatus.BAD_REQUEST.value());
return;
}else{
user.setCreationDate(new Date());
user.setLastSeenDate(new Date());
userRepository.save(user);
//response.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE);
return;
}
}
I dont want to manually add the content type header for each response , and i thought that the "produces" annotation should take care of this
any ideas?
This tends to be a little confusing - produces parameter of a #RequestMapping annotation does not really modify the response header, it is a way to narrow down the appropriate handler method based on the Accept header that the user has specified. Think of it this way, #RequestMapping and all the parameters associated with it is just a way to filter down to the appropriate method for Spring MVC to call.
The MessageConverter responsible for converting the responses to the appropriate media type does plug in the response Content-Type header, I think the issue in your case is because you are not setting the Accept header in your mock test - .accept(MediaType.APPLICATION_JSON)
The issue is that you're not returning anything. Your response body is empty.
In a way it makes sense, there is no content, what would be the point in defining a Content-Type? Setting the Accept header also won't get you anywhere. Furthermore, you should be able to reproduce this same behaviour outside of your unit tests too, i.e. it's not an issue with your unit test/mock setup.
You could either:
return some content
consider returning a 204 (No Content), if you really don't want to return anything (still wouldn't give you a Content-Type header but it would make clear that there is no content)
add the header manually as in the workaround commented out in your question