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]

Reply via email to