Springboot/Angular2 - How to handle HTML5 urls?
Asked Answered
U

9

53

I believe this is a simple question, but I couldn't find an answer or at least use the correct terms in the search.

I am setting up Angular2 and Springboot together. By default, Angular will use paths like localhost:8080\dashboard and localhost:8080\dashboard\detail.

I'd like to avoid using path as hashs, if possible. As Angular documentation states:

The router's provideRouter function sets the LocationStrategy to the PathLocationStrategy, making it the default strategy. We can switch to the HashLocationStrategy with an override during the bootstrapping process if we prefer it.

And then...

Almost all Angular 2 projects should use the default HTML 5 style. It produces URLs that are easier for users to understand. And it preserves the option to do server-side rendering later.

The issue is that when I try to access localhost:8080\dashboard, Spring will look for some controller mapping to this path, which it won't have.

Whitelabel Error Page
There was an unexpected error (type=Not Found, status=404).
No message available

I thought initially to make all my services to be under localhost:8080\api and all my static under localhost:8080\app. But how do I tell Spring to ignore requests to this app path?

Is there a better solution with either Angular2 or Boot?

Utah answered 22/7, 2016 at 1:5 Comment(3)
Your angular route should look like localhost:8080\#dashboard and localhost:8080\#dashboard\detailHeavensent
hi @tashi, I'd like to avoid using hashes if possible... I updated the topic to reflect this.. I didn't make it clear in the first time..Utah
no just use the html styleTransponder
T
58

I have a solution for you, you can add a ViewController to forward requests to Angular from Spring boot.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ViewController {

@RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" })
   public String index() {
       return "forward:/index.html";
   }
}

here I have redirected all my angular2 ("/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers") to my SPA You can do the same for your preject and you can also redirect your 404 error page to the index page as a further step. Enjoy!

Transponder answered 4/8, 2016 at 22:2 Comment(12)
if i use this method, i always get a full page refresh :(Crowned
@Crowned no you should not get a full page refresh u have another problemTransponder
@Transponder no all right, only get full page refresh if i reload with f5 oder press enter the new url. but thats how it should be. i thought wrong...Crowned
@Transponder you also saved my day! :) but I am wondering if there is a way not to provide all url's to redirect to index.html but just provide some generic solution that whatever I type "wrong" it will also redirect to index.html something like: @RequestMapping({ "/" }) -> but it doesn't work :(Methadone
@Methadone Of course, you have. You even have several ways to do it. IMHO, the most natural is customizing the Spring MVC configuration. https://mcmap.net/q/337136/-springboot-angular2-how-to-handle-html5-urlsCanonical
This answer works. However, I believe this is the BEST ANSWER: #38517167 Thank you @CanonicalOstracoderm
This working, but I would not recommend this because when you add some new routes, you will have to modify the backend side. So prefer @Canonical answer.Walachia
Looking at David's answer, it routes ALL resource paths to index.html? Doesn't that also route the *.js, *.png, *.ttf, etc, etc to index.html? How would the server pass those resources?Corriveau
@Corriveau it routes to index.html just if the resource can't be resolved. It is delegating to Angular the job to deal with the 404 errors. I think it is a really good approach.Powdery
@Corriveau The problem with that approach is that 404 errors are meant to be dealt with in the server side. If they are not, Angular receives a nonexistent route and it generates a javascript error on browser console.Powdery
@Corriveau For the 404 problem, one can follow this approach: techiediaries.com/…Powdery
it will work only if both Angular + Spring are in the same project no ?Fleming
C
63

In my Spring Boot applications (version 1 and 2), my static resources are at a single place :

src/main/resources/static

static being a folder recognized by Spring Boot to load static resources.

Then the idea is to customize the Spring MVC configuration.
The simpler way is using Spring Java configuration.

I implement WebMvcConfigurer to override addResourceHandlers(). I add in a single ResourceHandler to the current ResourceHandlerRegistry.
The handler is mapped on every request and I specify classpath:/static/ as resource location value (you may of course adding others if required).
I add a custom PathResourceResolver anonymous class to override getResource(String resourcePath, Resource location).
And the rule to return the resource is the following : if the resource exists and is readable (so it is a file), I return it. Otherwise, by default I return the index.html page. Which is the expected behavior to handle HTML 5 urls.

Spring Boot 1.X Application :

Extending org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter is the way.
The class is an adapter of the WebMvcConfigurer interface with empty methods allowing sub-classes to override only the methods they're interested in.

Here is the full code :

import java.io.IOException;

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.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.resource.PathResourceResolver;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

       
    @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");
            }
        });
    }
}

Spring Boot 2.X Application :

org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter was deprecated.
Implementing directly WebMvcConfigurer is the way now as it is still an interface but it has now default methods (made possible by a Java 8 baseline) and can be implemented directly without the need for the adapter.

Here is the full code :

import java.io.IOException;

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;

@Configuration
public class WebMvcConfig 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");
            }
        });
    }
}

EDIT to address some comments :

