You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@grails.apache.org by "codeconsole (via GitHub)" <gi...@apache.org> on 2026/08/20 20:10:21 UTC
[PR] feat: make the hidden HTTP method filter optional [grails-core]
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] feat: make the hidden HTTP method filter optional [grails-core]
Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole closed pull request #16182: feat: make the hidden HTTP method filter optional
URL: https://github.com/apache/grails-core/pull/16182
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] feat: make the hidden HTTP method filter optional [grails-core]
Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #16182:
URL: https://github.com/apache/grails-core/pull/16182#issuecomment-5361366621
Alternative implementation opened as #16183: same `grails.web.hiddenmethod.filter.enabled` property, but with the filter off it relocates the `_method` override into the dispatcher rather than generating POST variant routes. The two are mutually exclusive — #16183 carries a comparison table. Whichever direction reviewers prefer, the other should be closed.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] feat: make the hidden HTTP method filter optional [grails-core]
Posted by "codeconsole (via GitHub)" <gi...@apache.org>.
codeconsole commented on PR #16182:
URL: https://github.com/apache/grails-core/pull/16182#issuecomment-5363218894
Superseded by #16183, which the discussion converged on — closing this in its favour.
The short version of why: this PR kept browser forms working by generating two POST routes (`POST /books/$id` → update and `POST /books/$id/delete` → delete) so that no server-side method override was needed at all. #16183 keeps the first of those routes but reaches `delete` through `_method` resolved inside the dispatcher, which avoids adding a second URL to every resource.
Two findings from this PR carry over and are worth preserving:
- **#9926 was never actually fixed for `resources:` mappings.** `RestfulController` has declared `update: ['PUT', 'POST']` since 2016, but no route was ever generated to deliver a POST to `update`, so the permission has been unreachable. Both PRs add `POST /$controller/$id`; #16183 carries it.
- **A startup failure new in 8.0**: `spring.mvc.hiddenmethod.filter.enabled=true` collides with Grails' identically-named bean and fails startup with a `BeanDefinitionOverrideException`. Fixed in #16183 too.
The `FormTagLib` restructure here — reordering method resolution so the form action could be rewritten to `/books/$id/delete` — is not needed by the converged design, which never changes the form's action URL. That was the riskiest change in either branch, so avoiding it is a real gain.
--
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: notifications-unsubscribe@grails.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org