73

I'm trying to figure out how to "preserve" the BindingResult so it can be used in a subsequent GET via the Spring <form:errors> tag. The reason I want to do this is because of Google App Engine's SSL limitations. I have a form which is displayed via HTTP and the post is to an HTTPS URL. If I only forward rather than redirect then the user would see the https://whatever.appspot.com/my/form URL. I'm trying to avoid this. Any ideas how to approach this?

Below is what I'd like to do, but I only see validation errors when I use return "create".

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public final String submit(
    @ModelAttribute("register") @Valid final Register register,
    final BindingResult binding) {

    if (binding.hasErrors()) {
        return "redirect:/register/create";
    }

    return "redirect:/register/success";
}
Taylor Leese
  • 51,004
  • 28
  • 112
  • 141

8 Answers8

79

Since Spring 3.1 you can use RedirectAttributes. Add the attributes that you want to have available before doing the redirect. Add both, the BindingResult and the object that you are using to validate, in this case Register.

For BindingResult you will use the name: "org.springframework.validation.BindingResult.[name of your ModelAttribute]".

For the object that you are using to validate you will use the name of ModelAttribute.

To use RedirectAttributes you have to add this in your config file. Among other things you are telling to Spring to use some newer classes:

<mvc:annotation-driven />

Now the errors will be displayed wherever you are redirecting

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public final String submit(@ModelAttribute("register") @Valid final Register register, final BindingResult binding, RedirectAttributes attr, HttpSession session) {

if (binding.hasErrors()) {
    attr.addFlashAttribute("org.springframework.validation.BindingResult.register", binding);
    attr.addFlashAttribute("register", register);
    return "redirect:/register/create";
}

return "redirect:/register/success";
}
Oscar
  • 1,357
  • 2
  • 17
  • 24
  • 4
    It is definitely worth noting that the RedirectAttributes do not work in conjunction with the (obsolete?) ModelAndView concept. They assume your method returns a simple string, as the above example does. – kevinmrohr Jan 25 '13 at 18:46
  • After this ( in my case ) the attribute is now in the model of the controller handling the redirection, but the binding result has no errors. I had to pass the list of errors and add them to the binding results like this: https://gist.github.com/OscarRyz/6381954 – OscarRyz Aug 29 '13 at 18:52
  • 2
    when doing this i get the error: java.io.NotSerializableException: org.springframework.format.support.DefaultFormattingConversionService – Jens Aug 28 '14 at 10:39
  • @Jens the NotSerializableException is finally fixed in Spring 4.3.4 (it only took 5 years :)) https://jira.spring.io/browse/SPR-8282 – zutnop Mar 24 '17 at 17:32
  • I need help, when I use 'RedirectAttributes attr' then i get error : Failed to convert value of type 'org.springframework.validation.BeanPropertyBindingResult' ..... – Vikas Roy May 08 '22 at 05:46
67

In addition to Oscar's nice answer, if you are following that RedirectAttributes approach, do not forget that you are actually passing the modelAttribute to the redirected page. This means if you create a new instance of that modelAttribute for the redirected page (in a controller), you will lose the validation errors. So, if your POST controller method is something like this:

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public final String submit(@ModelAttribute("register") @Valid final Register register, final BindingResult binding, RedirectAttributes attr, HttpSession session) {

if (binding.hasErrors()) {
    attr.addFlashAttribute("org.springframework.validation.BindingResult.register", binding);
    attr.addFlashAttribute("register", register);
    return "redirect:/register/create";
}

return "redirect:/register/success";
}

Then you will probably need to do a modification in your register create page GET controller. From this:

@RequestMapping(value = "/register/create", method = RequestMethod.GET)
public String registerCreatePage(Model model) {
    // some stuff
    model.addAttribute("register", new Register());
    // some more stuff
}

to

@RequestMapping(value = "/register/create", method = RequestMethod.GET)
public String registerCreatePage(Model model) {
    // some stuff
    if (!model.containsAttribute("register")) {
        model.addAttribute("register", new Register());
    }
    // some more stuff
}

Source: http://gerrydevstory.com/2013/07/11/preserving-validation-error-messages-on-spring-mvc-form-post-redirect-get/