For those that store their static resources at another location as src/main/resources/static, change the value of the var args parameter of addResourcesLocations() consequently.
For example if you have static resources both in static and in the public folder (no tried) :

  registry.addResourceHandler("/**/*")
    .addResourceLocations("classpath:/static/", "/public")
Canonical answered 20/10, 2017 at 17:13 Comment(8)
Should WebMvcConfig extends WebMvcConfigurerAdapter instead of implementing WebMvcConfigurer since it's an interface?Cothurnus
If you use Spring Boot 1, yes you should use WebMvcConfigurerAdapter . But in Spring Boot 2, it was deprecated, WebMvcConfigurer is still an interface but it has now default methods (made possible by a Java 8 baseline) and can be implemented directly without the need for the adapter.Canonical
I updated to make a clear distinction according to the version.Canonical
I had exclude the my angular app url from security config along with this. All is well except the images part. I had images in assets in agular those are not displaying now. Also I had other static html in public folder which are not working now.Grange
@Grange did you resolve this issue, assets not workingSupreme
@Supreme assets are probably not being loaded because it routes all requests to index.html, including *.js, *.png, *.ttf, etc, etc? (unless I understood wrong)Corriveau
This is the best Solution and should be the accepted answer.Levenson
None of solutions worked for me.. I have upgraded to Spring 2.7 recently and hitting url localhost:8888 doesn’t redirect me to index.html it gives 404 error & warning in spring saying - No mapping for GET /Theall
T
58

I have a solution for you, you can add a ViewController to forward requests to Angular from Spring boot.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ViewController {

@RequestMapping({ "/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers" })
   public String index() {
       return "forward:/index.html";
   }
}

here I have redirected all my angular2 ("/bikes", "/milages", "/gallery", "/tracks", "/tracks/{id:\w+}", "/location", "/about", "/tests","/tests/new","/tests/**","/questions","/answers") to my SPA You can do the same for your preject and you can also redirect your 404 error page to the index page as a further step. Enjoy!

Transponder answered 4/8, 2016 at 22:2 Comment(12)
if i use this method, i always get a full page refresh :(Crowned
@Crowned no you should not get a full page refresh u have another problemTransponder
@Transponder no all right, only get full page refresh if i reload with f5 oder press enter the new url. but thats how it should be. i thought wrong...Crowned
@Transponder you also saved my day! :) but I am wondering if there is a way not to provide all url's to redirect to index.html but just provide some generic solution that whatever I type "wrong" it will also redirect to index.html something like: @RequestMapping({ "/" }) -> but it doesn't work :(Methadone
@Methadone Of course, you have. You even have several ways to do it. IMHO, the most natural is customizing the Spring MVC configuration. https://mcmap.net/q/337136/-springboot-angular2-how-to-handle-html5-urlsCanonical
This answer works. However, I believe this is the BEST ANSWER: #38517167 Thank you @CanonicalOstracoderm
This working, but I would not recommend this because when you add some new routes, you will have to modify the backend side. So prefer @Canonical answer.Walachia
Looking at David's answer, it routes ALL resource paths to index.html? Doesn't that also route the *.js, *.png, *.ttf, etc, etc to index.html? How would the server pass those resources?Corriveau
@Corriveau it routes to index.html just if the resource can't be resolved. It is delegating to Angular the job to deal with the 404 errors. I think it is a really good approach.Powdery
@Corriveau The problem with that approach is that 404 errors are meant to be dealt with in the server side. If they are not, Angular receives a nonexistent route and it generates a javascript error on browser console.Powdery
@Corriveau For the 404 problem, one can follow this approach: techiediaries.com/…Powdery
it will work only if both Angular + Spring are in the same project no ?Fleming
L
16

You can forward all not found resources to your main page by providing custom ErrorViewResolver. All you need to do is to add this to your @Configuration class:

@Bean
ErrorViewResolver supportPathBasedLocationStrategyWithoutHashes() {
    return new ErrorViewResolver() {
        @Override
        public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
            return status == HttpStatus.NOT_FOUND
                    ? new ModelAndView("index.html", Collections.<String, Object>emptyMap(), HttpStatus.OK)
                    : null;
        }
    };
}
Lanham answered 27/10, 2016 at 9:48 Comment(4)
To add an explanation, ErrorViewResolver is an interface that needs to be implemented by your class having the @Configuration annotation, other then that, this is a good dynamic solution, handling over the responsability for errorhandling to the Angular app, running inside the Spring Boot app.Hailstorm
Since I am using Angular 6, I had to use "index" instead of "index.html".Recce
That's the solution i finally went for. It would be cleaner to return a HttpStatus of type redirect instead of OK though, it makes more sense semantically.Currency
HttpStatus.NOT_FOUND should be used instead of OK.Although
I
11

You can forward everything not mapped to Angular using something like this:

@Controller
public class ForwardController {

    @RequestMapping(value = "/**/{[path:[^\\.]*}")
    public String redirect() {
        // Forward to home page so that route is preserved.
        return "forward:/";
    }
} 

Source: https://mcmap.net/q/340390/-spring-boot-with-angularjs-html5mode

My Spring Boot server for angular is also a gateway server with the API calls to /api to not have a login page in front of the angular pages, you can use something like.

