Spring Boot will serve static resources or my 404 redirection Handler, but not both - spring

I have a Spring Boot app bundled with React, that I am wanting to re-direct all requests that aren't found as static resources back to the index (so React can handle the single-page routing).
Currently, I can serve the static resources or do the 404 redirection, but not both.
Does anyone see any issues with my config?
Static resources are in my Jar at:
classes/static/index.html (etc)
Spring Boot application.yaml config
spring:
main:
allow-bean-definition-overriding: true
mvc:
throw-exception-if-no-handler-found: true
static-path-pattern: /**
resources:
static-locations: classpath:/static
NotFoundResourceHandler.java:
#ControllerAdvice
public class NotFoundHandler {
#ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<String> renderDefaultPage() {
try {
InputStream inputStream = new ClassPathResource("/static/index.html").getInputStream();
String body = StreamUtils.copyToString(inputStream, Charset.defaultCharset());
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(body);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("There was an error completing the action.");
}
}
}
The above configuration will serve the static resources fine. I've also been able to get it to use the 404 Handler (when I was serving the resources from /, and some other configurations) - but in that case it was using that for every resource, including the JS/CSS/Media files).
Thanks for any help you can provide!
Update:
It's now resolved. I now understand that it's not working because it is mapping every request (/) to the static handler, so that is handling all 404's. I got it to work by mapping the static handler to a specific path (/subdirectory/) and the 404 handler should be thrown for any requests outside of that /subdirectory path (/brokenpath should cause the 404 handler to fire) - which I then set the 404 handler to redirect to /subdirectory/index.html.

Update: It's now resolved. I now understand that it's not working because it is mapping every request (/) to the static handler, so that is handling all 404's. I got it to work by mapping the static handler to a specific path (/subdirectory/) and the 404 handler should be thrown for any requests outside of that /subdirectory path (/brokenpath should cause the 404 handler to fire) - which I then set the 404 handler to redirect to /subdirectory/index.html.

Related

Spring MaxUploadSizeExceededException not handled ExceptionHandler

I wanna handling MaxUploadSizeExceededException.
# application.properties
spring.servlet.multipart.resolve-lazily=true
server.tomcat.max-swallow-size=-1
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=1MB
#RestControllerAdvice
class MyControllerAdvice {
#ExceptionHandler(MaxUploadSizeExceededException.class)
public MyDTO myHandler(MaxUploadSizeExceededException ex) {
log.error("ERROR!");
return MyDTO.build();
}
}
When I upload file large size, then it caught by application.properties and go to ExceptionHandler. And
swagger test
log is printed
response MyDTO
But web page's request
log is printed
but in my Chrome console, I cant see MyDTO
Chrome Network tab display failed to response data no data found for resource with given identifier
Why it doesn't work?

Swagger ui adds an additional /path in the testing page when behind zuul

I have several microservices behind a spring boot application embedding zuul, let's call it "gateway".
Here's my config file:
zuul:
prefix: /api
routes:
api-login:
path: /login/**
url: http://localhost:8070/the-login/
api-other:
...
I want to show the documentation for every service of mine in my gateway application so I created the following:
#Component
#Primary
#EnableAutoConfiguration
public class SwaggerDocumentationController implements SwaggerResourcesProvider{
#Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<SwaggerResource>();
resources.add(swaggerResource("login-service", "/api/login/api-docs", "2.0"));
...
return resources;
}
private SwaggerResource swaggerResource(String name, String location, String version) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion(version);
return swaggerResource;
}
}
It works pretty much fine: each time the user goes to /gateway/api/login it gets redirected to my microservice, so to /the-login/. Also, when the user goes to /gateway/login/swagger-ui.html they can see the documentation.
The problem is that when the user tries an api from the swagger ui documentation page they get:
{
"timestamp": "2018-05-12T15:35:38.840+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/the-login/the-login/LoginTest"
}
As you can see zuul added one more /the-login to the path!
Is this a swagger-ui bug or am I getting something wrong?
You are using the-login as context path in api-login service. So when you load gateway service swagger, its add the-login into swagger base url as well, like this.
Thats the reason the-login is getting appended twice (one from base-url and one from config). This might be a bug in swagger ui.
A workaround I tried and it worked
Remove /the-login from application.yml
zuul:
prefix: /api
routes:
api-login:
path: /login/**
url: http://localhost:8070/
api-other:
Now swagger resource will not load, so need to modify swagger resource path.
Add the-login in swagger resources
resources.add(swaggerResource("login-service", "/api/login/the-login/api-docs", "2.0"));

How to filter request based on domain in spring-mvc

I have one war file for my application and I will be using 2 domains to access it. For example I want to access admin.jsp using admin.mydomain.com/adminpage and other jsp pages I want to access with local.mydomain.com.
Also, admin.jsp should be only accessible via admin.mydomain.com and not via local.mydomain.com. How to do this in spring-security / spring-mvc? Is there a support in spring framework for this?
Any help on this would be helpful. Thanks.
You can implement RequestMatcher, and maybe like
HostOnlyRequestMatch(String relativePath, String hostname)
and then override the boolean matches(HttpServletRequest request) method, and if the relativePath and hostname are same with request, return true.
Add the requestMatcher to http like this:
http
.authorizeRequests()
.requestMatcher(new HostOnlyRequestMatch("/admin", "admin.mydomain.com")).permitAll()
.antMatchers("/admin").denyAll();
One way would be to configure proxy (e.g. Nginx) to route your requests to your application server (e.g Tomcat) properly. Read here for more details https://www.nginx.com/resources/admin-guide/reverse-proxy/
You can get the requested url from request object in you mvc controller and if it is not form correct domain then you can throw or show proper error based on your project. Following is the code snippet
#Controller
#RequestMapping("/adminpage")
public class AdminPageController{
#RequestMapping(method = RequestMethod.GET)
public String getAdminPage(HttpServletRequest request) {
String url = request.getRequestURL().toString();
if(!url.contains("admin.mydomain.com")) {
throw RuntimeException("Not accessible through this domain.");
// You can implement your own logic of showing error here
}
}
}

Spring Boot Web app - Redirect controller

I need to have a controller (or other component) that handles all 404 errors and smartly redirects to the correct page (this is based on a table src/ target). I have found a few questions about handling thrown exceptions FROM controllers so I have made the following:
#ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Throwable.class)
#ResponseBody
ResponseEntity<String> handleControllerException(HttpServletRequest req, Throwable ex) {
String slug = req.getRequestURI(); // this is the URL that was not found
URI location=null;
try {
// lookup based on slug ...
location = new URI("http://cnn.com"); // let's say I want to redirect to cnn
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setLocation(location);
return new ResponseEntity<String>("Page has permanently moved", responseHeaders, HttpStatus.PERMANENT_REDIRECT);
}
}
I have not made any other configuration changes
The two issues with this are:
It only catches exceptions thrown by my other controllers and not 404 errors
It catches ALL types of exceptions and not just 404
Any ideas on how to implement a kind of "catch-all"?
This is probably not the best Spring boot solution, but maybe a similar idea can be extrapolated from it and applied to your application. I had a similar problem when working with Spring 3.x, and here's the solution I came up with.
My scenario was that I had multiple servlets running under the same application context. One of those handled a web interface, and another one handled API calls. I wanted the servlet handling the API calls to handle 404s differently than the UI servlet.
I created a new class which extended DispatcherServlet, and overridden the noHandlerFound method. I wired my new class in my web.xml in the <servlet>tags instead of the default DispatcherServlet class I had there before.
<servlet>
<servlet-name>api-servlet</servlet-name>
<servlet-class>path.to.your.new.DispatcherServletClass</servlet-class>
...
</servlet>

How to control the web context of Spring Hateoas generated links?

We are building an API and are using Spring RestControllers and Spring HATEOAS.
When the war file is deployed to a container and a GET request is made to http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places, the HATEOAS links look like this:
{
"links" : [ {
"rel" : "self",
"href" : "http://localhost:8080/placesapi-packaged-war-1.0.0-SNAPSHOT/places",
"lastModified" : "292269055-12-02T16:47:04Z"
} ]
}
in that the web context is that of the deployed application (eg: placesapi-packaged-war-1.0.0-SNAPSHOT)
In a real runtime environment (UAT and beyond), the container is likely to be sat behind a http server such as Apache where a virtual host or similar fronts the web application. Something like this:
<VirtualHost Nathans-MacBook-Pro.local>
ServerName Nathans-MacBook-Pro.local
<Proxy *>
AddDefaultCharset Off
Order deny,allow
Allow from all
</Proxy>
ProxyPass / ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/
ProxyPassReverse / ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/
</VirtualHost>
Using the above, when we make a GET request to http://nathans-macbook-pro.local/places, the resultant response looks like this:
{
"links": [ {
"rel": "self",
"href": "http://nathans-macbook-pro.local/placesapi-packaged-war-1.0.0-SNAPSHOT/places",
"lastModified": "292269055-12-02T16:47:04Z"
} ]
}
It's wrong because the link in the response contains the web app context, and if a client were to follow that link they would get a 404
Does anyone know how to control the behaviour of Spring HATEOAS in this respect? Basically I need to be able to control the web context name that it generates within links.
I did a bit of poking around and can see that with a custom header X-Forwarded-Host you can control the host and port, but I couldn't see anything similar to be able to control the context.
Other options we've considered involve either deploying the app to the ROOT context or to a fixed named context, and then set up our virtual host accordingly. However, these feel like compromises rather than solutions because ideally we would like to host several versions of the application on the same container (eg: placesapi-packaged-war-1.0.0-RELEASE, placesapi-packaged-war-1.0.1-RELEASE, placesapi-packaged-war-2.0.0-RELEASE etc) and have the virtual host forward to the correct app based on http request header.
Any thoughts on this would be very much appreciated,
Cheers
Nathan
First, in case you weren't aware, you can control the context of the web application (under Tomcat at least) by creating webapp/META-INF/context.xml containing the line:
<Context path="/" />
... which will make set the application context to be the same as what you are using (/).
However, that wasn't your question. I posed a similar question a little while back. As a result, from what I can gather, there's no out-of-the-box mechanism for controlling the generated links manually. Instead I created my own modified version of ControllerLinkBuilder, which built up the base of the URL using properties defined in application.properties. If setting the context on your application itself is not an option (i.e. if you're running multiple versions under the same Tomcat instance) then I think that this is your only option, if ControllerLinkBuilder is not building up your URLs correctly.
Had a very similar problem. We wanted our public URL to be x.com/store and internally our context path for hosts in a cluster was host/our-api. All the URLS being generated contained x.com/our-api and not x.com/store and were unresolvable from the public dirty internet.
First just a note, the reason we got x.com was because our reverse-proxy does NOT rewrite the HOST header. If it did we'd need to add an X-Forwarded-Host header set to x.com so HATEOAS link builder would generate the correct host. This was specific to our reverse-proxy.
As far as getting the paths to work...we did NOT want to use a custom ControllerLinkBuilder. Instead we rewrite the context in a servlet filter. Before i share that code, i want to bring up the trickiest thing. We wanted our api to generate useable links when going directly to the tomcat nodes hosting the war, thus urls should be host/our-api instead of host/store. In order to do this the reverse-proxy needs to give a hint to the web app that the request came through the reverse-proxy. You can do this with headers, etc. Specifically for us, we could ONLY modify the request url, so we changed our load balancer to rewrite x.com/store to host/our-api/store this extra /store let us know that the request came through the reverse-proxy, and thus needed to be using the public context root. Again you can use another identifier (custom header, presence of X-Forwared-Host, etc) to detect the situation..or you may not care about having individual nodes give back usable URLs (but it's really nice for testing).
public class ContextRewriteFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest req, ServletResponse res, final FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
//There's no cleanup to perform so no need for try/finally
chain.doFilter(new ContextRewriterHttpServletRequestWrapper(request), res);
}
private static class ContextRewriterHttpServletRequestWrapper extends HttpServletRequestWrapper {
//I'm not totally certain storing/caching these once is ok..but i can't think of a situation
//where the data would be changed in the wrapped request
private final String context;
private final String requestURI;
private final String servletPath;
public ContextRewriterHttpServletRequestWrapper(HttpServletRequest request){
super(request);
String originalRequestURI = super.getRequestURI();
//If this came from the load balancer which we know BECAUSE of the our-api/store root, rewrite it to just be from /store which is the public facing context root
if(originalRequestURI.startsWith("/our-api/store")){
requestURI = "/store" + originalRequestURI.substring(25);
}
else {
//otherwise it's just a standard request
requestURI = originalRequestURI;
}
int endOfContext = requestURI.indexOf("/", 1);
//If there's no / after the first one..then the request didn't contain it (ie /store vs /store/)
//in such a case the context is the request is the context so just use that
context = endOfContext == -1 ? requestURI : requestURI.substring(0, endOfContext);
String sPath = super.getServletPath();
//If the servlet path starts with /store then this request came from the load balancer
//so we need to pull out the /store as that's the context root...not part of the servlet path
if(sPath.startsWith("/store")) {
sPath = sPath.substring(6);
}
//I think this complies with the spec
servletPath = StringUtils.isEmpty(sPath) ? "/" : sPath;
}
#Override
public String getContextPath(){
return context;
}
#Override
public String getRequestURI(){
return requestURI;
}
#Override
public String getServletPath(){
return servletPath;
}
}
}
It's a hack, and if anything depends on knowing the REAL context path in the request it will probably error out...but it's been working nicely for us.
ProxyPass /placesapi-packaged-war-1.0.0-SNAPSHOT
ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/
ProxyPassReverse /placesapi-packaged-war-1.0.0-SNAPSHOT
ajp://localhost:8009/placesapi-packaged-war-1.0.0-SNAPSHOT/

Resources