spring boot doesn't shows custom error pages - java

I added spring-boot-starter-thymeleaf dependency to my project that using spring boot 2.3.1.RELEASE and placed error.html file inside src/main/resources/templates with name error.html and other custom error pages inside src/main/resources/templates/error` as you can see in below image:
and add this configuration in application.yml:
server:
error:
whitelabel:
enabled: false
and exclude ErrorMvcAutoConfiguration by add #SpringBootApplication(exclude = {ErrorMvcAutoConfiguration.class}) into Application class.
But, unfortunately i see this below page when error occurs, for example for 404 error!
How can i solve this problem? i also googled for this but didn't find anything to help.

Try to use WebServerFactoryCustomizer:
#Configuration
public class WebConfig implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
#Override
public void customize(ConfigurableServletWebServerFactory factory) {
factory.addErrorPages(
new ErrorPage(HttpStatus.FORBIDDEN, "/403"),
new ErrorPage(HttpStatus.NOT_FOUND, "/404"),
new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"));
}
}
And ErrorController:
#Controller
public class ErrorController {
#GetMapping("/403")
public String forbidden(Model model) {
return "error/403";
}
#GetMapping("/404")
public String notFound(Model model) {
return "error/404";
}
#GetMapping("/500")
public String internal(Model model) {
return "error/500";
}
#GetMapping("/access-denied")
public String accessDenied() {
return "error/access-denied";
}
}
I have the same structure and it works for me:
Example: Customize the Error Messages
PS: in mine application.yml i don't have any properties for error handling

You don't need to exclude ErrorMvcAutoConfiguration.
Simply put error pages to these places.
see also:
https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-error-handling-custom-error-pages

add this configuration in application.yml/application.properties:
server.error.path=/error
creat your error pages inder error path
error/400.html
error/500.html
error/error.html
create class implements ErrorController:
#Controller
public class MyErrorController implements ErrorController {
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode == HttpStatus.NOT_FOUND.value()) {
return "/error/404";
}
else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "/error/500";
}
}
return "/error/error";
}
}

Related

Redirect to custom error page from #RolesAllowed

I am using Vaadin 21 and I am trying to handle the error throwed by #RolesAllowed("myRole"), to redirect the user to a custom error page.
I tried to implement a custom AccessDeniedHandler and an AuthentificationEntryPoint, but it doesn't go through them if my user is already connected.
/**
* Require login to access internal pages and configure login form.
*/
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()...
.and()
.exceptionHandling().accessDeniedPage("/accessDenied")
...
super.configure(http);
...
}
I ended up on a white view with this error :
Could not navigate to 'mypage'
With Vaadin DEBUG activated, I have my list of routes, with the reason of the error "Access Denied".
For more informations, my #Configuration class is implementing VaadinWebSecurityConfigurerAdapter.
Any idea how can I redirect my users if they do no have access to a page protected by #RolesAllowed annotation?
Thank you
Ok so I found out how to do it.
Vaadin has a special way to override the Route Not Found page, which is the target of #RolesAllowed when you do not have the right role (curiously).
The Route Not Found error is also used to handle the error when you try to navigate to a route which does not exist.
So it is basic, I just had to create a class which extends RouteNotFoundError, and do a forward to my AccessDenied.view
import javax.servlet.http.HttpServletResponse;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.ErrorParameter;
import com.vaadin.flow.router.NotFoundException;
import com.vaadin.flow.router.RouteNotFoundError;
import project.vaadin.view.AccessDeniedView;
public class CustomNotFoundTarget extends RouteNotFoundError {
private static final long serialVersionUID = 3337229943239284836L;
#Override
public int setErrorParameter(final BeforeEnterEvent event, final ErrorParameter<NotFoundException> parameter) {
event.forwardTo(AccessDeniedView.class);
return HttpServletResponse.SC_NOT_FOUND;
}
}
And voilĂ , I didn't had to modify my HttpSecurity for this. And because my configuration auto redirect to the login view when a user is not logged, I will not encounter this view by mistake for an anonymous user. Perfect in my case.
I will share my solution, work around.
I was able to return different page for not found and for not authorised / forbidden.
Spring boot version: 2.7.5
Vaadin version: 23.2.5
First create Annotation.
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.TYPE, ElementType.METHOD})
public #interface RolesPermitted {
String[] value();
}
Add the annotation to the view.
#Route("/example")
#RolesAllowed("ADMIN")
#RolesPermitted("ADMIN")
public class ExampleView extends VerticalLayout {
public ExampleView() {
add(new H1("Example view"));
}
}
Unfortunately you will need to keep #RolesAllowed or #PermitAll as well.
Next implement BeforeEnterListener.
#Component
public class CustomBeforeEnterListener implements BeforeEnterListener {
#Override
public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
RolesPermitted annotation = beforeEnterEvent.getNavigationTarget().getAnnotation(RolesPermitted.class);
if (annotation != null) {
boolean permitted = false;
String[] roles = annotation.value();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (String role : roles) {
String authority = "ROLE_" + role;
if(authorities.contains(new SimpleGrantedAuthority(authority))) {
permitted = true;
}
}
if (!permitted) {
throw new AccessDeniedException("Forbidden");
}
}
}
}
Then you will need UIInitListener implemented.
#Component
public class CustomUIInitListener implements UIInitListener {
private final CustomBeforeEnterListener customBeforeEnterListener;
public CustomUIInitListener(CustomBeforeEnterListener customBeforeEnterListener) {
this.customBeforeEnterListener = customBeforeEnterListener;
}
#Override
public void uiInit(UIInitEvent uiInitEvent) {
uiInitEvent.getUI().addBeforeEnterListener(customBeforeEnterListener);
}
}
Finally you need to implement VaadinServiceInitListener.
#Component
public class CustomVaadinServiceInitListener implements VaadinServiceInitListener {
private final CustomUIInitListener customUIInitListener;
public CustomVaadinServiceInitListener(CustomUIInitListener customUIInitListener) {
this.customUIInitListener = customUIInitListener;
}
#Override
public void serviceInit(ServiceInitEvent serviceInitEvent) {
serviceInitEvent.getSource().addUIInitListener(customUIInitListener);
}
}
The implementation of BeforeEnterListener will throw AccessDeniedException in case user hasn't got the required role. In order to catch that you need the following class.
#Tag(Tag.DIV)
#AnonymousAllowed
#DefaultErrorHandler
public class AccessDeniedExceptionHandler extends Component implements HasErrorParameter<AccessDeniedException> {
public AccessDeniedExceptionHandler() {
}
#Override
public int setErrorParameter(BeforeEnterEvent event,
ErrorParameter<AccessDeniedException> parameter) {
getElement().setText("Tried to navigate to a view without correct access rights");
return HttpServletResponse.SC_FORBIDDEN;
}
}
Despite the return statement return HttpServletResponse.SC_FORBIDDEN;
You might notice that the response code will be 200. It is an open issue in this version of Vaadin.
https://github.com/vaadin/flow/issues/13421
Credits:
https://vaadin.com/docs/latest/routing/exceptions
Why isn't an custom implemented VaadinServiceInitListener is listening in vaadin 13.0.2?

Disable 1/yes/on and 0/no/off and allow only true/false to be passed via query-param or path-param

I want to disable Spring built-in deserialization of integer 0/1 and the strings
no/off yes/on that are mapped to false/true respectively when they are
passed via query-parameter or path-variable.
When any value that is not true/false is passed I want to throw IllegalArgumentException.
First I've implemented my own StringToBooleanConverter:
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
#Component
public class StringToBooleanConverter implements Converter<String, Boolean> {
#Override
public Boolean convert(String source) {
String value = source.trim().toLowerCase();
if ("true".equals(value)) {
return Boolean.TRUE;
} else if ("false".equals(value)) {
return Boolean.FALSE;
}
throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
}
}
And since I've provided an implementation of similar class spring will use my StringToBooleanConverter and not the one from org.springframework.core.convert.support
But here is where my problem arise. When I send e.g. 0 code is reaching to the IllegalArgumentException and then execution passed to CustomBooleanEditor in the
org.springframework.beans.propertyeditors and the method public void setAsText(#Nullable String text) throws IllegalArgumentException {} is invoked and does almost the same logic
as StringToBooleanConverter.
How can I disable Spring from calling this method and use only the StringToBooleanConverter?
Try registering your Converter as follows:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.removeConvertible(String.class, Boolean.class);
registry.addConverter(new StringToBooleanConverter());
}
}
If this does not work, which I suspect. Try creating your own CustomBooleanEditor as follows:
#Configuration
public class Config {
#Bean
public CustomBooleanEditor getCustomBooleanEditor() {
return new CustomBooleanEditor("true", "false", false);
}
}
Configuring ObjectMapper disabling the following feature helped to solve the problem:
#Bean
#Primary
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
.build();
}

Spring Integradion DSL answer to frontend

Good day! If i use transformer in my integration flow, i didn`t recieve answer to frontend, just waiting for responce. If i remove transformer, all is ok. Here is my controller method:
#GetMapping("/get/{name}")
public ResponseEntity<String> getSpaceShip(#PathVariable String name) {
SpaceShip spaceShip = new SpaceShip(name, 0);
gateway.spaceShipCreated(spaceShip);
return ResponseEntity.ok("Started!");
}
and Configuration:
#Configuration
public class SpaceShipConfiguration {
#MessagingGateway(defaultRequestChannel = "rename")
public interface Gateway {
SpaceShip spaceShipCreated(SpaceShip spaceShip);
}
#Bean
public IntegrationFlow spaceShipMoving() {
return IntegrationFlows.from("rename")
.handle("renameService", "rename")
.handle("fuelService", "addFuel")
//.transform(Transformers.toJson())
.handle("debug", "printMessage")
.get();
}
}
I got the error - my gateway
#MessagingGateway(defaultRequestChannel = "rename")
public interface Gateway {
SpaceShip spaceShipCreated(SpaceShip spaceShip);
}
must return the Object, but after transformation can't. Need just return void in my case.

How to add "api" prefix to all controllers under com.myproject.api?

I was trying to find it but I found many different scenarios but not this one.
What I want to do is to add "/api/" prefix to all routes in controllers under com.myproject.api .
I want "/api/*" for all controllers under package com.myapp.api and no prefix for all controllers under com.myapp.web
Is it possible with Spring / Spring Boot ?
With Spring Boot, this worked for me :
#Configuration
#EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api",
HandlerTypePredicate.forBasePackage("com.your.package"));
}
}
If you are using springboot, you can add the following:
server.servlet.context-path=/api
to application.properties file.
I achieved the result I think you are looking for in the following way, so long as you are using MVC.
First make a configuration class that implements WebMvcRegistrations
#Configuration
public class WebMvcConfig implements WebMvcRegistrations {
#Value("${Prop.Value.String}") //"api"
private String apiPrefix;
#Value("${Prop.Package.Names}") //["com.myapp.api","Others if you like"]
private String[] prefixedPackages;
#Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PrefixedApiRequestHandler(apiPrefix,prefixedPackages);
}
}
Then create a class that extends RequestMappingHandlerMapping
and overrides getMappingForMethod
#Log4j2
public class PrefixedApiRequestHandler extends RequestMappingHandlerMapping {
private final String prefix;
private final String[] prefixedPackages;
public PrefixedApiRequestHandler(final String prefix, final String... packages) {
super();
this.prefix = prefix;
this.prefixedPackages = packages.clone();
}
#Override
protected RequestMappingInfo getMappingForMethod(final Method method, final Class<?> handlerType) {
RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
if (info == null) {
return null;
}
for (final String packageRef : prefixedPackages) {
if (handlerType.getPackageName().contains(packageRef)) {
info = createPrefixedApi().combine(info);
log.trace("Updated Prefixed Mapping " + info);
return info;
}
}
log.trace("Skipped Non-Prefixed Mapping " + info);
return info;
}
private RequestMappingInfo createPrefixedApi() {
String[] patterns = new String[prefix.length()];
for (int i = 0; i < patterns.length; i++) {
// Build the URL prefix
patterns[i] = prefix;
}
return new RequestMappingInfo(
new PatternsRequestCondition(patterns,
getUrlPathHelper(),
getPathMatcher(),
useSuffixPatternMatch(),
useTrailingSlashMatch(),
getFileExtensions()),
new RequestMethodsRequestCondition(),
new ParamsRequestCondition(),
new HeadersRequestCondition(),
new ConsumesRequestCondition(),
new ProducesRequestCondition(),
null);
}
}
You should then see /api/(ControllerMapping) for all mappings, in the specified packages only. Note: I have #RequestMapping("/") at the top of my controller.
Already answered here and here.
Add an application.properties file under src/main/resources, with the following option:
server.contextPath=/api
Check the official reference for common properties.
You should add #RequestMapping("/api") to top of every desired #Controller or #RestController class.
When both the class and method have that annotation, Spring Boot appends them while building the url. In below example the method will be bound to /api/user route.
#RequestMapping("/api")
#RestController
public class MyController {
#RequestMapping("/user")
public List<User> getUsers(){
return ...
}
}

