codeconsole opened a new pull request, #16183:
URL: https://github.com/apache/grails-core/pull/16183

   ## Description
   
   > **This is an alternative to #16182.** Both add 
`grails.web.hiddenmethod.filter.enabled`; they differ in what happens when it 
is `false`. They are mutually exclusive — please pick one. A comparison is at 
the end.
   
   Grails registers `org.grails.web.filters.HiddenHttpMethodFilter` 
unconditionally, rewriting a `POST` into a `PUT`, `PATCH` or `DELETE` when the 
request carries a `_method` parameter or an `X-HTTP-Method-Override` header. 
There is no way to turn it off.
   
   This PR adds `grails.web.hiddenmethod.filter.enabled` (default `true`, so 
existing applications are unchanged). Switching it off does not switch method 
override off — it **moves the override inside the dispatcher**.
   
   No approved issue exists, so — background:
   
   **Why an off switch is worth having.** The filter reads a request parameter 
*before* the dispatcher runs. `ControllersAutoConfiguration` attaches a 
`MultipartConfigElement` to the dispatcher servlet registration, and Tomcat 
resolves the mapped servlet's multipart config at filter time, so 
`request.getParameter("_method")` on a `multipart/form-data` POST triggers full 
container-side multipart parsing — temporary files and all — before any routing 
or authorization decision has been made, and outside 
`GrailsDispatcherServlet`'s `MultipartException` handling.
   
   There is also a policy dimension: the Grails filter is deliberately wider 
than Spring's. Spring's `HiddenHttpMethodFilter` reads only the `_method` 
parameter and honours only `PUT`, `PATCH` and `DELETE`; the Grails one also 
trusts the `X-HTTP-Method-Override` header — which any client can set — and 
applies **any** method name it is given, including `GET`.
   
   ## What changes
   
   `GrailsDispatcherServlet` resolves the override in `checkMultipart`, which 
runs **after** multipart resolution and **after** the filter chain, and 
publishes the wrapped request through `GrailsWebRequest`. That last part is 
what makes it work end to end: a controller's `request`, `allowedMethods`, 
interceptors and URL mapping resolution all read through the Grails API and 
therefore agree on the method. `UrlMappingsHandlerMapping` resolves the same 
override independently so mapping resolution is correct on its own terms. Both 
delegate to a new `org.grails.web.util.HiddenHttpMethod` so the rules cannot 
drift.
   
   Requests without an override are returned untouched, leaving multipart 
handling exactly as it was.
   
   The resolution is deliberately narrower than the filter it stands in for:
   
   | | Filter (default) | Dispatcher (filter off) |
   |---|---|---|
   | `_method` parameter | yes | yes |
   | `X-HTTP-Method-Override` header | yes | **no** |
   | Methods accepted | any, including `GET` | `PUT`, `PATCH`, `DELETE` only |
   
   **Forms, scaffolded views and GSP templates are unchanged in either mode.** 
No new URLs, no route-count increase, no `g:form` changes.
   
   ## Also fixed: a startup failure that is new in 8.0
   
   Boot's `WebMvcAutoConfiguration` registers its own hidden-method filter 
under the same `hiddenHttpMethodFilter` bean name, and its 
`@ConditionalOnMissingBean` keys on 
`org.springframework.web.filter.HiddenHttpMethodFilter`, which the Grails 
`FilterRegistrationBean` does not satisfy. With bean-definition overriding 
disabled by default, setting `spring.mvc.hiddenmethod.filter.enabled=true` 
therefore failed application startup with a `BeanDefinitionOverrideException`. 
Grails' registration now backs off when Boot's property is explicitly enabled.
   
   This could not occur in 7.x, where `@EnableWebMvc` kept Boot's bean from 
existing at all, so it arrived with the `@EnableWebMvc` removal in 8.0.
   
   ## Behaviour change to be aware of
   
   With the filter **disabled**, servlet filters and the Spring Security filter 
chain see the request's real `POST` method, because the rewrite now happens 
inside the dispatcher. A security rule matching `DELETE /books/**` will not 
fire for a browser form submit, and since the URL is unchanged, `update` and 
`delete` are not distinguishable by path either. **This is the one consequence 
that fails silently** and it is called out in the upgrade guide. Applications 
that leave the filter enabled are unaffected.
   
   ## How this compares to #16182
   
   #16182 takes the other route: with the override off it generates `POST` 
variant routes (`POST /books/$id` → `update`, `POST /books/$id/delete` → 
`delete`) and has `g:form` target them, so there is genuinely **no** 
server-side method override in that mode.
   
   | | #16182 (POST variants) | this PR (dispatcher) |
   |---|---|---|
   | Production diff | ~229 lines, 7 files, 6 modules | ~210 lines, 7 files, 6 
modules |
   | `FormTagLib` | restructured | untouched |
   | New public URLs | 2 per `resources` block | none |
   | Route count when disabled | +2 per resource | unchanged |
   | Removes server-side override | **yes** | no — relocates it |
   | Security sees | `POST /books/1/delete` | `POST /books/1` |
   | Can a path rule tell `delete` from `update` | **yes** | no |
   | Opens flipping the default in a future major | **yes** | no, `_method` 
stays |
   
   The two are close in size, which is worth stating plainly because it was not 
obvious up front. This approach
   looked considerably smaller when it only changed which method string reached 
`matchAll` — but that version was
   broken: `AllowedMethodsHelper` reads the controller's `request`, which still 
reported `POST`, so a form submit
   routed to `delete` was rejected with a 405 by `RestfulController`'s own 
`allowedMethods`. Making the override
   visible to the rest of the stack required the request wrapper and the 
`GrailsWebRequest` publication, and that
   is most of the difference.
   
   So the choice is not really about size. Choose #16182 to follow Spring Boot 
and Micronaut toward no
   server-side method override at all, and to keep a path distinction available 
to authorization rules. Choose
   this PR to keep `_method` as the permanent mechanism, add no URL surface, 
and leave `g:form` untouched.
   
   ## Testing
   
   - `HiddenHttpMethodSpec` — the resolution rules, including that the header 
is not read and that `GET`/`TRACE`/unknown names are refused
   - `GrailsDispatcherServletHiddenMethodSpec` — that the dispatched request 
*and* `GrailsWebRequest.currentRequest` both report the overridden method, 
which is what `allowedMethods` reads, and that a request without an override is 
returned untouched
   - `HiddenHttpMethodHandlerMappingSpec` — a form `POST` reaching the `PUT`, 
`PATCH` and `DELETE` routes of a `resources:` mapping, and being ignored in the 
default mode
   
   ## Documentation
   
   `grails-doc` upgrade guide section 45 and the REST guide's *Linking to 
Resources* page.
   
   ---
   
   Generative AI tooling (Claude Code) was used in preparing this contribution, 
in line with the [ASF policy on generative 
tooling](https://www.apache.org/legal/generative-tooling.html). All changes 
were reviewed and verified against the project's test and style gates by the 
submitter.
   
   https://claude.ai/code/session_01Pwd8dRc4WWHEPpbgrmxZmn
   


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