tl;dr: how to enable spring's ResourceUrlEncodingFilter for spring boot Error pages?
(Question written while using spring boot 1.3.7.RELEASE and Spring Framework/MVC 4.2.4.RELEASE)
Some background: We have a fairly standard spring boot/spring webmvc project using Thymeleaf as the view layer. We have the out-of-the-box spring boot Resource Chain enabled to serve static assets.
Our thymeleaf views have standard url-encoding syntax in them such as <script th:src="#{/js/some-page.js}"></script>. This relies on Spring's org.springframework.web.servlet.resource.ResourceUrlEncodingFilter to transform the url into an appropriately-versioned url such as /v1.6/js/some-page.js.
Our error handling is done by:
setting server.error.whitelabel.enabled=false
subclassing spring boot's default BasicErrorController to override public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
relying on our already-configured thymeleaf view resolvers to render our custom error page
The problem is: the ResourceUrlEncodingFilter isn't applying on our error pages. I assume it's a lack of the filter being registered for ERROR dispatched requests, but it's not obvious to me: a) how to customize this in spring boot; and b) why this wasn't done by default.
Update 1:
The issue seems to be with a combination of OncePerRequestFilter and the ERROR dispatcher. Namely:
ResouceUrlEncodingFilter does not bind to the ERROR dispatcher by default. While overriding this is messy it's not impossible, but doesn't help due to:
OncePerRequestFilter (parent of ResourceUrlEncodingFilter) sets an attribute on the Request indicating it's been applied so as to not re-apply. It then wraps the response object. However, when an ERROR is dispatched, the wrapped response is not used and the filter does not re-wrap due to the request attribute still being present.
Worse still, the logic for customizing boolean hasAlreadyFilteredAttribute is not overridable by request. OncePerRequestFilter's doFilter() method is final, and getAlreadyFilteredAttributeName() (the extension point) does not have access to the current request object to get the dispatcher.
I feel like I must be missing something; it seems impossible to use versioned resources on a 404 page in spring boot.
Update 2: A working but messy solution
This is the best I've been able to come up with, which still seems awfully messy:
public abstract class OncePerErrorRequestFilter extends OncePerRequestFilter {
#Override
protected String getAlreadyFilteredAttributeName() {
return super.getAlreadyFilteredAttributeName() + ".ERROR";
}
#Override
protected boolean shouldNotFilterErrorDispatch() {
return false;
}
}
public class ErrorPageCapableResourceUrlEncodingFilter extends OncePerErrorRequestFilter {
// everything in here is a perfect copy-paste of ResourceUrlEncodingFilter since the internal ResourceUrlEncodingResponseWrapper is private
}
// register the error-supporting version if the whitelabel error page has been disabled ... could/should use a dedicated property for this
#Configuration
#AutoConfigureAfter(WebMvcAutoConfiguration.class)
#ConditionalOnClass(OncePerErrorRequestFilter.class)
#ConditionalOnWebApplication
#ConditionalOnEnabledResourceChain
#ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", havingValue="false", matchIfMissing = false)
public static class ThymeleafResourceUrlEncodingFilterErrorConfiguration {
#Bean
public FilterRegistrationBean errorPageResourceUrlEncodingFilterRegistration() {
FilterRegistrationBean reg = new FilterRegistrationBean();
reg.setFilter(new ErrorPageCapableResourceUrlEncodingFilter());
reg.setDispatcherTypes(DispatcherType.ERROR);
return reg;
}
}
Better solutions?
This has been reported in spring-projects/spring-boot#7348 and a fix is on its way.
It seems you've made an extensive analysis of the issue; too bad you didn't report this issue earlier. Next time, please consider raising those on the Spring Boot tracker.
Thanks!
Related
I have a custom ErrorController like this:
#Controller
public class CustomErrorController implements ErrorController {
#RequestMapping("/error42")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
System.err.println(status);
if (Objects.isNull(status)) return "error";
int statusCode = Integer.parseInt(status.toString());
String view = switch (statusCode) {
case 403 -> "errors/403";
case 404 -> "errors/404";
case 500 -> "errors/500";
default -> "error";
};
return view;
}
}
And then I've set the server.error.path property like this:
server.error.path=/error42
So far, so good. Everything works fine. All the errors go through my CustomErrorController.
But when I set the error path to server.error.path=/error - and of course I change the request mapping annotation to #RequestMapping("/error") - this won't work anymore.
Spring Boot now completely ignores my CustomErrorController. I know, I've set the path to the one Spring Boot usually defines as standard, but is there no way to override this?
Many thanks for any information clearing up this weird behavior.
I found the error, and it was solely my own fault. Since especially at the beginning of a Spring Boot career, the setting options quickly become overhelming, and one can lose sight of one or the other adjustment made, I would still like to leave this question and answer it myself.
The culprit was a self-configured view that i did weeks ago and completely lost track of:
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
/* FYI: will map URIs to views without the need of a Controller */
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login")
.setViewName("/login");
registry.addViewController("/error") // <--- Take this out !!!
.setViewName("/error");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
May this help others facing the same mystery, why once again nothing runs quite as desired...
I'm building a Quarkus app which handles http requests with resteasy and calls another api with restclient and I need to propagate a header and add another one on the fly so I added a class that implements ClientHeadersFactory.
Here's the code:
#ApplicationScoped
public abstract class MicroServicesHeaderHandler implements ClientHeadersFactory {
#Inject
MicroServicesConfig config;
#Override
public MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
// Will be merged with outgoing headers
return new MultivaluedHashMap<>() {{
put("Authorization", Collections.singletonList("Bearer " + config.getServices().get(getServiceName()).getAccessToken()));
put("passport", Collections.singletonList(incomingHeaders.getFirst("passport")));
}};
}
protected abstract String getServiceName();
My issue is that the injection of the config doesn't work. I tried both with #Inject and #Context, as mentioned in the javadoc of ClientHeadersFactory. I also tried to make the class non abstract but it doesn't change anything.
MicroServicesConfig is a #Startup bean because it needs to be initialized before Quarkus.run() is called, otherwise the hot reload doesn't work anymore, since it's required to handle requests.
Here's the code FYI:
#Getter
#Startup
#ApplicationScoped
public final class MicroServicesConfig {
private final Map<String, MicroService> services;
MicroServicesConfig(AKV akv, ABS abs) {
// some code to retrieve an encrypted file from a secure storage, decrypt it and initialize the map out of it
}
It appears to be an issue with ClientHeadersFactory because if I inject my bean in my main class (#QuarkusMain), it works. I'm then able to assign the map to a public static map that I can then access from my HeaderHandler with Application.myPublicStaticMap but that's ugly so I would really prefer to avoid that.
I've searched online and saw several people having the same issue but according to this blogpost, or this one, it should work as of Quarkus 1.3 and MicroProfile 3.3 (RestClient 1.4) and I'm using Quarkus 1.5.2.
Even the example in the second link doesn't work for me with the injection of UriInfo so the issue doesn't come from the bean I'm trying to inject.
I've been struggling with this for weeks and I'd really like to get rid of my workaround now.
I'm probably just missing something but it's driving me crazy.
Thanks in advance for your help.
This issue has finally been solved in Quarkus 1.8.
I'm running a GraphQL API using GraphQL-SPQR and Spring Boot.
At the moment, I am throwing RuntimeExceptions to return GraphQL errors. I have a customExceptionHandler that implements DataFetcherExceptionHandler that returns errors in the correct format, as shown below:
class CustomExceptionHandler : DataFetcherExceptionHandler {
override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters?): DataFetcherExceptionHandlerResult {
// get exception
var exception = handlerParameters?.exception
val locations = listOf(handlerParameters?.sourceLocation)
val path = listOf(handlerParameters?.path?.segmentName)
// create a GraphQLError from your exception
if (exception !is GraphQLError) {
exception = CustomGraphQLError(exception?.localizedMessage, locations, path)
}
// cast to GraphQLError
exception as CustomGraphQLError
exception.locations = locations
exception.path = path
val errors = listOf<GraphQLError>(exception)
return DataFetcherExceptionHandlerResult.Builder().errors(errors).build()
}
}
I use the CustomExceptionHandler as follows (in my main application class):
#Bean
fun graphQL(schema: GraphQLSchema): GraphQL {
return GraphQL.newGraphQL(schema)
.queryExecutionStrategy(AsyncExecutionStrategy(CustomExceptionHandler()))
.mutationExecutionStrategy(AsyncSerialExecutionStrategy(CustomExceptionHandler()))
.build()
}
I'd like to set a header variable for a UUID that corresponds to the exception, for logging purposes. How would I do that?
Even better, is it possible to create a Spring Bean that puts the UUID in the header for all queries and mutations?
Thanks!
when you're using spring boot, there's two options:
you're using the spring boot graphql spqr starter (which brings it's own controller to handle all graphQL requests)
you're using plain graphql-spqr and have your own controller to handle GraphQL requests
In any case, you've got a few options:
Making your CustomExceptionHandler a Spring Bean and Autowiring HttpServletResponse
That would probably be the easiest way to go - and it would probably work in any case: You could simply make your CustomExceptionHandler a Spring bean and have it autowire the HttpServletRequest - in the handler method, you could then set it to whatever you would like it to be. Here's some dummy code in Java (sorry, I am not proficient enough in Kotlin):
#Component
class CustomExceptionHandler implements DataFetcherExceptionHandler {
private final HttpServletResponse response;
public CustomExceptionHandler(HttpServletResponse response) {
this.response = response;
}
#Override
public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
response.setHeader("X-Request-ID", UUID.randomUUID().toString());
// ... your actual error handling code
}
}
This is going to work because spring will realise that HttpServletRequest differs for each request. It will therefore inject a dynamic proxy into your error handler that will point to the actual HttpServletResponse instance for every request.
I would argue, that it's not the most elegant way, but it will certainly solve your problem.
for the graphql-spqr spring boot starter
There's a default controller implementation that is used in projects using this starter. That controller will handle every graphql request that you receive. You can customise it, by implementing your own GraphQLExecutor and making it a spring bean. That executor is responsible to call the GraphQL engine, pass the parameters in and output the response. Here's the default implementation, that you might want to base your work on.
Similarly to the previous solution, you could autowire the HttpServletResponse in that class and set a HTTP Response header.
That solution would allow you to decide, if you want to set a request id in all cases, or just in specific error cases. (graphql.execute returns an object from which you can get the information if and what errors existed)
when using graphql-spqr without the spring boot starter
Locate your GraphQL controller, add an argument to that method of type HttpServletRequest - and then add headers to that as you prefer (see previous section on some more specific suggestions)
I'm integrating spring web sockets capability into an existing spring mvc application, everything works as expected, except for enabling custom Spring Conversion on my inbound messages via #DestinationVariable.
Now I already have custom converters fully working for the http side, ex #RequestParam or #PathVariable but the same conversion on a websocket controller method throws a ConverterNotFoundException
Ex. I have a custom converter that converts String into Users
public class StringToUserConverter implements Converter<String,User>{
#Autowired UserDAO userDAO;
#Override
public User convert(String id) {
return userDAO.getUser(Integer.parseInt(id));
}
}
And this works exactly as expected in my http controllers, where I can pass in an id, and its automatically converted to the domain class
public String myControllerMethod(#RequestParam User user)
However the same does not work for my websocket controller for a parameter annotated with #DestinationVariable
#MessageMapping("/users/{user}")
#SendTo("/users/greetings")
public String send(#DestinationVariable User user) {
return "hello"
}
I stepped through the code and I can see that the DestinationVariableMethodArgumentResolver has the default conversion service which doesnt include my custom coverters
So how do I register custom converters, or a custom ConversionService so that it works for web sockets like it already does for http controllers
So now I'm running into the same issue with #Header annotation for JmsListener methods.
Same idea, #Header User user, throws the ConverterNotFound exception.
#JmsListener(destination = "testTopic")
public void testJmsListener(Message m, #Header User user)..
Here I was trying to pass the user id on the message header, and have spring convert it, but to no avail, only basic default conversions are supported, like strings or numbers.
I have stepped through quite a bit of initialization code in Spring here, and I can see that a new DefaultConversionService gets instantiated in many places, without any consideration for external configuration.
It looks like these modules are not nearly as mature as Spring MVC or the developers took a shortcut. But based on my inspection there is no way to easily configure custom converters.
Ok and here is the very hacky, not recommended, approach that did work. Its pretty convoluted and brittle, Im not going to use it, but just for illustration purposes here is what it took to register a custom converter for #Header jms mapping.
Here Im passing in a user_email on the jms message header, and wanted spring to automatically convert the id/email into the actual domain object User. I already had a working converter that does this well in mvc/http mode.
public class StringToUserConverter implements Converter<String,User>{
#Autowired
UserDAO userDAO;
public User convert(String email) {
return userDAO.getByEmail(email);
}
}
The above part is pretty standard and straight forward. Here comes the idiotically convoluted part. I stepped through the spring jms listener initialization code and found lowest spot where I could cut-in with my custom converter for jms #Header.
I created a service, that will #Autowire one of springs Jms beans, and then sets a custom conversion service on it using #PostConstruct. Even here some of the properties were private, so I had to use reflection to read them
#Service
public class JmsCustomeConverterSetter {
#Autowired
StringToUserConverter stringToUserConverter;
#Autowired
JmsListenerAnnotationBeanPostProcessor jmsPostProcessor;
#PostConstruct
public void attachCustomConverters() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
//create custom converter service that includes my custom converter
GenericConversionService converterService = new GenericConversionService();
converterService.addConverter(stringToUserConverter); //add custom converter, could add multiple here
DefaultConversionService.addDefaultConverters(converterService); //attach some default converters
//reflection to read the private field so i can use it later
Field field = jmsPostProcessor.getClass().getDeclaredField("beanFactory"); //NoSuchFieldException
field.setAccessible(true);
BeanFactory beanFactory = (BeanFactory) field.get(jmsPostProcessor); //IllegalAccessException
DefaultMessageHandlerMethodFactory f = new DefaultMessageHandlerMethodFactory();
f.setConversionService(converterService);
f.setBeanFactory(beanFactory); //set bean factory read using reflection
f.afterPropertiesSet();
jmsPostProcessor.setMessageHandlerMethodFactory(f);
}
}
Creating the DefaultMessageHandlerMethodFactory was based on code I saw in org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory.
I would definitely not recommend using this in production. It is fairly brittle and unnecessarily complex.
Spring...sometimes it's a breath of fresh air... and sometimes it's convoluted-clap-trap
I have a spring 3 controller with a validator for one of the methods. It insists on validating every object on the model. Would anyone be able to explain to me why it does this or if I'm doing something wrong?
According to the docs, 5.7.4.3 Configuring a JSR-303 Validator for use by Spring MVC (http://static.springsource.org/spring/docs/3.0.0.RC3/spring-framework-reference/html/ch05s07.html)
With JSR-303, a single javax.validation.Validator instance typically validates all model objects that declare validation constraints. To configure a JSR-303-backed Validator with Spring MVC, simply add a JSR-303 Provider, such as Hibernate Validator, to your classpath. Spring MVC will detect it and automatically enable JSR-303 support across all Controllers.
Example:
#Controller
public class WhaleController {
#Autowired
private Validator myValidator;
#Autowired
private WhaleService whaleService;
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(this.myValidator);
}
#RequestMapping(value="/save-the-whales")
#Transactional
public void saveTheWhales(#Valid WhaleFormData formData, BindingResult errors, Model model) {
if (!errors.hasFieldErrors()) {
Whale whale = new Whale();
whale.setBreed( formData.getBreed() );
this.whaleService.saveWhale( whale );
model.addAttribute("whale", whale);
}
model.addAttribute("errors", errors.getFieldErrors());
}
}
When run it will complain that Whale is an invalid target for myValidator (which is set to validate WhaleFormData, and does so fine). Whale is a POJO with no validation constraints, annotation and no config anywhere. Through trial and error I've found that ANY object placed on the model will attempt to be validated and fail if the validator is not setup to handle it. Primitives are just fine.
Can anyone tell me why this is, point me to the appropriate documentation and/or tell me the best way to put something on the model without having it validated?
In the case above I would like to place "whale" on the model as it will now have a unique whaleId() that it received from my persistence layer.
Thanks!
I guess this behaviour is not covered in the documentation well.
The problem is caused by the following:
By default, #InitBinder-annotated method is called for each non-primitive model attribute, both incoming and outcoming (the purpose of calling it for outcoming attibutes is to allow you to register custom PropertyEditors, which are used by form tags when rendering a form).
DataBinder.setValidator() contains a defensive check that call Validator.supports() and throws an exception if false is returned. So, there is no attempt to perform a validation, just an early check.
The solution is to restrict the scope of #InitBinder to particular attribute:
#InitBinder("whaleFormData")
protected void initBinder(WebDataBinder binder) { ... }