Spring Boot with redirecting with single page angular2
Asked Answered
D

13

33

I have a single page Angular app with Spring Boot. It looks like the following:

src
  main
  java
    controller
       HomeController
       CustomerController
       OtherController
  webapp
    js/angular-files.js
    index.html

Spring boot correctly defaults to webapp folder and serves index.html file.

What I am looking to do is:

  1. For every local REST request not starting with /api overwrite and redirect to default webapp/index.html. I plan to serve anything /api to the spring controllers.

  2. Is there a way to prefix all controllers with API so that I do not have to write API every time? e.g.

    @RequestMapping("/api/home") can write shorthand in code @RequestMapping("/home")

or

@RequestMapping("/api/other-controller/:id") can write shorthand  @RequestMapping("/other-controller/:id")

I'm looking for every API request, e.g. 1) http://localhost:8080/api/home keep API with API and resolve to correct controller and return JSON, however if someone enters a URL like http:///localhost/some-url or http:///localhost/some-other/123/url then it will serve the index.html page and keep the URL.

enter image description here

Alternative ways to do it: try adding #ErrorViewResolver: Springboot/Angular2 - How to handle HTML5 urls?

Dwelling answered 11/5, 2017 at 11:12 Comment(3)
you can have a look into this, mkyong.com/spring-boot/spring-boot-how-to-change-context-pathKnighterrant
You can try creating a custom annotation which will include @RequestMapping("/api") and apply that to your api controllers. Then use @RequestMapping on methods for specific urls.Tiffie
Probably there is an additional requirement in this question: index.html will probably have references to js and css files which, although not being "index.html", should not be handled as /api/** requestsVenusian
L
25

For every local REST request not starting with /api overwrite and redirect to default webapp/index.html. I plan to serve anything /api to the spring controllers.

Update 15/05/2017

Let me re-phrase your query for other readers. (Correct me, if misunderstood)

Background
Using Spring Boot and Serving static resources from classpath

Requirement
All 404 non api requests should be redirected to index.html.

NON API - means Requests in which URL doesn't start with /api.
API - 404 should throw 404 as usual.

Sample Response
/api/something - will throw 404
/index.html - will server index.html
/something - will redirect to index.html

My Solution

Let the Spring MVC throw exceptions, if any handler is not available for the given resource.

Add following to application.properties

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Add a ControllerAdvice as follows

@ControllerAdvice
public class RedirectOnResourceNotFoundException {

    @ExceptionHandler(value = NoHandlerFoundException.class)
    public Object handleStaticResourceNotFound(final NoHandlerFoundException ex, HttpServletRequest req, RedirectAttributes redirectAttributes) {
        if (req.getRequestURI().startsWith("/api"))
            return this.getApiResourceNotFoundBody(ex, req);
        else {
            redirectAttributes.addFlashAttribute("errorMessage", "My Custom error message");
            return "redirect:/index.html";
        }
    }

    private ResponseEntity<String> getApiResourceNotFoundBody(NoHandlerFoundException ex, HttpServletRequest req) {
        return new ResponseEntity<>("Not Found !!", HttpStatus.NOT_FOUND);
    }
}

You can customize the error message as you like.

Is there a way to prefix all controllers with api so that I do not have to write api every time.

For this, you can create a BaseController and set the RequestMapping path to /api

Example

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api")
public abstract class BaseController {}

And extend this BaseController and make sure you do not annotate child class with @RequestMapping

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FirstTestController extends BaseController {
    @RequestMapping(path = "/something")
    public String sayHello() {
        return "Hello World !!";
    }

}

Previous Answer

You can create a Filter which redirects to /index.html if request path doesn't startsWith /api.

// CODE REMOVED. Check Edit History If you want.
Lorenelorens answered 13/5, 2017 at 14:35 Comment(8)
This looks close to what I am looking for. However it has stopped serving the static content inside webapp? returns ERR_TOO_MANY_REDIRECTSDwelling
Too many redirects can be due to your Spring Security configuration. Can u share TRACE logs ?Lorenelorens
I have spring security disabled.. I added logging on doFilter method and it keeps going back to itself.. Its seems to me that it has disabled some of the default spring boot behaviour that resolved to /webapp/index.html.. its stuck in a loop.Dwelling
I have added trace here. You can see the passed one (before adding redirect filter) and the failed one. plnkr.co/edit/McziX5tI179dHiynSw1t?p=previewDwelling
Ohh !! my bad. For your work, you don't even need a filter. Basically, you want to redirect all 404 to index.html. Rest of the requests will be managed by Spring Resource Handling Mapper. If somebody hits /api/1 - corresponding it will be matched with /api/** and the corresponding controller will be called. On the other hand, if someone hits /xxx and it is not available in your static resources, the servlet will throw 404.Lorenelorens
I have updated my answer. Please check and revert back.Lorenelorens
Almost but not quite. Spring complains about mapping to "/index.html" not foundBrazen
Thank you for great info this helps alot but I am getting error of - page not redirecting properlyGeminate
P
52

If you're tired of trying to solve this problem by following so many conflicting solutions - look here!!

After hours upon hours trying to follow all the scattered advice from dozens of stack overflow and blog posts, I've finally found the minimum PURE spring boot + angular 6 application to always redirect to index.html after a refresh on a non-root page WHILE maintaining all your REST API endpoint paths. No @EnableWebMvc, no @ControllerAdvice, no changes to application.properties, no custom ResourceHandlerRegistry modifications, just simplicity:

Very important pre-requisite

You *must* include the output of ng build into Spring's resources/static folder. You can accomplish this via the maven-resources-plugin. Learn here: Copying multiple resource directories to independent target directories with maven

Code

@Controller
@SpringBootApplication
public class MyApp implements ErrorController {

    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
    }

    private static final String PATH = "/error";

    @RequestMapping(value = PATH)
    public String error() {
        return "forward:/index.html";
    }

    @Override
    public String getErrorPath() {
        return PATH;
    }
}

Reasoning

  • Including the output of ng-build into resources/static at build time allows spring view redirects ("forward:/index.html") to succeed. It seems spring cannot redirect to anything outside of the resources folder so if you're trying to access pages at the root of the site, it won't work.
  • With default functionality (i.e. no additions of @EnableWebMvc or changes to application.properties) navigating to / automatically serves the index.html (iff it was included in the resources/static folder) so no need to make changes there.
  • With default functionality (as stated above), any error encountered in a spring boot app routes to /error and implementing ErrorController overrides that behavior to - you guessed it - route to index.html which allows Angular to take over the routing.

Remarks

Pisa answered 1/11, 2018 at 15:46 Comment(11)
Note also that this needs to be annotated with @Controller. @RestController will not work.Cither
Are you god? wow... I expend so much time!! thank you very much bro!Sprinkler
Is there a way to do it with RestController ?Tamatave
Just did it exactly as explained and it works perfectly fine. Thank you very much.Tamatave
@DanOrtega you could use use @Controller and include a @ResponseBody on the methods you want to have a response body? Because @RestController apparently just adds @Controller and @ResponseBody to the class from what I've read online anyway. :)Halting
If you look in the console you'll notice an error response code with every page request, the solution is to add a @ResponseStatus(HttpStatus.OK) to the error() method. Take care, brother. :)Halting
Thank you buddy.Tamatave
Another thing to keep in mind, you can configure the resources/static at runtime. giving you the same result in the end spring.resources.static-locations: file:/var/lib/myApp/webRootGazzo
Thanks for the suggestions. Worked like a charm. Just an addition, I did not add my static files to resources/static directory. Instead I added my custom directory under resources ==> resources/customDir and added a configuration in application.properties ==> spring.web.resources.static-locations=classpath:/customDir/. Rest worked as same.Acantho
ErrorController does not have any method called getErrorPath, not sure which Error controller you are referring toSuilmann
So I have an issue where my application throws an error on a POST method. When it calls my MyApp.error() function it fails when redirecting to "forward:/index.html" with a 405 Request method 'POST' not supported. It works when an error is called during a GET method. I'm not sure how to get this index.html to accept the POST call, as I don't really want it to do anything except allow the application to continue running as it was (just like what happens with a GET). How do I solve this?Federicofedirko
L
25

For every local REST request not starting with /api overwrite and redirect to default webapp/index.html. I plan to serve anything /api to the spring controllers.

Update 15/05/2017

Let me re-phrase your query for other readers. (Correct me, if misunderstood)

Background
Using Spring Boot and Serving static resources from classpath

Requirement
All 404 non api requests should be redirected to index.html.

NON API - means Requests in which URL doesn't start with /api.
API - 404 should throw 404 as usual.

Sample Response
/api/something - will throw 404
/index.html - will server index.html
/something - will redirect to index.html

My Solution

Let the Spring MVC throw exceptions, if any handler is not available for the given resource.

Add following to application.properties

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

Add a ControllerAdvice as follows

@ControllerAdvice
public class RedirectOnResourceNotFoundException {

    @ExceptionHandler(value = NoHandlerFoundException.class)
    public Object handleStaticResourceNotFound(final NoHandlerFoundException ex, HttpServletRequest req, RedirectAttributes redirectAttributes) {
        if (req.getRequestURI().startsWith("/api"))
            return this.getApiResourceNotFoundBody(ex, req);
        else {
            redirectAttributes.addFlashAttribute("errorMessage", "My Custom error message");
            return "redirect:/index.html";
        }
    }

    private ResponseEntity<String> getApiResourceNotFoundBody(NoHandlerFoundException ex, HttpServletRequest req) {
        return new ResponseEntity<>("Not Found !!", HttpStatus.NOT_FOUND);
    }
}

You can customize the error message as you like.

Is there a way to prefix all controllers with api so that I do not have to write api every time.

For this, you can create a BaseController and set the RequestMapping path to /api

Example

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api")
public abstract class BaseController {}

And extend this BaseController and make sure you do not annotate child class with @RequestMapping

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FirstTestController extends BaseController {
    @RequestMapping(path = "/something")
    public String sayHello() {
        return "Hello World !!";
    }

}

Previous Answer

You can create a Filter which redirects to /index.html if request path doesn't startsWith /api.

// CODE REMOVED. Check Edit History If you want.
Lorenelorens answered 13/5, 2017 at 14:35 Comment(8)
This looks close to what I am looking for. However it has stopped serving the static content inside webapp? returns ERR_TOO_MANY_REDIRECTSDwelling
Too many redirects can be due to your Spring Security configuration. Can u share TRACE logs ?Lorenelorens
I have spring security disabled.. I added logging on doFilter method and it keeps going back to itself.. Its seems to me that it has disabled some of the default spring boot behaviour that resolved to /webapp/index.html.. its stuck in a loop.Dwelling
I have added trace here. You can see the passed one (before adding redirect filter) and the failed one. plnkr.co/edit/McziX5tI179dHiynSw1t?p=previewDwelling
Ohh !! my bad. For your work, you don't even need a filter. Basically, you want to redirect all 404 to index.html. Rest of the requests will be managed by Spring Resource Handling Mapper. If somebody hits /api/1 - corresponding it will be matched with /api/** and the corresponding controller will be called. On the other hand, if someone hits /xxx and it is not available in your static resources, the servlet will throw 404.Lorenelorens
I have updated my answer. Please check and revert back.Lorenelorens
Almost but not quite. Spring complains about mapping to "/index.html" not foundBrazen
Thank you for great info this helps alot but I am getting error of - page not redirecting properlyGeminate
P
23

Try this instead

@SpringBootApplication
@Controller
class YourSpringBootApp { 

    // Match everything without a suffix (so not a static resource)
    @RequestMapping(value = "/**/{path:[^.]*}")       
    public String redirect() {
        // Forward to home page so that route is preserved.(i.e forward:/intex.html)
        return "forward:/";
    }
}
Pren answered 11/5, 2017 at 16:45 Comment(8)
what does the regex "/{[path:[^\\.]*}" match against?.. looks like it matches anything and forwards it to / ... how would this not capture the /api requests?Dwelling
Match everything without a suffix (so not a static resource)Pren
answser to @Dwelling : convention: all paths that do not contain a period (and are not explicitly mapped already) are Angular routes, and should forward to the home page. Source spring.io/blog/2015/05/13/…Kitchenette
This works like a charm. Don't need workaround in error handlers! Thanks!Serena
I guess I am doing something wrong. Now all I get is "forward:/" in my browser ^^Broadfaced
@displayname That is because you are using @RestController annotation and therefore "forward:/" is considerered the response body. Try using @Controller instead.Intercommunion
Very nice solution!Pelag
Working well with spring-boot 3.2.1 but you must add spring.mvc.pathmatch.matching-strategy=ant_path_matcher to you application.yml Also Spring-Security version 6 when you migrating from authorizeRequests to authorizeHttpRequests requires: .dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll(), .requestMatchers(toStaticResources().atCommonLocations()).permitAll() and .requestMatchers("/**").permitAll() or even better: .requestMatchers("/**").permitAll() -> .requestMatchers(regexMatcher("^(?!\\/api).*$")).permitAll() to skip all non API requests.Fierro
R
12
@Controller
public class RedirectController {
    /*
     * Redirects all routes to FrontEnd except: '/', '/index.html', '/api', '/api/**'
     */
    @RequestMapping(value = "{_:^(?!index\\.html|api).*$}")
    public String redirectApi() {
        return "forward:/";
    }
}
Rodeo answered 19/11, 2018 at 14:8 Comment(0)
J
8

