Adding Hilla as a frontend to an existing Sping Boot Rest API

I’m trying to implement a Hilla frontend to my Spring Boot Rest API (Written in Kotlin, Webflux). I have existing API security restrictions with its own JWT security regime.

We already have consumers to the API, and the use-case for the hilla frontend is backoffice functions, to correct data etc…

I’ve tried following several guides online, but i cannot seem to get Hilla working with the existing securityconfig. Here is an example of what i’ve tried. (with vaadin.url-mapping set to “/ui”)

import com.vaadin.flow.spring.security.VaadinWebSecurity  
import org.springframework.context.annotation.Bean  
import org.springframework.context.annotation.Configuration  
import org.springframework.core.annotation.Order  
import org.springframework.http.HttpStatus  
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity  
import org.springframework.security.config.annotation.web.builders.HttpSecurity  
import org.springframework.security.config.http.SessionCreationPolicy  
import org.springframework.security.core.GrantedAuthority  
import org.springframework.security.core.authority.SimpleGrantedAuthority  
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper  
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter  
import org.springframework.security.web.SecurityFilterChain  
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler  
import org.springframework.web.cors.CorsConfiguration  
import org.springframework.web.cors.UrlBasedCorsConfigurationSource  
import java.time.Duration  
import java.util.stream.Collectors  
import java.util.stream.Stream  
  
@Configuration  
@EnableMethodSecurity  
@Order(1)  
class ApiSecurityConfig {  
    @Bean  
    fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {  
        val configuration = CorsConfiguration()  
        configuration.allowedOriginPatterns = listOf("https://*.com*")  
        configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE")  
        configuration.allowedHeaders = listOf("Content-Type")  
        configuration.allowCredentials = true  
        configuration.setMaxAge(Duration.ofDays(1))  
        val source = UrlBasedCorsConfigurationSource()  
        source.registerCorsConfiguration("/**", configuration)  
        return source  
    }  
  
    @Throws(Exception::class)  
    @Bean  
    fun standardFilterChain(http: HttpSecurity): SecurityFilterChain {  
        http.securityMatcher("/api/**")  
            .authorizeHttpRequests { // Authorize special paths  
                it.requestMatchers(  
                    "/deposit/*",  
                    "/actuator/health",  
                    "/actuator/health/*",  
                    "/actuator/info",  
                    "/actuator/metrics",  
                    "/actuator/prometheus",  
                    "/swagger-ui/",  
                    "/swagger-ui/*",  
                    "/swagger-ui.html",  
                    "/v3/api-docs",  
                    "/v3/api-docs/**",  
                    "/external/**",  
                ).permitAll()  
            }  
            .authorizeHttpRequests { it.anyRequest().authenticated() }  
            .cors { it.configurationSource(corsConfigurationSource()) }  
            .csrf { it.disable() }  
            .oauth2ResourceServer { oauth2 ->  
                oauth2.jwt { jwt ->  
                    jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())  
                }  
            }            .sessionManagement { sessionManagement ->  
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)  
            }  
        return http.build()  
    }  
  
    @Bean  
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {  
        val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()  
        grantedAuthoritiesConverter.setAuthoritiesClaimName("groups")  
        grantedAuthoritiesConverter.setAuthorityPrefix("")  
  
        val jwtAuthenticationConverter = JwtAuthenticationConverter()  
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)  
        return jwtAuthenticationConverter  
    }  
}  
  
  
@Configuration  
@Order(2)  
class VaadinSecurityConfig : VaadinWebSecurity() {  
    override fun configure(http: HttpSecurity) {  
        super.configure(http)  
        setOAuth2LoginPage(http, "/oauth2/authorization/keycloak")  
        http.oauth2Login { oauth2 ->  
            oauth2.userInfoEndpoint { userInfoEndpoint -> userInfoEndpoint.userAuthoritiesMapper(this.userAuthoritiesMapper()) }  
        }        http.logout {  
            it.logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))  
        }  
    }  
  
    override fun filterChain(http: HttpSecurity?): SecurityFilterChain {  
        http?.securityMatcher("/ui", "/ui/**", "/connect/**")  
        return super.filterChain(http)  
    }  
  
    @Bean  
    fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {  
        return GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority?> ->  
            authorities.stream()  
                .filter { authority: GrantedAuthority? -> OidcUserAuthority::class.java.isInstance(authority) }  
                .map { authority: GrantedAuthority? ->  
                    val oidcUserAuthority = authority as OidcUserAuthority?  
                    val userInfo = oidcUserAuthority!!.userInfo  
                    val roles = userInfo.getClaim<List<String>>("groups")  
                    roles.stream().map { r: String? -> SimpleGrantedAuthority("ROLE_$r") }  
                }                .reduce(Stream.empty()) { joinedAuthorities: Stream<SimpleGrantedAuthority>, roleAuthorities: Stream<SimpleGrantedAuthority> ->  
                    Stream.concat(  
                        joinedAuthorities,  
                        roleAuthorities  
                    )  
                }  
                .collect(Collectors.toList())  
        }  
    }  
}