Utku Özdemir
  • 7,390
  • 2
  • 52
  • 49
  • This solved it for me. Linked article explains it even better. Thanks. – adnans Oct 19 '14 at 13:17
  • 9
    Common pitfall: change "register" in "org.springframework.validation.BindingResult.register" to the name of your modelAttribute – borjab Oct 24 '14 at 16:32
  • 1
    This is more correct approach!!!!!!!! i didn't saw this answer and was struggling with @Oscar answer, one suggestion, for oscar that please add this answer to your answer so that your answer will be complete and no one will struggle like i did. :) thanks guys saved my time. – Rupesh Yadav. Jan 12 '16 at 15:50
  • A better way is to keep the object creation in the JSP file. Like this: `<% if( request.getAttribute("register") == null ) reqest.setAttribute("register", new Register()) %>`. If you have a lot of controllers resolving that view you don't want to put this logic into each and every one of them. And although Java code in JSP is considered bad practice this code is not business logic and really belongs to the form tag. – UTF_or_Death Aug 16 '16 at 13:25
  • _A better way is to keep the object creation in the JSP file_ **except** if you don't have a JSP as view technology... – maxxyme Dec 20 '16 at 11:12
  • 7
    If you can't access to the source article on gerrydevstory.com, here is an archived version: http://web.archive.org/web/20160606223639/https://gerrydevstory.com/2013/07/11/preserving-validation-error-messages-on-spring-mvc-form-post-redirect-get/ – maxxyme Dec 20 '16 at 11:41
  • I did a happy dance after a half hour of struggling with "preserve BindingResult after redirect" -- the if-statement was indeed what fixed it for me – Ghoti and Chips Sep 07 '22 at 09:02
2

I would question why you need the redirect. Why not just submit to the same URL and have it respond differently to a POST? Nevertheless, if you really want to do this:

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public final String submit(
    @ModelAttribute("register") @Valid final Register register,
    final BindingResult binding,
    HttpSession session) {

    if (binding.hasErrors()) {
        session.setAttribute("register",register);
        session.setAttribute("binding",binding);
        return "redirect:/register/create";
    }

    return "redirect:/register/success";
}

Then in your "create" method:

model.put("register",session.getAttribute("register"));
model.put("org.springframework.validation.BindingResult.register",session.getAttribute("register"));
rjsang
  • 1,757
  • 3
  • 16
  • 26
  • Tried your method, an exception is thrown: "Neither BindingResult nor plain target object for bean name 'xxxx' available as request attribute" – user979051 Aug 04 '13 at 14:50
  • Most likely an error in your JSP. http://www.mkyong.com/spring-mvc/spring-mvc-neither-bindingresult-nor-plain-target-object-for-bean-name-xxx-available-as-request-attribute/ – rjsang Aug 04 '13 at 22:25
  • As My point adding all the attributes to the session bad(unless its necessary). FlashAttributes can use once so I rather like to use it than session. But it depends on your aspect of developing. I uses FlashAttributes when I use single jsp for form submitting and Show success or failed messages without using JavaScript. – Menuka Ishan Jan 13 '17 at 07:41
1

The problem is you're redirecting to a new controller, rather than rendering the view and returning the processed form page. You need to do something along the lines of:

String FORM_VIEW = wherever_your_form_page_resides

...

if (binding.hasErrors())
    return FORM_VIEW;

I would keep the paths outside of any methods due to code duplication of strings.

Lewis
  • 11
  • 1
  • 2
    The whole point is I want to do the redirect and still display the form errors. I know it will work fine if I forward to a view directly. – Taylor Leese May 10 '10 at 15:17
  • @Lewis: Thanks man. This solution helped me with my issue. I had a 'post' that returns list of items and the user get redirected to a different page(details) when clicking on individual line items.I wanted to redirect back to the previous page from details page. I added a GET in the controller and passed the previously processed model to the initial view as you stated. – yonikawa Nov 23 '16 at 17:13
1