Too late on this thread, but thought it might help someone

Tried many solutions, but this looked pretty straight forward and great to me

import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
 
import java.io.IOException;
 
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new PathResourceResolver() {
                    @Override
                    protected Resource getResource(String resourcePath, Resource location) throws IOException {
                        Resource requestedResource = location.createRelative(resourcePath);
 
                        return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                                : new ClassPathResource("/static/index.html");
                    }
                });
    }
}

Credits: https://keepgrowing.in/java/springboot/make-spring-boot-surrender-routing-control-to-angular/

Jewbaiting answered 21/10, 2020 at 18:46 Comment(2)
Thanks so much! This is the one that worked for me. Other solution would not go along with serving static content and handling non existing paths. Also this makes the most sense, because handling /error or Exceptions is already much later in the chain.Gonta
THANKS ! after trying a lot of solutions this one works as expected. Make sense and is simpleHeda
D
2

The solution that works to me is to overwrite the BasicErrorController of Spring Boot:

@Component
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @RequestMapping(produces = "text/html")
    @Override
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NOT_FOUND) {
            return new ModelAndView("forward:/");
        } else {
            return super.errorHtml(request, response);
        }
    }
}

The method errorHtml only intercepts not found requests and is transparent for responses 404 (not found) from the api.

Depalma answered 27/11, 2019 at 14:42 Comment(0)
C
2