This seems to me to be a basic use-case that Vaadin/Hilla should work with. So any ideas?

Seems to be an interesting case, what doesn’t work exactly? Do you have problems with calls to the @BrowserCallable annotated services? Or the UI and router don’t render anything? Any specific errors in browser’s console? Exceptions, runtime errors on the server side?

I’m not a kotlin expert, but IIRC in Java if you override the filterChain method you have to add the @Bean annotation as it is not inherited. And it would be good to use exactly the same bean name as in the parent class.

After further testing, it seems that it is vaadin.url-mapping property that does not agree with this. And my debugging had been a goose-chase around that.

I have a working config with vaadin on root (not wanted).

import com.vaadin.flow.spring.security.VaadinWebSecurity  
import org.springframework.context.annotation.Bean  
import org.springframework.context.annotation.Configuration  
import org.springframework.core.annotation.Order  
import org.springframework.http.HttpStatus  
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity  
import org.springframework.security.config.annotation.web.builders.HttpSecurity  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity  
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer  
import org.springframework.security.config.http.SessionCreationPolicy  
import org.springframework.security.core.GrantedAuthority  
import org.springframework.security.core.authority.SimpleGrantedAuthority  
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper  
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority  
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter  
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter  
import org.springframework.security.web.SecurityFilterChain  
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler  
import org.springframework.web.cors.CorsConfiguration  
import org.springframework.web.cors.UrlBasedCorsConfigurationSource  
import java.time.Duration  
import java.util.stream.Collectors  
import java.util.stream.Stream  
  
@Configuration  
@EnableMethodSecurity  
@Order(1)  
class ApiSecurityConfig {  
    @Bean  
    fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {  
        val configuration = CorsConfiguration()  
        configuration.allowedOriginPatterns = listOf("https://*.com*")  
        configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE")  
        configuration.allowedHeaders = listOf("Content-Type")  
        configuration.allowCredentials = true  
        configuration.setMaxAge(Duration.ofDays(1))  
        val source = UrlBasedCorsConfigurationSource()  
        source.registerCorsConfiguration("/**", configuration)  
        return source  
    }  
  
    @Bean  
    fun webSecurityCustomizer(): WebSecurityCustomizer {  
        return WebSecurityCustomizer { web ->  
            web.debug(true)  
        }  
    }  
  
