You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I am observing unexpected behavior in Spring MVC 3.0.5 in our application with @Controllers that use class-level @RequestMappings in conjunction with a "default" or "root" path mapping specified at the method-level (relative to the class-level base path). Rather than try to explain the problem, I'll just illustrate it from several simple use cases in which I've reproduced the unexpected behavior.
Consider the following @Controller definition and the @RequestMapping rules defined within:
@Controller
@RequestMapping("/invite")
public class InviteController {
@RequestMapping(method=RequestMethod.GET)
public void invitePage(@FacebookUserId String facebookUserId, Model model) { ... }
@RequestMapping(value="/accept", method=RequestMethod.GET, params="token")
public String acceptInvitePage(@RequestParam String token, Model model, HttpServletResponse response);
@RequestMapping(value="/accept", method=RequestMethod.POST)
public String acceptInvite(@RequestParam String token, @Valid SignupForm form, BindingResult formBinding, Model model);
}
Now consider the following web requests:
GET /invite
GET /invite/bogus
GET /invite/accept
GET /invite/accept?token=123456789
With the @RequestMapping configuration above, for each web request I list which handler method was invoked and if that was expected:
|GET /invite | invitePage (expected) |
|GET /invite/bogus | 404 (expected) |
|GET /invite/accept | invitePage (NOT expected. Expected 404 or 405, instead a 500 ultimately resulted due to RuntimeException since the invitePage handler was relying on view name translation to kick in, expecting only to be invoked for GET /invite requests) |
|GET /invite/accept?token=123456789 | (acceptInvitePage (expected) |
Attempting to generalize what is happening here, it seems if I do not specify a @RequestMapping path value at the method level, that handler method is treated as the "default fallback" in the case of no other match. However, this default rule seems to only apply if we access a sub-resource path that is referenced in another @RequestMapping rule, such as the '/accept' sub-path here. If you access a completely unknown sub-path, such as 'bogus' here, a 404 is returned. I find this confusing, and would expect a 404 in both cases. With the @Controller code above, I am simply trying to state the following:
invitePage should be the handler for GET /invite requests
acceptInvitePage should be the handler for GET /invite/accept?token={token} requests.
acceptInvite should be the handler for POST /invite/accept requests.
Any other request within the GET /invite path should return a 404.
Unfortunately, if I send a GET request to /accept, invitePage will actually be invoked! Even If I remove the acceptInvitePage handler method, a GET request to /accept will still result in invitePage being invoked. Only if I remove the "acceptInvitePage" AND "acceptInvite" methods, will I then get a 404 if I send a request to GET /invite/accept (or any other path within /invite.)
I reported this issue to Juergen and he advised me to be explicit with my relative-root mappings at the method level by using "/" as the value instead of defining no value at all. I tried that and ended up with code like this:
@Controller
@RequestMapping("/invite")
public class InviteController {
@RequestMapping(value="/", method=RequestMethod.GET)
public void invitePage(@FacebookUserId String facebookUserId, Model model) { ... }
@RequestMapping(value="/accept", method=RequestMethod.GET, params="token")
public String acceptInvitePage(@RequestParam String token, Model model, HttpServletResponse response);
@RequestMapping(value="/accept", method=RequestMethod.POST)
public String acceptInvite(@RequestParam String token, @Valid SignupForm form, BindingResult formBinding, Model model);
}
Here's what happened with this configuration:
|GET /invite | 404 (NOT expected) |
|GET /invite/bogus | 404 (expected) |
|GET /invite/accept | 405 (good enough since only a POST is allowed if no token query parameter is present) |
|GET /invite/accept?token=123456789 | acceptInvitePage (expected) |
Unfortunately, now the root mapping of GET /invite, and any other mapping like it where the "/" value is specified the method level, does not match. That's a show stopper.
Juergen then suggested I try the /path/** syntax at the class-level. I did that and got the following code and results:
@Controller
@RequestMapping("/invite/**")
public class InviteController {
@RequestMapping(value="/", method=RequestMethod.GET)
public void invitePage(@FacebookUserId String facebookUserId, Model model) { ... }
@RequestMapping(value="/accept", method=RequestMethod.GET, params="token")
public String acceptInvitePage(@RequestParam String token, Model model, HttpServletResponse response);
@RequestMapping(value="/accept", method=RequestMethod.POST)
public String acceptInvite(@RequestParam String token, @Valid SignupForm form, BindingResult formBinding, Model model);
}
|GET /invite | invitePage (expected) |
|GET /invite/bogus | 404 (expected) |
|GET /invite/accept | invitePage (NOT expected - Spring MVC appears to fallback to this even though invitePage is mapped explicitly to "/"! Expected 405 |
|GET /invite/accept?token=123456789 | acceptInvitePage (expected) |
So this sort of works; however, the default fallback behavior appears to have changed. Furthermore, I discovered this causes side effects that effect one of my other @Controllers, TwitterInviteController which handles a nested path under the /invite namespace. Specifically, that @Controller is now defined like this:
@Controller
@RequestMapping("/invite/twitter")
public class TwitterInviteController {
@RequestMapping(value="/", method=RequestMethod.GET)
public void friendFinder(Account account, Model model) { ... }
}
With the class-level mapping of /invite/** in InviteController, TwitterInviteController works: if I send a GET request to /invite/twitter, friendFinder is called. However, if I change /invite/** in InviteController back to simply "/invite/", TwitterInviteController no longer works. Accessing GET /invite/twitter returns a 404 and Spring MVC warns of no matching handler method. I became concerned seeing a @RequestMapping rule in one @Controller effect another one defined independently.
Also shown above with /invite/**, sending a GET request to /invite/accept caused the invitePage handler mapped to "/" to be invoked. I cannot explain why this happened, since I did define the "/" value explicitly.
Finally, I tried /invite/* just to see what would happened in that case. I got the same behavior as the /invite option, which I did not expect.
So at this point, I am confused over what I should be doing here to get the behavior I want. I am seriously considering defining all @RequestMapping path mappings explicitly at the method level for the time being, and not using the class-level mapping feature at all until its rules are more clearly defined.
On the Roo side we should see this problem not as a problem that requires changes but rather as something that comes somewhat unexpected ;)
Roo does use type level @RequestMapping("/owners") in conjunction with method level @RequestMapping(method = RequestMethod.GET). Since no value like "/" is defined in the method level mapping, all should be fine.
However, I am somewhat surprised to hear that a POST submission will also fallback to the GET method as shown above in cases where no explicit POST mapping is defined. I would have expected to see a 404 return code rather than an exception caused by the fact that my GET method does not receive the (potentially required) method parameters it expects.
In a Roo MVC view this will then render a view which indicates 'an internal error occured' rather than 'resource not found', so not too bad - just awkward.
Keith Donald opened SPR-7707 and commented
I am observing unexpected behavior in Spring MVC 3.0.5 in our application with
@Controllers
that use class-level@RequestMappings
in conjunction with a "default" or "root" path mapping specified at the method-level (relative to the class-level base path). Rather than try to explain the problem, I'll just illustrate it from several simple use cases in which I've reproduced the unexpected behavior.Consider the following
@Controller
definition and the@RequestMapping
rules defined within:Now consider the following web requests:
With the
@RequestMapping
configuration above, for each web request I list which handler method was invoked and if that was expected:|GET /invite | invitePage (expected) |
|GET /invite/bogus | 404 (expected) |
|GET /invite/accept | invitePage (NOT expected. Expected 404 or 405, instead a 500 ultimately resulted due to RuntimeException since the invitePage handler was relying on view name translation to kick in, expecting only to be invoked for GET /invite requests) |
|GET /invite/accept?token=123456789 | (acceptInvitePage (expected) |
Attempting to generalize what is happening here, it seems if I do not specify a
@RequestMapping
path value at the method level, that handler method is treated as the "default fallback" in the case of no other match. However, this default rule seems to only apply if we access a sub-resource path that is referenced in another@RequestMapping
rule, such as the '/accept' sub-path here. If you access a completely unknown sub-path, such as 'bogus' here, a 404 is returned. I find this confusing, and would expect a 404 in both cases. With the@Controller
code above, I am simply trying to state the following:Unfortunately, if I send a GET request to /accept, invitePage will actually be invoked! Even If I remove the acceptInvitePage handler method, a GET request to /accept will still result in invitePage being invoked. Only if I remove the "acceptInvitePage" AND "acceptInvite" methods, will I then get a 404 if I send a request to GET /invite/accept (or any other path within /invite.)
I reported this issue to Juergen and he advised me to be explicit with my relative-root mappings at the method level by using "/" as the value instead of defining no value at all. I tried that and ended up with code like this:
Here's what happened with this configuration:
|GET /invite | 404 (NOT expected) |
|GET /invite/bogus | 404 (expected) |
|GET /invite/accept | 405 (good enough since only a POST is allowed if no token query parameter is present) |
|GET /invite/accept?token=123456789 | acceptInvitePage (expected) |
Unfortunately, now the root mapping of GET /invite, and any other mapping like it where the "/" value is specified the method level, does not match. That's a show stopper.
Juergen then suggested I try the /path/** syntax at the class-level. I did that and got the following code and results:
|GET /invite | invitePage (expected) |
|GET /invite/bogus | 404 (expected) |
|GET /invite/accept | invitePage (NOT expected - Spring MVC appears to fallback to this even though invitePage is mapped explicitly to "/"! Expected 405 |
|GET /invite/accept?token=123456789 | acceptInvitePage (expected) |
So this sort of works; however, the default fallback behavior appears to have changed. Furthermore, I discovered this causes side effects that effect one of my other
@Controllers
, TwitterInviteController which handles a nested path under the /invite namespace. Specifically, that@Controller
is now defined like this:With the class-level mapping of /invite/** in InviteController, TwitterInviteController works: if I send a GET request to /invite/twitter, friendFinder is called. However, if I change /invite/** in InviteController back to simply "/invite/", TwitterInviteController no longer works. Accessing GET /invite/twitter returns a 404 and Spring MVC warns of no matching handler method. I became concerned seeing a
@RequestMapping
rule in one@Controller
effect another one defined independently.Also shown above with /invite/**, sending a GET request to /invite/accept caused the invitePage handler mapped to "/" to be invoked. I cannot explain why this happened, since I did define the "/" value explicitly.
Finally, I tried /invite/* just to see what would happened in that case. I got the same behavior as the /invite option, which I did not expect.
So at this point, I am confused over what I should be doing here to get the behavior I want. I am seriously considering defining all
@RequestMapping
path mappings explicitly at the method level for the time being, and not using the class-level mapping feature at all until its rules are more clearly defined.Affects: 3.0.5
Referenced from: commits 8762ec9
The text was updated successfully, but these errors were encountered: