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]

Reply via email to