    @Throws(Exception::class)  
    @Bean  
    fun standardFilterChain(http: HttpSecurity): SecurityFilterChain {  
        http  
            .securityMatcher(  
                "/api/**",  
                "/actuator/**",  
                "/swagger-ui.html",  
                "/swagger-ui/**",  
                "/v3/api-docs",  
                "/v3/api-docs/**"  
            )  
            .authorizeHttpRequests { // Authorize special paths  
                it.requestMatchers(  
                    "/api/deposit/*/form/marc",  
                    "/api/external/**",  
                    "/actuator/health",  
                    "/actuator/health/*",  
                    "/actuator/info",  
                    "/actuator/metrics",  
                    "/actuator/prometheus",  
                    "/swagger-ui/",  
                    "/swagger-ui/*",  
                    "/swagger-ui.html",  
                    "/v3/api-docs",  
                    "/v3/api-docs/**",  
                ).permitAll()  
            }  
            .authorizeHttpRequests { it.anyRequest().authenticated() }  
            .cors { it.configurationSource(corsConfigurationSource()) }  
            .csrf { it.disable() }  
            .oauth2ResourceServer { oauth2 ->  
                oauth2.jwt { jwt ->  
                    jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())  
                }  
            }            .sessionManagement { sessionManagement ->  
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)  
            }  
        return http.build()  
    }  
  
    @Bean  
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {  
        val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()  
        grantedAuthoritiesConverter.setAuthoritiesClaimName("groups")  
        grantedAuthoritiesConverter.setAuthorityPrefix("")  
  
        val jwtAuthenticationConverter = JwtAuthenticationConverter()  
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)  
        return jwtAuthenticationConverter  
    }  
}  
  
  
@Order(2)  
@EnableWebSecurity  
@Configuration  
class VaadinSecurityConfig: VaadinWebSecurity() {  
    override fun configure(http: HttpSecurity) {  
        super.configure(http)  
        setOAuth2LoginPage(http, "/oauth2/authorization/keycloak")  
        http.oauth2Login { oauth2 ->  
            oauth2.userInfoEndpoint { userInfoEndpoint -> userInfoEndpoint.userAuthoritiesMapper(this.userAuthoritiesMapper()) }  
        }        http.logout {  
            it.logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))  
        }  
    }  
  
    @Bean(name = ["VaadinSecurityFilterChainBean"])  
    override fun filterChain(http: HttpSecurity): SecurityFilterChain {  
        http.securityMatcher(  
            "/", // Root path for Vaadin  
            "/png-resetter", // A view in Vaadin Hilla React  
            "/login/**",  
            "/oauth2/**",  
            "/ui/connect/**",  
            "/ui/**",  
            "/ui",  
            "/connect/**",  
            "/VAADIN/**"  
        )  
        return super.filterChain(http)  
    }  
  
    @Bean  
    fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {  
        return GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority?> ->  
            authorities.stream()  
                .filter { authority: GrantedAuthority? -> OidcUserAuthority::class.java.isInstance(authority) }  
                .map { authority: GrantedAuthority? ->  
                    val oidcUserAuthority = authority as OidcUserAuthority?  
                    val userInfo = oidcUserAuthority!!.userInfo  
                    val roles = userInfo.getClaim<List<String>>("groups")  
                    roles.stream().map { r: String? -> SimpleGrantedAuthority("ROLE_$r") }  
                }                .reduce(Stream.empty()) { joinedAuthorities: Stream<SimpleGrantedAuthority>, roleAuthorities: Stream<SimpleGrantedAuthority> ->  
                    Stream.concat(  
                        joinedAuthorities,  
                        roleAuthorities  
                    )  
                }  
                .collect(Collectors.toList())  
        }  
    }  
}

When i set

