Background: I am the developer of AngularJ Universal - a library that provides server side rendering support for Angular 2+ applications. Because it is quite hard to know all routes of a SPA application, the user can use a custom written Spring Boot starter and define all routes via a property value. The starter then registers endpoints for these routes and handles the matching requests by a custom view resolver/view.

Let's say we have an Angular application that registers the routes /, /home and /about. For reading the initial page on the server side, an index file index.html is placed in src/main/resources/public.

Problem The Spring Boot auto configuration finds a "welcome page" in src/main/resources/public/index.html and configures a WelcomePageHandlerMapping for the endpoint /.

For serving static resources, this is a well known and desired behavior, but I have all my resources in src/main/resources/public because the initial server side rendering process is done on the server side and after serving that to the client, the client code kicks in and accesses resources from the src/main/resources/public directory.

I tried to override this behavior with the following code snippet, but the WelcomePageHandlerMapping is still registered in the end:

public class GenericAdapter implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        ViewControllerRegistration registration = registry.addViewController("/");
        registration.setViewName("/");
    }
}

Sadly that doesn't work: Take a look at the missing / endpoint before /home and at the trailing WelcomePageHandlerMapping endpoint:

2018-03-03 22:51:05.874  INFO 17324 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/home] onto handler of type [class org.springframework.web.servlet.mvc.ParameterizableViewController]
2018-03-03 22:51:05.874  INFO 17324 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/about] onto handler of type [class org.springframework.web.servlet.mvc.ParameterizableViewController]
2018-03-03 22:51:05.887  INFO 17324 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-03 22:51:05.887  INFO 17324 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-03 22:51:05.917  INFO 17324 --- [  restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-03-03 22:51:05.940  INFO 17324 --- [  restartedMain] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [public/index.html]

Solution: So I am looking for a way to disable the WelcomePageHandlerMapping:

  1. It's not a problem of Spring Boot. I should rename my index.html so it is not detected as default welcome page (Would be hard and annoying, but somehow understandable)

  2. Provide a way that ViewControllerRegistration.addViewController can override the WelcomePageHandlerMapping endpoint

  3. Provide a way to disable WelcomePageHandlerMapping at all

Replication: Bellow you will find a minimal working example. Please keep in mind:

  1. Don't forget to create a welcome page in src/main/resources/public/index.html for testing - otherwise you will not be able to reproduce the problem

  2. To provide a minimal working example, I had to "rip out" some code of my Spring Boot starter. The classes/beans GenericAdapter (Including the getGenericAdapter bean), GenericViewResolver and GenericView are located in the Spring Boot starter.

You can find the original starter here: https://github.com/swaechter/angularj-universal/tree/master/angularj-universal-spring-boot-starter

(In case I made some mistakes/abuse an API, please feel free to mention that)

package ch.swaechter.viewtest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.AbstractTemplateView;
import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
import org.springframework.web.servlet.view.AbstractUrlBasedView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;

@SpringBootApplication
public class ViewControllerApplication {

    // Routes loaded by my starter via properties
    private final List<String> routes = Arrays.asList("/", "/home", "/about");

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

    // Comment in the code bellow to override the welcome page - a thing that doesn't work with ViewControllerRegistry.addViewController
    /*@Controller
    @RequestMapping("/")
    public class ContentController {

        @GetMapping("/")
        public ModelAndView overrideWelcomePage() {
            return new ModelAndView("/");
        }
    }*/

    // All code bellow this line belongs to the custom Spring Boot starter and is just here to provide a minimal working example

    // Custom web MVC configurer registered as bean (My Spring Boot starter is loaded via @AutoConfigureAfter(WebMvcAutoConfiguration.class)
    @Bean
    public WebMvcConfigurer getGenericAdapter() {
        return new GenericAdapter();
    }

    /**
     * Add all application routes dynamically
     */
    @Component // Annotation is only here to provide a minimal working example
    public class GenericAdapter implements WebMvcConfigurer {

        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            for (String url : routes) {
                ViewControllerRegistration registration = registry.addViewController(url);
                registration.setViewName(url);
            }
        }
    }

    /**
     * Provide a view resolver that supports our application routes
     */
    @Component  // Annotation is only here to provide a minimal working example
    public class GenericViewResolver extends AbstractTemplateViewResolver {

        public GenericViewResolver() {
            setViewClass(requiredViewClass());
            setOrder(0);
        }

        @Override
        public Class<?> requiredViewClass() {
            return GenericView.class;
        }

        @Override
        public boolean canHandle(String modelname, Locale locale) {
            return routes.contains(modelname);
        }

        @Override
        public AbstractUrlBasedView buildView(String uri) {
            return new GenericView();
        }
    }

    /**
     * Render our application requests and write the result to the HTTP response
     */
    public class GenericView extends AbstractTemplateView {

        @Override
        protected boolean isUrlRequired() {
            return false;
        }

        @Override
        protected void renderMergedTemplateModel(Map<String, Object> map, HttpServletRequest request, HttpServletResponse response) throws Exception {
            // Note: The URI is passed to an Angular application that is server side rendered
            String uri = request.getRequestURI().substring(request.getContextPath().length());
            response.setContentType("text/html");
            PrintWriter writer = response.getWriter();
            writer.println("Rendered SPA application for: " + uri);
            writer.flush();
            writer.close();
        }
    }
}