Most reasonable solution, imho, for Spring Boot 2+ (code is in Kotlin):

@Component
class ForwardErrorsToIndex : ErrorViewResolver {
   override fun resolveErrorView(request: HttpServletRequest?, 
                              status: HttpStatus?, 
                              model: MutableMap<String, Any>?): ModelAndView {
      return ModelAndView("forward:/index.html")
   }
}
Carbaugh answered 18/4, 2020 at 21:54 Comment(0)
A
1

I don't know why, but the root url "/" would not resolve without adding a little more code. This is what I ended up with.

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;

@EnableWebMvc
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new PathResourceResolver() {
                    @Override
                    protected Resource getResource(String resourcePath, Resource location) throws IOException {
                        Resource requestedResource = location.createRelative(resourcePath);
                        
                        return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                                : new ClassPathResource("/static/index.html");
                    }
                });

        registry.addResourceHandler("/**/*")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new PathResourceResolver() {
                    @Override
                    protected Resource getResource(String resourcePath, Resource location) throws IOException {
                        Resource requestedResource = location.createRelative(resourcePath);
                        
                        return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
                                : new ClassPathResource("/static/index.html");
                    }
                });
    }
}
Abandon answered 27/8, 2022 at 4:45 Comment(1)
This is because with @EnableWebMvc, the WebMvcAutoConfiguration is not used anymore, which handles the forward of / to /index.html. If you remove @EnableWebMvc, your resolver is called and / should still be working.Gonta
P
0