vaadin:
	url-mapping: /ui/*

My services return HTML. ex
POST http://localhost:PORT/backend/ui/connect/AdminService/getUserInfo
Response:

<!doctype html><!--
This file is auto-generated by Vaadin.
-->
<html lang="en">
 <head>
  <script initial="">window.Vaadin = window.Vaadin || {};window.Vaadin.VaadinLicenseChecker = {  maybeCheck: (productInfo) => {  }};window.Vaadin.devTools = window.Vaadin.devTools || {};window.Vaadin.devTools.createdCvdlElements = window.Vaadin.devTools.createdCvdlElements || [];window.Vaadin.originalCustomElementDefineFn = window.Vaadin.originalCustomElementDefineFn || window.customElements.define;window.customElements.define = function (tagName, constructor, ...args) {const { cvdlName, version } = constructor;if (cvdlName && version) {  const { connectedCallback } = constructor.prototype;  constructor.prototype.connectedCallback = function () {    window.Vaadin.devTools.createdCvdlElements.push(this);    if (connectedCallback) {      connectedCallback.call(this);    }  }}window.Vaadin.originalCustomElementDefineFn.call(this, tagName, constructor, ...args);};</script>
  <script initial="">window.Vaadin = window.Vaadin || {};window.Vaadin.ConsoleErrors = window.Vaadin.ConsoleErrors || [];const browserConsoleError = window.console.error.bind(window.console);console.error = (...args) => {    browserConsoleError(...args);    window.Vaadin.ConsoleErrors.push(args);};window.onerror = (message, source, lineno, colno, error) => {const location=source+':'+lineno+':'+colno;window.Vaadin.ConsoleErrors.push([message, '('+location+')']);};window.addEventListener('unhandledrejection', e => {    window.Vaadin.ConsoleErrors.push([e.reason]);});</script>
  <script initial="">window.Vaadin.devToolsPlugins = [];
window.Vaadin.devToolsConf = {"enable":true,"url":"./../ui/VAADIN/push","backend":"SPRING_BOOT_DEVTOOLS","liveReloadPort":35729,"token":"9e7c9c5f-6f92-4f6a-886d-c7b817043182"};
</script>
  <script initial="">window.Vaadin = window.Vaadin || {};
window.Vaadin.developmentMode = true;
</script>
  <script initial="">if (!('CSSLayerBlockRule' in window)) {
    window.location.search='v-r=oldbrowser';
}
</script>
  <script initial="">window.Vaadin = window.Vaadin || {};window.Vaadin.TypeScript= {};</script>
  <meta name="_csrf_parameter" content="_csrf">
  <meta name="_csrf_header" content="X-CSRF-TOKEN">
  <meta name="_csrf" content="Hm-oxtCi-fbrK4mGCL4CEV0ok0jNkpIKS2byt16ZHO6ie7dnLAqQ8-HEyMbGGb3gOpM2dG1Mvir08KsnKgCQ0marKNrAGoFU">
  <script type="module">const csrfParameterName = '_csrf';
const csrfCookieName = 'XSRF-TOKEN';
window.addEventListener('formdata', (e) => {
  if (!e.formData.has(csrfParameterName)) {
    return;
  }

  const cookies = new URLSearchParams(document.cookie.replace(/;\s*/, '&'));
  if (!cookies.has(csrfCookieName)) {
    return;
  }

  e.formData.set(csrfParameterName, cookies.get(csrfCookieName));
});
</script>
  <script initial="">window.Vaadin = window.Vaadin || {};
window.Vaadin.featureFlagsUpdaters = window.Vaadin.featureFlagsUpdaters || [];
window.Vaadin.featureFlagsUpdaters.push((activator) => {
});</script><base href="./../..">
  <script type="text/javascript">window.JSCompiler_renameProperty = function(a) { return a;}</script>
  <script type="module">
import { inject } from "/backend/ui/VAADIN/@vite-plugin-checker-runtime";
inject({
  overlayConfig: {},
  base: "/backend/ui/VAADIN/",
});
</script>
  <script type="module">
import RefreshRuntime from "/backend/ui/VAADIN/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
  <script type="module" src="/backend/ui/VAADIN/@vite/client"></script>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body, #outlet {
      height: 100vh;
      width: 100%;
      margin: 0;
    }
  </style><!-- index.ts is included here automatically (either by the dev server or during the build) -->
  <script type="module" src="/backend/ui/VAADIN/generated/vite-devmode.ts" onerror="document.location.reload()"></script>
  <script type="module" src="/backend/ui/VAADIN/generated/vaadin.ts"></script>
  <style>.v-reconnect-dialog,.v-system-error {position: absolute;color: black;background: white;top: 1em;right: 1em;border: 1px solid black;padding: 1em;z-index: 10000;max-width: calc(100vw - 4em);max-height: calc(100vh - 4em);overflow: auto;} .v-system-error {color: indianred;pointer-events: auto;} .v-system-error h3, .v-system-error b {color: red;}</style>
  <style>[hidden] { display: none !important; }</style><!--CSSImport end--><!--Stylesheet end-->
  <script initial="" src="./../../VAADIN/static/push/vaadinPush.js"></script>
  <script>window.Vaadin = window.Vaadin ?? {};
