Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected behavior with class-level @RequestMappings [SPR-7707] #12363

Closed
spring-projects-issues opened this issue Nov 1, 2010 · 2 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Milestone

Comments

@spring-projects-issues
Copy link
Collaborator

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:

@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.


Affects: 3.0.5

Referenced from: commits 8762ec9

@spring-projects-issues
Copy link
Collaborator Author

Keith Donald commented

I switched to exclusive use of method-level @RequestMappings (no class-level mappings) in Greenhouse and things there seem to work as expected.

@spring-projects-issues
Copy link
Collaborator Author

Stefan Schmidt commented

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.

@spring-projects-issues spring-projects-issues added type: bug A general bug in: web Issues in web modules (web, webmvc, webflux, websocket) labels Jan 11, 2019
@spring-projects-issues spring-projects-issues added this to the 3.1 M1 milestone Jan 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

2 participants