The only way to persist objects between requests (ie a redirect) is to store the object in a session attribute. So you would include "HttpServletRequest request" in method parameters for both methods (ie, get and post) and retrieve the object via request.getAttribute("binding"). That said, and having not tried it myself you may need to figure out how to re-bind the binding to the object in the new request.

Another "un-nicer" way is to just change the browser URL using javascript

klonq
  • 3,535
  • 4
  • 36
  • 58
  • 2
    your proposed way is interesting, however I think it would be even better if you can include some sample code – Hoàng Long Jan 15 '13 at 04:54
0

I don't know the exact issue with Google App Engine but using the ForwardedHeaderFilter may help to preserve the original scheme that the client used. This filter was added in Spring Framework 4.3 but some Servlet containers provide similar filters and the filter is self-sufficient so you can also just grab the source if needed.

Rossen Stoyanchev
  • 4,910
  • 23
  • 26
0

Here is an update to this same question for Spring/Spring Boot in 2023 (at least what is working for me with thymeleaf). Updating Utku Özdemir's answer:

@Valid will automatically perform validation before entering controller and call a MethodArgumentNotValidException upon failure, so you have to get rid of @Valid in method definition and manually use org.springframework.validation.Validator (that is, if you don't want to centrally handle the form errors by overwriting exceptions).

Also,BindingResult now has a static key (MODEL_KEY_PREFIX) you can call to get it's proper key name.

Also, use org.springframework.web.servlet.view.RedirectView instead of a string to redirect with flashAttributes.

 @Autowired
 Validator validator;
 
 @RequestMapping(value = "/submit", method = RequestMethod.POST)
 public final RedirectView submit(@ModelAttribute("register") final Register register, final BindingResult binding, RedirectAttributes attr, HttpSession session) {
     
          //Validate manually (remember to remove @Valid in method declare)
          validator.validate(register, binding);
    
          if (binding.hasErrors()) {
               attr.addFlashAttribute(BindingResult.MODEL_KEY_PREFIX+"register", binding);
               attr.addFlashAttribute("register", register);
               //remember to change return type of method from string to RedirectView
               return new RedirectView("/register/create", true);
          }
          //remember to change return type of method from string to RedirectView
          return new RedirectView("/register/success", true);
    
     }

FlashAttributes are automatically added to the model in the redirected method. That goes for both the entity (Request) and the BindingErrors for the fields. However, if (for some weird reason) you need to get them from the FlashAttributes directly (instead of just the Model), you can access them using org.springframework.web.servlet.support.RequestContextUtils using the request. This utility is actually used for accessing custom flash attributes that you may have added in the POST because those (unlike the BindingErrors), you'll have to add manually to the model.

@RequestMapping(value = "/register/create", method = RequestMethod.GET)
public String registerCreatePage(HttpServletRequest request, Model model) {

  // get the BindingResults errors from flash attributes if you want (but kind of pointless)
  Map<String, ?> flashMap = RequestContextUtils.getInputFlashMap(request);
  if (flashMap != null && flashMap.containsKey(BindingResult.MODEL_KEY_PREFIX + "register")) {
        BindingResult fieldErrors1 = (BindingResult) flashMap.get(BindingResult.MODEL_KEY_PREFIX + "register");
    }
  if (flashMap != null && flashMap.containsKey("register")) {
        Register register = (Register) flashMap.get("register");
    }

  // or just get it from the Model cause it was already added by the system
  BindingResult fieldErrors2 = (BindingResult)model.asMap().get(BindingResult.MODEL_KEY_PREFIX+"register");
  Register register2 = (Register)model.asMap().get("register");  

  //Or just return form cause thymeleaf or other view should find errors and model as is.
}
tlarson
  • 353
  • 4
  • 11
-1

Perhaps this is a bit simplistic, but have you tried adding it to your Model? I.e., include the Model in your method's arguments, then add the BindingResult to it, which is then available in your view.

model.addAttribute("binding",binding);

I think you may have to use a forward rather than a redirect (in my head I can't remember if a redirect loses the session or not — I could be wrong about this as I don't have any documentation handy, i.e., if you're not getting the BindingResult after adding it to the Model, try using a forward instead to confirm this).

Ichiro Furusato
  • 620
  • 6
  • 12