window.Vaadin.views = {"":{"title":"Main","rolesAllowed":null,"loginRequired":true,"route":"","lazy":false,"register":false,"menu":{"title":null,"order":null,"exclude":true,"icon":null,"menuClass":null},"flowLayout":false,"params":{}},"/png-deposit-resetter":{"title":"Png Resetter","rolesAllowed":["ROLE_T_dimo_admin"],"loginRequired":true,"route":"png-deposit-resetter","lazy":false,"register":false,"menu":{"title":"Png Resetter","order":1.0,"exclude":false,"icon":"vaadin:step-backward","menuClass":null},"flowLayout":false,"params":{}}};</script>
 </head>
 <body>
  <!-- This outlet div is where the views are rendered -->
  <div id="outlet"></div>
  <script>window.Vaadin = window.Vaadin || {};
window.Vaadin.registrations = window.Vaadin.registrations || [];
window.Vaadin.registrations.push({"is":"flow/SpringInstantiator","version":"24.8.0.alpha8"},{"is":"flow/hotdeploy","version":"24.8.0.alpha8"},{"is":"java","version":"21.0.6"},{"is":"SpringFramework","version":"6.1.13"},{"is":"has-endpoint","version":"24.8.0.alpha5"},{"is":"has-react","version":"24.8.0.alpha5"},{"is":"hilla","version":"24.8.0.alpha5"},{"is":"SpringBoot","version":"3.3.4"},{"is":"has-hilla-fs-route","version":"24.8.0.alpha5"});</script><vaadin-dev-tools></vaadin-dev-tools>
 </body>
</html>

And with logs in the backend:

2025-06-03T16:49:07.713+02:00  INFO 3139147 --- [nio-8082-exec-4] Spring Security Debugger                 :   
  
************************************************************  
  
Request received for POST '/ui/connect/AdminService/getUserInfo':  
  
org.apache.catalina.connector.RequestFacade@16e2a778  
  
servletPath:/ui  
pathInfo:/connect/AdminService/getUserInfo  
headers:   
host: localhost:8082  
user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0  
accept: application/json  
accept-language: en-US,en;q=0.5  
accept-encoding: gzip, deflate, br, zstd  
referer: http://localhost:8082/backend/ui/  
x-csrf-token: <redacted>  
content-type: application/json  
content-length: 2  
origin: http://localhost:8082  
dnt: 1  
sec-gpc: 1  
connection: keep-alive  
cookie: JSESSIONID=<redacted>; csrfToken=<redacted>  
sec-fetch-dest: empty  
sec-fetch-mode: cors  
sec-fetch-site: same-origin  
priority: u=4  
  
  
Security filter chain: [  
  DisableEncodeUrlFilter  
  WebAsyncManagerIntegrationFilter  
  SecurityContextHolderFilter  
  HeaderWriterFilter  
  CorsFilter  
  CsrfFilter  
  LogoutFilter  
  OAuth2AuthorizationRequestRedirectFilter  
  OAuth2LoginAuthenticationFilter  
  RequestCacheAwareFilter  
  SecurityContextHolderAwareRequestFilter  
  AnonymousAuthenticationFilter  
  ExceptionTranslationFilter  
  AuthorizationFilter  
]  
  
  
************************************************************  
  
  
2025-06-03T16:49:07.713+02:00 DEBUG 3139147 --- [nio-8082-exec-4] o.s.security.web.FilterChainProxy        : Securing POST /ui/connect/AdminService/getUserInfo  
2025-06-03T16:49:07.713+02:00 DEBUG 3139147 --- [nio-8082-exec-4] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=OAuth2AuthenticationToken <redacted>]  
2025-06-03T16:49:07.717+02:00 DEBUG 3139147 --- [nio-8082-exec-4] o.s.security.web.FilterChainProxy        : Secured POST /ui/connect/AdminService/getUserInfo  
2025-06-03T16:49:07.717+02:00 DEBUG 3139147 --- [nio-8082-exec-4] c.v.b.devserver.AbstractDevServerRunner  : Requesting resource from Vite http://127.0.0.1:35997/backend/ui/connect/AdminService/getUserInfo  
2025-06-03T16:49:07.719+02:00 DEBUG 3139147 --- [nio-8082-exec-4] c.v.b.devserver.AbstractDevServerRunner  : Resource not served by Vite /connect/AdminService/getUserInfo

So is there some “trick” i need to be able to host vaadin on “/ui” and not root?