import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

/**
 * This sets up basic authentication for the microservice, it is here to prevent
 * massive screwups, many applications will require more secuity, some will require less
 */

@EnableOAuth2Sso
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .logout().logoutSuccessUrl("/").and()
                .authorizeRequests()
                .antMatchers("/api/**").authenticated()
                .anyRequest().permitAll().and()
                .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
}
Interlace answered 17/1, 2018 at 23:7 Comment(1)
In my case, all I needed was @RequestMapping(value = "/{:[^\\.]*}") in the ForwardControllerDidactic
K
3

To make it more simple you can just implement ErrorPageRegistrar directly..

@Component
public class ErrorPageConfig implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/"));
    }

}

This would forward the requests to index.html.

@Controller
@RequestMapping("/")
public class MainPageController {

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping({ "/" })
    public String forward() {
        return "forward:/";
    }
}
Keavy answered 15/3, 2017 at 19:34 Comment(2)
This solution throws exceptionsGonnella
Please help me with exception stacktraceKeavy
S
2

I did it with a plain old filter:

public class PathLocationStrategyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        if(request instanceof HttpServletRequest) {
            HttpServletRequest servletRequest = (HttpServletRequest) request;

            String uri = servletRequest.getRequestURI();
            String contextPath = servletRequest.getContextPath();
            if(!uri.startsWith(contextPath + "/api") && 
                !uri.startsWith(contextPath + "/assets") &&
                !uri.equals(contextPath) &&
                // only forward if there's no file extension (exclude *.js, *.css etc)
                uri.matches("^([^.]+)$")) {

                RequestDispatcher dispatcher = request.getRequestDispatcher("/");
                dispatcher.forward(request, response);
                return;
            }
        }        

        chain.doFilter(request, response);
    }
}

Then in web.xml:

<web-app>
    <filter>
        <filter-name>PathLocationStrategyFilter</filter-name>
        <filter-class>mypackage.PathLocationStrategyFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>PathLocationStrategyFilter</filter-name>
        <url-pattern>*</url-pattern>
    </filter-mapping>
</web-app>
Starlight answered 7/9, 2018 at 0:26 Comment(0)
S
0

These are the three steps you need to follow:

  1. Implement your own TomcatEmbeddedServletContainerFactory bean and set up the RewriteValve

      import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;  
      ...
      import org.apache.catalina.valves.rewrite.RewriteValve; 
      ... 
    
      @Bean TomcatEmbeddedServletContainerFactory servletContainerFactory() {
        TomcatEmbeddedServletContainerFactory factory = new TomcatEmbeddedServletContainerFactory();
        factory.setPort(8080);
        factory.addContextValves(new RewriteValve());
        return factory;
      }
    
  2. Add a rewrite.conf file to the WEB-INF directory of your application and specify the rewrite rules. Here is an example rewrite.conf content, which I'm using in the angular application to take advantage of the angular's PathLocationStrategy (basicly I just redirect everything to the index.html as we just use spring boot to serve the static web content, otherwise you need to filter your controllers out in the RewriteCond rule):

      RewriteCond %{REQUEST_URI} !^.*\.(bmp|css|gif|htc|html?|ico|jpe?g|js|pdf|png|swf|txt|xml|svg|eot|woff|woff2|ttf|map)$
      RewriteRule ^(.*)$ /index.html [L]
    
  3. Get rid of the useHash (or set it to false) from your routing declarations:

      RouterModule.forRoot(routes)
    

or

      RouterModule.forRoot(routes, {useHash: false})
Seoul answered 8/9, 2017 at 10:23 Comment(1)
My app is a standalone jar with embedded tomcat. Where is the WEB-INF directory suppose to be? I only know about my /src/main/resources/public folder where I put all my angular 4 static htmls.Arevalo
W
0

forward all Angular routing with index.html. Including base href.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ViewController {

@RequestMapping({ "jsa/customer","jsa/customer/{id}",})
   public String index() {
       return "forward:/index.html";
   }
}

In my case jsa is base href.

Washhouse answered 23/2, 2018 at 5:57 Comment(0)
H
0

in my opinion the best way is to separate the User Interface paths and API paths by adding a prefix to them and serve the UI app entrypoint (index.html) for every path that matches UI prefix:

step 1 - add a prefix for all your UI paths (for example /app/page1, /app/page2, /app/page3, /app/page2/section01 and so on).

step 2 - copy UI files (HTML, JS, CSS, ...) into /resources/static/

step 3 - serve index.html for every path that begins with /app/ by a controller like this:

@Controller
public class SPAController {
    @RequestMapping(value = "/app/**", method = RequestMethod.GET)
    public ResponseEntity<String> defaultPath() {
        try {
            // Jar
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("/static/index.html");
            // IDE
            if (inputStream == null) {
                inputStream = this.getClass().getResourceAsStream("/static/index.html");
            }
            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("Error in redirecting to index");
        }
    }

    @GetMapping(value = "/")
    public String home(){
        return "redirect:/app";
    }
}
Hydraulic answered 24/2, 2022 at 16:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.