For whole application, you can add context path in application.properties

server.contextPath=/api

It will append "/api" to every requested URL after http://localhost:8080/api/home

For Redirection,

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addRedirectViewController("/", "/home");
    registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
    super.addViewControllers(registry);
}

Put this bunch of code in WebMVCConfig.java

Prakrit answered 11/5, 2017 at 12:22 Comment(3)
thanks for the feedback. it fixes half the issue... I still want anything not api to redirect and load the index.html.Dwelling
I edited my answer for redirection i hope it will work for youPrakrit
not sure if that example excludes api and keep them going to controller? I have updated question to explain further.Dwelling
E
0

In the @Configuration bean you can add a ServletRegistrationBean to make the spring server for the /api/* resquest only, then in the Controller you don't need to add it.

@Bean
public ServletRegistrationBean dispatcherRegistration() {
    ServletRegistrationBean registration = new ServletRegistrationBean(
            dispatcherServlet());
    registration.addUrlMappings("/api/*");
    registration.setLoadOnStartup(1);
    registration.setName("mvc-dispatcher");
    return registration;
}
Efflux answered 16/5, 2017 at 6:16 Comment(0)
P
0

If you don't want to describe resources manually (WebMvcConfigurer#addResourceHandlers). And you don't want users of your REST API to be shocked by getting index.html. Also you always try to follow the minimum customizations of Spring Boot to keep as much of OOTB features as possible.

import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.ModelAndView;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;

@Component
public class CustomErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request,
                                         HttpStatus status,
                                         Map<String, Object> model) {
        String path = String.valueOf(model.get("path"));

        if (path.startsWith("/api")) {
            String error = String.valueOf(model.get("error"));

            throw new ResponseStatusException(status, error);
        }

        return new ModelAndView("index.html", model, status);
    }
}

Please note that default implementation

package org.springframework.boot.autoconfigure.web.servlet.error;

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class) // <-- will be excluded.
DefaultErrorViewResolver conventionErrorViewResolver() {
    return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
Prady answered 5/4, 2023 at 19:8 Comment(0)
H
-1

Ok, let's start with the simple part of your question:

Is there a way to prefix all controllers with api so that I do not have to write api every time?

The answer is yes, just mark your controller with a "global" @RequestMapping annotation, for example:

@RestController
@RequestMapping("/api")
public class ApiController{

   @RequestMapping("/hello") 
   public String hello(){
      return "hello simple controller";
   }

   @RequestMapping("/hello2") 
   public String hello2(){
      return "hello2 simple controller";
   }
}

In the example above you can invoke hello method with this URL: /api/hello

and the second method with this URL: /api/hello2

This is how I didn't have to mark each method with /api prefix.

Now, to the more complex part of your question:

is how to achieve a redirect if the request doesn't start with /api prefix?

You can do it by returning an HTTP status code (302) of Redirect, after all, angularJs "speaks" REST natively, thus you can't force a redirect from Java/Spring code like you use to.

Then just return an HTTP message with the status code of 302, and on your angularJS do the actual redirection.

For example:

On AngularJS:

var headers = {'Content-Type':'application/json', 'Accept':'application/json'}

var config = {
    method:'GET'
    url:'http://localhost:8080/hello',
    headers:headers
};

http(config).then(
    function onSuccess(response){
        if(response.status == 302){
            console.log("Redirect");
            $location("/")
        }
}, function onError(response){
    console.log("An error occured while trying to open a new game room...");
});

On Spring:

@RestController
@RequestMapping("/api")
public class ApiController{

   @RequestMapping("/hello") 
   public ResponseEntity<String> hello(){
      HttpHeaders header = new HttpHeaders();
      header.add("Content-Type", "application/json");
      return new ResponseEntity<String>("", header, HttpStatus.FOUND);
   }
}

of course, you'll need to custom it to your project.

Haystack answered 14/5, 2017 at 1:36 Comment(0)
B
-2

All you need to try is put the index.html to src/main/resources/static/

See Example: https://github.com/reflexdemon/shop/tree/master/src/main/resources/static

In my package.josn I try to copy it to this location.

See PackageJSON: https://github.com/reflexdemon/shop/blob/master/package.json#L14

Barthel answered 14/5, 2017 at 1:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.