codeconsole opened a new pull request, #16182:
URL: https://github.com/apache/grails-core/pull/16182
## Description
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), and makes `resources:` mappings and
`g:form` cooperate so that scaffolded views keep working when it is switched
off.
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. For a JSON API POST
the cost is only a query-string parse, but for multipart it is the whole body.
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 and applies **any** method name it
is given. Applications that do not need browser method override should be able
to decline both.
Spring Boot has shipped this filter disabled by default since 2.2 and
Micronaut has no equivalent at all, so an off switch also keeps the door open
to changing the default in a future major.
## What changes
**`grails.web.hiddenmethod.filter.enabled`**, gating the filter registration
in `ControllersAutoConfiguration`, plus a marker bean that logs one startup
warning when the override is switched off.
**POST variant routes.** With the override disabled a browser form can no
longer reach the `PUT` and `DELETE` routes a `resources:` mapping generates,
because browsers submit only `GET` and `POST`. In that mode — and *only* that
mode — two extra routes are generated:
| Method | URL | Action |
|---|---|---|
| `POST` | `/books/$id` | `update` |
| `POST` | `/books/$id/delete` | `delete` |
`update` reuses the member URL, so a form's `action` attribute is
byte-identical in both modes. `delete` takes a segment of its own, mirroring
the existing `/books/$id/edit` route. No variant is generated for `patch`,
because `RestfulController.patch()` delegates to `update()` and both resolve to
the same target — a second URL would be a synonym. A singular `resource:`
mapping has no id segment and `POST /book` is already the `save` route, so
there both actions take a segment. `includes:`/`excludes:` and nested
`collection`/`member` blocks propagate.
An application that leaves the override enabled generates exactly the
mappings it does today and pays nothing for the feature.
**`g:form` targets the variant routes automatically**, and stops emitting
the `_method` field when nothing will read it. Scaffolded views and existing
GSP templates therefore need no changes — `<g:form resource="${book}"
method="DELETE">` renders `/books/1/delete` with the override off and
`/books/1` plus `_method` with it on. This required resolving the form's HTTP
method *before* generating the link, which previously happened in the opposite
order. Forms whose target cannot be resolved to a mapping (a literal
`url="/some/path"`) are left alone and log a warning.
**`allowedMethods`** gains `POST` for `delete` in `RestfulController` and in
the scaffolding controller templates, so the variant routes reach their
actions. `update` already permitted `POST`.
## 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.
## Alternative considered
Resolving `_method` during URL matching instead — deleting the filter's
pre-dispatch parameter read without adding any routes or touching `g:form`. It
was prototyped and rejected: it changes only which method string reaches
`matchAll`, while `AllowedMethodsHelper.isAllowed` (and interceptors, and
controller code) still read `request.method`, which stays `POST`. A form submit
routed to `delete` would then be rejected with a 405 by `RestfulController`'s
own `allowedMethods`. Making the override visible to the rest of the stack
requires wrapping the request, and the natural layer to wrap a request for
whole-stack consistency is a servlet filter — which is what already exists.
It is also strictly worse for authorization: with POST variants the URL
still distinguishes `update` from `delete`, so a method-based security rule can
be rewritten path-wise. Resolving `_method` at the mapping leaves `POST
/books/1` meaning both.
## Behaviour change to be aware of
With the override **disabled**, `update` and `delete` become reachable by
`POST` as well as by `PUT` and `DELETE`. A Spring Security rule or servlet
filter matching only `DELETE /books/**` will not cover `POST
/books/$id/delete`. This is the one consequence that fails silently, and it is
called out in the upgrade guide. Applications that leave the override enabled
are unaffected.
## Testing
- `PostOverrideVariantResourceMappingSpec` — route generation, including
`includes:`/`excludes:`, nested resources, singular resources, and that nothing
extra is generated in the default mode
- `FormTagLibHiddenMethodDisabledSpec` — end-to-end through real URL
mappings and real form rendering, covering `resource=`/`action=`, `method=`,
the `url=[resource:…]` map shape used by the Spring Security scaffolded views,
nested resources, and the literal-URL fallback
- `ControllersAutoConfigurationSpec` — registration, the property gate, the
startup warning, user-bean back-off, and the Boot collision
- `RestfulControllerSubclassSpec` — `update` and `delete` accepting a form
`POST`
- `HiddenHttpMethodFilterTests` — extended to cover non-POST requests, empty
and custom parameters, parameter-over-header precedence, case folding, and the
unrestricted-method behaviour that distinguishes this filter from Spring's
## 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]