I may be wrong, but I think Hilla endpoint controller is not affected by vaadin.urlMapping.
The endpoint controller prefix is /connect by default, but can be changed with the vaadin.endpoint.prefix property.

What’s the result if you make a POST to /connect/AdminService/getUserInfo (without /ui/)?

POST to /connect/AdminService/getUserInfo gives me the correct result :thinking:

I tried configuring vaadin.endpoint.prefix

vaadin:
    url-mapping: /ui/*
    endpoint:
        prefix: /ui/connect

Intellij also cannot seem to resolve it Cannot resolve configuration property 'vaadin.endpoint.prefix'.

But i still only get HTML back, and now if i POST without /ui i get 404

timestamp	"2025-06-04T08:53:58.568+00:00"
status	404
error	"Not Found"
trace	"org.springframework.web.servlet.resource.NoResourceFoundException: No static resource connect/AdminService/getUserInfo.\n\tat org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(ResourceHttpRequestHandler.java:585)\n\tat org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter.handle(HttpRequestHandlerAdapter.java:52)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doSe…at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\n"
message	"No static resource connect/AdminService/getUserInfo."
path	"/backend/connect/AdminService/getUserInfo"

I suppose that if you change the endpoint prefix to clash with the urlMapping, still Vaadin Flow servlet takes precedence over the Hilla controller.

You can try to set vaadin.exclude-urls=/ui/connect/** and see if it works for your use case.

Tried setting

vaadin:
    url-mapping: /ui/*
    endpoint:
        prefix: /ui/connect
    exclude-urls: /ui/connect/**

Still having the same issue

Also tried to set the endpoint to something entirely different:

vaadin:
    url-mapping: /ui/*
    endpoint:
        prefix: /hilla/endpoint

Calls to http://localhost:8082/backend/hilla/endpoint/AdminService/getUserInfo works with it, but the frontend still POST’s to /ui/connect.

Right, vaadin.exclude-urls is meant to work only when VaadinServlet is mapped to /*

I guess that if you change the endpoint prefix you need to manually configure the client

Copy the src/main/frontend/generated/connect-client.default.ts file to src/main/frontend/connect-client.ts and modify the prefix property value.

Something like this

import { ConnectClient as ConnectClient_1 } from "@vaadin/hilla-frontend";
const client_1 = new ConnectClient_1({ prefix: "/hilla/endpoint" });
export default client_1;

There is an open issue about this topic: Make the client prefix configurable · Issue #148 · vaadin/hilla · GitHub

I found the same issue, and have tested a bit.

vaadin:
    url-mapping: /ui/*
    endpoint:
        prefix: /hilla/connect

With a custom client (frontend/connect-client.ts) with hilla/connect as the prefix. And middleware function which removes /ui from the requests.

import { ConnectClient as ConnectClient_1 } from "@vaadin/hilla-frontend";
import {MaybePromise, MiddlewareContext, MiddlewareNext} from "@vaadin/hilla-frontend/Connect.js";
const mwareFunction = (context: MiddlewareContext, next: MiddlewareNext): MaybePromise<Response> => {
    if (["GET", "POST"].includes(context.request.method) && context.request.url.includes("/ui/hilla/connect/")) {
        const currentUrl = new URL(context.request.url);
        context.request = new Request(
            currentUrl.toString().replace("/ui", ""),
            {
                method: context.request.method,
                headers: context.request.headers,
                body: context.request.body,
                credentials: context.request.credentials,
                cache: context.request.cache,
                redirect: context.request.redirect,
                integrity: context.request.integrity,
                keepalive: context.request.keepalive,
                signal: context.request.signal
            }
        );
    }
    return next(context);
}
const client_1 = new ConnectClient_1({ prefix: "hilla/connect", middlewares: [mwareFunction]});
export default client_1;

with security config

import com.vaadin.flow.spring.security.VaadinWebSecurity
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.http.HttpStatus
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
import java.time.Duration
import java.util.stream.Collectors
import java.util.stream.Stream

@Configuration
@EnableMethodSecurity
@Order(1)
class ApiSecurityConfig {
    @Bean
    fun corsConfigurationSource(): UrlBasedCorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOriginPatterns = listOf("https://*.nb.no*")
        configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE")
        configuration.allowedHeaders = listOf("Content-Type")
        configuration.allowCredentials = true
        configuration.setMaxAge(Duration.ofDays(1))
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }

    @Bean
    fun webSecurityCustomizer(): WebSecurityCustomizer {
        return WebSecurityCustomizer { web ->
            web.debug(true)
        }
    }

    @Throws(Exception::class)
    @Bean
    fun standardFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .securityMatcher(
                "/api/**",
                "/actuator/**",
                "/swagger-ui.html",
                "/swagger-ui/**",
                "/v3/api-docs",
                "/v3/api-docs/**"
            )
            .authorizeHttpRequests { // Authorize special paths
                it.requestMatchers(
                    "/api/deposit/*/form/marc",
                    "/api/external/**",
                    "/actuator/health",
                    "/actuator/health/*",
                    "/actuator/info",
                    "/actuator/metrics",
                    "/actuator/prometheus",
                    "/swagger-ui/",
                    "/swagger-ui/*",
                    "/swagger-ui.html",
                    "/v3/api-docs",
                    "/v3/api-docs/**",
                ).permitAll()
            }
            .authorizeHttpRequests { it.anyRequest().authenticated() }
            .cors { it.configurationSource(corsConfigurationSource()) }
            .csrf { it.disable() }
            .oauth2ResourceServer { oauth2 ->
                oauth2.jwt { jwt ->
                    jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
                }
            }
            .sessionManagement { sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }
        return http.build()
    }

    @Bean
    fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
        val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
        grantedAuthoritiesConverter.setAuthoritiesClaimName("groups")
        grantedAuthoritiesConverter.setAuthorityPrefix("")

        val jwtAuthenticationConverter = JwtAuthenticationConverter()
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
        return jwtAuthenticationConverter
    }
}


