Redirection from Namespaced Controllers in Grails
Post requests in web applications can often confuse less technically savvy users and in the worst case even cause inconsistent data if a non-idempotent post request is repeated. Post/Redirect/Get is the design pattern that's typically used to avoid most of the undesired side effects of post requests. Of course, the pattern can easily be implemented in Grails as well. Though, when using namespaced controllers, things can get a bit more complicated.
Benefits of Post/Request/Get Pattern
In a typical basic implementation a post response will directly include the page content. When the user will navigate back or forward to such a page, or try to refresh it, the browser won't display the page. Instead, it will show a warning, asking the user whether she wants to resubmit the post request. Unless she agrees, the page won't be displayed again. If she does agree, the same post request will be sent to the server once again, potentially repeating any side effects, the original request might have caused, e.g. repeating the order that has just been completed.
To avoid this behavior, the post request should perform a redirect to a different page (send a 302 Found or 303 See Other status code), containing the actual response. By doing this, the post request will be excluded from the browser history, completely preventing the user to unknowingly repeat it. When navigating forward and back, the browser will typically just show the cached page it previously received. If the user decides to refresh the page, a new get request will be sent for the page the response was redirected to. This will make the user experience much more pleasant.
Implementing the Pattern in Grails
Making the pattern work in Grails is simple enough. Instead of returning the model or calling render
directly in the action mapped to the post request, this will be done in another action. The post will conclude with a redirect
call to it. Any parameters required by the second action can be passed as redirect
arguments. For structured data that can't be passed like that, flash
session storage can be used.
class MyController {
def submit() {
// process the request first
// ...
// use session to pass complex data
flash.result = resultInstance
// pass standard parameters as redirect arguments
redirect(action: 'results', param1 = value
}
def results() {
// retrieve additional data, if necessary
// ...
// pass everything to the view
return [result: flash.result, param1: params.param1]
}
}
Namespaced Controllers
Packages don't play any role, when mapping requests to controllers. Because of this, controllers were initially required to have unique names. With the introduction of namespaces, this has changed. Controllers can now have a static namespace
property to different between them, even if they have the same name.
class MyController {
static namespace = 'myNamespace'
// action methods
// ...
}
By including namespace in UrlMappings
multiple controllers in different packages can now have the same name (although it's not required, it's a good idea for the package to match the controller namespace).
class UrlMappings {
static mappings = {
'/path' (controller: 'my', namespace: 'myNamespace')
'/anotherPath' (controller: 'my', namespace: 'otherNamespace')
}
}
Not only in UrlMappings
, namespace must be included whenever a namespaced controller is being referred to, for the mapping and reverse mapping to work correctly, e.g. when specifying the target action for a form
or a redirect
. Also, the namespace affects the search location for views: they must be located in views/myNamespace/my/action
, instead of views/my/action
.
If controllers specify a namespace, but their names are still globally unique, specifying the namespace in UrlMappings
is not required any more. For everything to work correctly, such controller must now never be referred to using a namespace. The controller's namespace will still affect the search path for the views, though. They can be placed in a folder matching the namespace, nevertheless, making it easier to organize the views in a larger application.
There's another gotcha related to this scenario, that bit me recently. When calling a redirect
from a namespaced controller, its namespace will automatically be used when resolving the target action. If this namespace isn't specified in UrlMappings
, reverse mapping won't be able to find the action in the same controller:
Error: Page Not Found (404)
Path: /my/index
Conventions in Grails make troubleshooting such issues quite tricky. Since reverse mapping couldn't find a matching record in UrlMappings
, it reverted to the default URL structure: /$controller/$action
. To fix the issue, redirect
must explicitly clear the namespace value:
redirect(action: 'index', namespace: null)
Reverse mapping will now find the matching record in UrlMappings
with no namespace defined and redirect to the URL defined there.
To make matters worse, reverse mapping adheres to the specified methods in UrlMappings
, as well.
class UrlMappings {
static mappings = {
'/path' (controller: 'my', namespace: 'myNamespace', method: 'GET')
}
}
Redirecting from a method handling a post request to an action that's mapped for get requests only, will result in failed reverse mapping, again resorting to default URL structure: /$controller/$action
. Lesson learned: don't limit a mapping to get method if you intend to redirect post requests to it.