Comment From: swaechter

CC'ing @bclozel because we had a discussion about that via Gitter

Comment From: swaechter

Hey, @bclozel, any thoughts on this?

Comment From: bclozel

Hi @swaechter

You're right, there is currently no configuration property to disable the welcome page support - as soon as an index.html file is present, this is enabled.

The WelcomePageHandlerMapping is ordered at 0 by default, which explains why it is considered before any registered ViewController. Those are registered under a SimpleUrlHandlerMapping ordered itself at 1.

I think we should change the default order of the WelcomePageHandlerMapping to respect developers' opinions if they provide one about that.

The favicon HandlerMapping is ordered at Ordered.HIGHEST_PRECEDENCE + 1 but I think we should order WelcomePageHandlerMapping at Ordered.LOWEST_PRECEDENCE - 1. Would that work for you?

Comment From: wilkinsona

Reopening as https://github.com/spring-projects/spring-boot/commit/220f8cdca29810dd5c2d6561f66c6d89ab13b8b1 appears to have broken spring-boot-sample-web-static:

[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 4.627 s <<< FAILURE! - in sample.web.staticcontent.SampleWebStaticApplicationTests
[ERROR] testHome(sample.web.staticcontent.SampleWebStaticApplicationTests)  Time elapsed: 0.164 s  <<< FAILURE!
org.junit.ComparisonFailure: expected:<[200]> but was:<[404]>
    at sample.web.staticcontent.SampleWebStaticApplicationTests.testHome(SampleWebStaticApplicationTests.java:48)

Comment From: bclozel

Thanks @wilkinsona - the chosen order is not the right one and collides with the registration order for ResourceHttpRequestHandler. I've moved it to 2, right after the SimpleUrlHandlerMapping for ViewController instances.

Comment From: swaechter

@bclozel Sorry for not answering earlier. Your explanation sounded reasonable and everything works in the latest 2.0.1 release. So thank you very much for the time & change!

Comment From: mmichalowicznuix

@bclozel @swaechter It's now 2023, I'm on spring-boot 2.7.9, I have an integration test against a @Controller with a GetMapping for / which has exclusion logic in it, meaning my business logic should 404, so a priority doesn't work here in my case. I just want to disable that bean altogether but I can't seem to excludeName=welcomePageHandlerMapping. I need a custom route for / without meddling from Spring autoconfigure. I've tried to remove it, but my logic involves creating the correct route through MockMvcBuilders.webAppContextSetup(WebApplicationContext)

    @GetMapping(value = "/", headers = "!SomeHeader")
    public String handleApplicationRoot() {
        return "forward:/index.html";
    }

Comment From: bclozel

@mmichalowicznuix asking a question on a 5 year old issue is not the right way to proceed here. Please create a new issue explaining the expected behavior, what you've tried and what you are seeing instead. Feel free to link to this issue as a reference.