@Order(2)
@EnableWebSecurity
@Configuration
class VaadinSecurityConfig: VaadinWebSecurity() {
    override fun configure(http: HttpSecurity) {
        super.configure(http)
        setOAuth2LoginPage(http, "/oauth2/authorization/keycloak")
        http.oauth2Login { oauth2 ->
            oauth2.userInfoEndpoint { userInfoEndpoint -> userInfoEndpoint.userAuthoritiesMapper(this.userAuthoritiesMapper()) }
        }
        http.logout {
            it.logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
        }
    }

    @Bean(name = ["VaadinSecurityFilterChainBean"])
    override fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http.securityMatcher(
            "/",
            "/login/**",
            "/oauth2/**",
            "/ui/**",
            "/connect/**",
            "/hilla/**",
            "/VAADIN/**"
        )
        return super.filterChain(http)
    }

    @Bean
    fun userAuthoritiesMapper(): GrantedAuthoritiesMapper {
        return GrantedAuthoritiesMapper { authorities: Collection<GrantedAuthority?> ->
            authorities.stream()
                .filter { authority: GrantedAuthority? -> OidcUserAuthority::class.java.isInstance(authority) }
                .map { authority: GrantedAuthority? ->
                    val oidcUserAuthority = authority as OidcUserAuthority?
                    val userInfo = oidcUserAuthority!!.userInfo
                    val roles = userInfo.getClaim<List<String>>("groups")
                    roles.stream().map { r: String? -> SimpleGrantedAuthority("ROLE_$r") }
                }
                .reduce(Stream.empty()) { joinedAuthorities: Stream<SimpleGrantedAuthority>, roleAuthorities: Stream<SimpleGrantedAuthority> ->
                    Stream.concat(
                        joinedAuthorities,
                        roleAuthorities
                    )
                }
                .collect(Collectors.toList())
        }
    }
}

This works, but i still need to include root / in the VaadinSecurityFilterChainBean, or else i get redirect issues with login.
Is there a way to not have vaadin go to root?

edited to add error info for login issues:

2025-06-04T14:02:04.480+02:00  INFO 881859 --- [io-8082-exec-10] Spring Security Debugger                 : 

************************************************************

Request received for POST '/ui/?v-r=uidl&v-uiId=1':

org.apache.catalina.connector.RequestFacade@747ff346

