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]