Spring data solr HttpSolrClient does not use core annotation from entity

With a configuration as follows
#Configuration
#EnableSolrRepositories(basePackages={"com.foo"}, multicoreSupport=true)
public class SolrConfig {
#Value("${solr.host}") String solrHost;
#Bean
public SolrClient solrClient() {
return new HttpSolrClient(solrHost);
}
#Bean
public SolrTemplate solrTemplate() {
return new SolrTemplate(solrClient());
}
}
I have a simple entity:
#SolrDocument(solrCoreName = "core1")
public class MyEntity implements Serializable {
If using SolrTemplate to execute queries, it does not use the coreName annotation on the document:
Page results = solrTemplate.queryForPage(search, MyEntity.class);
I get exception:
org.springframework.data.solr.UncategorizedSolrException: Error from server at http://localhost:8983/solr: Expected mime type application/octet-stream but got text/html.
[..]
Problem accessing /solr/select
[...]
<title>Error 404 Not Found</title>
Changing the SolrTemplate bean to:
#Bean
public SolrTemplate solrTemplate() {
return new SolrTemplate(solrClient(), "core1");
}
works
The guys over at spring-data confirmed this is expected behaviour and the template won't read the core from the entity annotation.
So in a multicoreSupport=true environment, if you want to use both the repository and the template you'll have to create 2 beans:
For the repository the base template:
#Bean
public SolrTemplate solrTemplate() {
return new SolrTemplate(solrClient());
}
and for injecting you will have another one:
#Bean
public SolrTemplate customTemplate() {
return new SolrTemplate(solrClient(), "core1");
}
Obviously if you don't need multicoreSupport=true none is needed!

Categories