Affects: 5.1.9
Also affects: 1.5.21
Using an annotated controller and using RedirectAttributes to send BindingResult via flash attributes to allow errors to be displayed at the redirect target. This works perfectly if the target of the redirect uses Model to locate the bean associated to the BindingResult i.e. the bean and associated BindingResult are transferred intact.
If the target of the redirect uses @ModelAttribute to retrieve the targeted bean as part of the method signature, then the BindingResult is reset and the error information lost.
Per my investigation, the issue seems to be introduced in the org.springframework.web.method.annotation.ModelAttributeMethodProcessor
. If the targeted bean is located within the ModelAndViewContainer, the BindingResult seems to be ignored and a new one created.
For clarity, the following signature will always result in an empty/reset BindingResult on redirect with flash attributes:
@GetMapping("/test1")
public void showTest1(@ModelAttribute TestCommand testCommand, BindingResult bindingResult, HttpServletRequest request, HttpServletResponse response) {
...
}
...and the following will result in the BindingResult being transferred through the redirect intact:
@GetMapping("/test2")
public void showTest2(Model model, HttpServletRequest request, HttpServletResponse response) {
...
}
Self-contained controller included in attachment here: TestController.zip. The use of HTML and responseBody.getWriter() was used to provide a completely functional test scenario. The same behaviour was observed initially using Thymeleaf templates. In both POST methods, the same flash attributes are added but only /test2 shows that the error existed after the redirect.
Comment From: rstoyanchev
Passing a BindingResult
across a redirect via flash attributes is not really expected nor actively supported. After an error, it is expected to remain on the same page rather than redirect.
Comment From: rstoyanchev
Something related was discussed previously in #17909.
Comment From: marcthornton
It was frustrating to me that the behaviour is different with @ModelAttribute annotations and without (since it took me a while to locate the actual issue, since I've used this approach on another project).
Are there other Spring-friendly ways of passing error information between @Controllers based on different beans, for future reference? With JSP tag libraries <form:errors and Thymeleaf #fields support built right in, we prefer to avoid custom solutions whenever possible.
For understanding why the redirect with error information, I am running a listing view with available operations on each row. For encapsulation purposes, it is preferable to let each operation (say Reassign or Remove) handle their own pre-validation rather than do it in the controller handling the list. If there is an error, then a redirect back to the listing controller to display the pre-validation error saves the user the step of having to return from a screen that simply says "sorry, you can't do that here". It's also preferable to retain and display operation-specific url's such as /list, /item/remove, /item/reassign, but it would be very odd and difficult to manage displaying the list under /item/reassign simply because there was a pre-validation error. Whether the error is in the list bean or an operation-specific bean, the need to have the BindingResult survive the redirect is required.
Either way, I do appreciate the information and understand that just because something is possible and even widely distributed, it doesn't mean it was intended behaviour or supported.
Comment From: rstoyanchev
It was frustrating to me that the behaviour is different with @ModelAttribute annotations and without
The thing to keep in mind is that this:
void showTest1(@ModelAttribute TestCommand testCommand, BindingResult bindingResult) {
// ...
}
is asking the framework to proactively apply data binding onto TestCommand
based on values from the current request and to include a BindingResult
for it. If TestCommand
happens to be in the model it will be used from there, but nevertheless data binding is applied potentially changing it, and the BindingResult
reflects the current binding.
This on the other hand is simply accessing the model:
void showTest1(Model model) {
// ...
}
As for your use case it is a little difficult to understand that from the given description but my first thought is that a redirect isn't a good way to delegate to another controller where validation errors are involved that need to be communicated back. I don't know if you have considered a forward instead of a redirect, e.g. "forward:" prefixed view name.
In any case BindingResult
is only intended to be used in the lifecycle of one request. You can pass it on via flash attributes but don't expect to be able to inject it into a controller method which is instead meant to apply data binding based on the current request.
Comment From: shawnz
I would also find this functionality helpful. I understand that the "right" thing to do in the context of Spring's design is to stay on the same page in the case of validation errors, but sometimes the client just doesn't want the annoying confirmation prompt upon refreshing a POST request, and doing a post-redirect-get is the easiest way of working around that. Although it's not ideal it would be nice to at least have the option of doing this for the situations where it's necessary.
Another way of solving this problem might be to implement some alternate way to pass field error messages to the template engine besides BindingResult, like some kind of structure that could be passed around in flash attributes or even persisted to the database, and then associated with a model attribute at a later time. That way BindingResult wouldn't have to be abused for this purpose to get nice looking persistent field errors in Thymeleaf.
It is of course possible to manually set your own BindingResult object to a model attribute with name BindingResult.MODEL_KEY_PREFIX + "beanName"
, but it is not really ideal.