luizfiliperm opened a new issue, #15281:
URL: https://github.com/apache/grails-core/issues/15281
## Context
We are currently migrating our application from **Grails 2.5.6** to **Grails
4.1.3**. During this process, we noticed a behavioral change in how **Maps
returned by Controllers** are handled before they reach our
Filters/Interceptors.
Our application depends on returning **ordered Maps** in the API responses.
Example:
```groovy
class SampleController {
def example() {
return [
status : "OK",
code : 123,
message: "Success"
]
}
}
```
Using **Grails 2**, when this result reached a **Filter**, the insertion
order was preserved, like exemplified below:
```groovy
class ResponseFilters {
def filters = {
all(controller: '*', action: '*') {
after = { model ->
// Order preserved: status → code → message
println "Model in filter: $model"
}
}
}
}
```
However, using **Grails 4.1.3**, the same Map comes **unordered**, which is
a problem for our context because our application is a **public API**.
Changing the order of the fields unexpectedly can cause considerable changes
for clients, which can also cause non-predictable issues in the future. The
customers expect a consistent and predictable structure, making this behavior
risky for them.
## Identified Behavior Change
Below are the code snippets of the framework that explain the change in
behavior:
### Grails 2 (order preserved)
Implementation in:
- `AbstractGrailsControllerHelper`
https://github.com/apache/grails-core/blob/2.5.x/grails-web-mvc/src/main/groovy/org/codehaus/groovy/grails/web/servlet/mvc/AbstractGrailsControllerHelper.java#L409-L410
This class uses a **LinkedHashMap** to build the Map inside a
`ModelAndView`, which keeps the insertion order.
### Grails 3+ (order lost)
Responsibility moved to:
- `UrlMappingsInfoHandlerAdapter`
https://github.com/apache/grails-core/blob/4.1.x/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/mvc/UrlMappingsInfoHandlerAdapter.groovy#L104-L105
This version uses a **HashMap**, which **does not keep the insertion order**.
## Root Cause
From Grails 3 onward, the internal `Model` passed between controllers and
filters/interceptors is created using a **HashMap**, which causes the insertion
order to be lost.
This is different from Grails 2, where a **LinkedHashMap** was used, and the
order of the fields was always preserved.
---
## Proposed Fix (Implemented and Tested)
We fixed the issue by rewriting the behavior of
`UrlMappingsInfoHandlerAdapter` to instantiate a `LinkedHashMap` instead of a
`HashMap`:
```groovy
// Original behavior:
//...
else if(result instanceof Map) {
String viewName =
controllerClass.actionUriToViewName(action)
def finalModel = new HashMap<String, Object>()
//...
// Updated behavior:
//...
else if(result instanceof Map) {
String viewName =
controllerClass.actionUriToViewName(action)
def finalModel = new LinkedHashMap<String, Object>()
//...
```
This matches the behavior from Grails 2 and makes sure the insertion order
is kept during the whole request lifecycle.
To apply this fix in our application (without changing the Grails source
code), we **recreated the `UrlMappingsInfoHandlerAdapter` class inside our
project**, using the **same package and class name** as the original one.
With this approach, our version is compiled and loaded **before the version
from the Grails web-URL-mappings plugin**, which allows us to override the
default behavior safely.
## Compatibility Notes
We did not notice any behavior changes or regressions after applying this
fix.
`ModelAndView` only requires the model to be a `Map`, and `LinkedHashMap`
fully meets this requirement without affecting any expected behavior.
## Alternative Solution Considered (Not Implemented)
Before overriding `UrlMappingsInfoHandlerAdapter`, we also looked at another
possible solution:
**Creating a custom Bean that implements
`org.grails.web.servlet.mvc.ActionResultTransformer`.**
This transformer is executed during the request handling flow, as shown in
the framework code below:
```groovy
if (actionResultTransformers) {
for (transformer in actionResultTransformers) {
result = transformer.transformActionResult(webRequest, action,
result)
}
}
```
Source:
https://github.com/apache/grails-core/blob/4.0.x/grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/mvc/UrlMappingsInfoHandlerAdapter.groovy#L92-L95
Our idea was to use this hook to intercept the controller result and convert
the returned Map into a `LinkedHashMap` before it reached the interceptors.
Even though this was technically possible, it had an important drawback:
The same interface (`ActionResultTransformer`) is also used by the
`ResponseRenderer`, as shown here:
https://github.com/apache/grails-core/blob/4.0.x/grails-plugin-controllers/src/main/groovy/grails/artefact/controller/support/ResponseRenderer.groovy#L257-L260
Because of that, applying the transformer globally would also change the
behavior of `render` inside controllers, potentially modifying responses and
causing non-predictable issues in other parts of the application.
For this reason, we chose to proceed with the rewritten class approach
instead.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]