servletPath:/ui
pathInfo:/
headers: 
host: localhost:8082
user-agent: Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0
accept: */*
accept-language: en-US,en;q=0.5
accept-encoding: gzip, deflate, br, zstd
content-type: application/json; charset=UTF-8
content-length: 326
origin: http://localhost:8082
dnt: 1
sec-gpc: 1
connection: keep-alive
referer: http://localhost:8082/backend/ui/login
cookie: JSESSIONID=913653EF9272ECD793D3DE77FFE56881; csrfToken=3dca0fdf-96c5-427e-9386-69852d20c566
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-origin


Security filter chain: no match


************************************************************


2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl     : Checking access for view com.vaadin.flow.component.UI$ClientViewPlaceholder
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.AnnotatedViewAccessChecker  : Access to view 'com.vaadin.flow.component.UI$ClientViewPlaceholder' with path 'login' is allowed
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] f.s.a.DefaultAccessCheckDecisionResolver : Access to view 'com.vaadin.flow.component.UI$ClientViewPlaceholder' with path 'login' allowed by 1 out of 1 navigation checkers  (0 neutral).
2025-06-04T14:02:04.486+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl     : Decision against 1 checker results: Access decision: ALLOW
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl     : Checking access for view com.vaadin.flow.router.RouteNotFoundError
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.AnnotatedViewAccessChecker  : Access to view 'com.vaadin.flow.router.RouteNotFoundError' with path 'login' is allowed
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] f.s.a.DefaultAccessCheckDecisionResolver : Access to view 'com.vaadin.flow.router.RouteNotFoundError' with path 'login' allowed by 1 out of 1 navigation checkers  (0 neutral).
2025-06-04T14:02:04.495+02:00 DEBUG 881859 --- [io-8082-exec-10] c.v.f.s.auth.NavigationAccessControl     : Decision against 1 checker results: Access decision: ALLOW
2025-06-04T14:02:04.500+02:00 DEBUG 881859 --- [io-8082-exec-10] c.vaadin.flow.router.RouteNotFoundError  : Route is not found

com.vaadin.flow.router.NotFoundException: Couldn't find route for 'login'
	at com.vaadin.flow.component.UI.handleErrorNavigation(UI.java:2117) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.component.UI.renderViewForRoute(UI.java:2025) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.component.UI.browserNavigate(UI.java:1880) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:239) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:488) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:298) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.lambda$fireEvent$2(ElementListenerMap.java:475) ~[flow-server-24.7.6.jar:24.7.6]
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
	at com.vaadin.flow.internal.nodefeature.ElementListenerMap.fireEvent(ElementListenerMap.java:475) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.communication.rpc.EventRpcHandler.handleNode(EventRpcHandler.java:62) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.communication.rpc.AbstractRpcInvocationHandler.handle(AbstractRpcInvocationHandler.java:79) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocationData(ServerRpcHandler.java:568) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.communication.ServerRpcHandler.lambda$handleInvocations$6(ServerRpcHandler.java:549) ~[flow-server-24.7.6.jar:24.7.6]
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) ~[na:na]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:549) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:376) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:138) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:63) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.VaadinService.handleRequest(VaadinService.java:1664) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.server.VaadinServlet.service(VaadinServlet.java:398) ~[flow-server-24.7.6.jar:24.7.6]
	at com.vaadin.flow.spring.SpringServlet.service(SpringServlet.java:106) ~[vaadin-spring-24.7.6.jar:na]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.30.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.security.web.ObservationFilterChainDecorator$FilterObservation$SimpleFilterObservation.lambda$wrap$1(ObservationFilterChainDecorator.java:479) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.security.web.ObservationFilterChainDecorator.lambda$wrapUnsecured$1(ObservationFilterChainDecorator.java:90) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:219) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.security.web.debug.DebugFilter.invokeWithWrappedRequest(DebugFilter.java:90) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:78) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.security.web.debug.DebugFilter.doFilter(DebugFilter.java:67) ~[spring-security-web-6.3.3.jar:6.3.3]
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195) ~[spring-webmvc-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230) ~[spring-security-config-6.3.3.jar:6.3.3]
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268) ~[spring-web-6.1.13.jar:6.1.13]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:113) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.13.jar:6.1.13]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
	at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

And in frontend Could not navigate to 'login'