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

   ## Summary
   
   This modernizes Grails' content-negotiation defaults for Grails 8, in two 
related parts:
   
   1. **MIME types are now framework defaults.** New applications no longer 
need a `grails.mime.types` block in `application.yml`; the framework supplies 
the full set out of the box, and any configuration you provide is merged over 
it.
   2. **The `Accept` header is honored for all clients by default.** The legacy 
"ignore the `Accept` header for browser user agents" behavior is now opt-in. 
This fixes browser `fetch()`/`axios` calls that request `application/json` but 
receive HTML.
   
   Both changes are backward compatible for typical apps and are covered by 
upgrade notes.
   
   ---
   
   ## Part 1 — MIME types as framework defaults
   
   Previously, every generated app carried a ~15-line `grails.mime.types` 
block, while the framework's built-in fallback (`MimeType.createDefaults()`) 
only knew 6 types. Removing the block silently shrank content negotiation to 
those 6.
   
   This change:
   
   - Expands `MimeType.createDefaults()` to the full set new apps historically 
declared: `all, atom, css, csv, form, html, js, json, multipartForm, pdf, rss, 
text, hal, xml`.
   - Changes `MimeTypesConfiguration` to **merge** a user's `grails.mime.types` 
over the defaults instead of replacing them. A declared extension overrides the 
default for that extension; extensions you don't mention keep their defaults; 
declared extensions are ordered first, so the documented "first entry is the 
default format" contract is preserved.
   - Stops generating the block from Grails Forge (GSP / JSON / Markup view 
features) and from the `web` and `rest-api` profiles.
   
   Result: a new app's `application.yml` is cleaner, and adding one custom type 
no longer wipes the rest.
   
   ---
   
   ## Part 2 — Honor the `Accept` header by default (stop User-Agent sniffing)
   
   ### What changed
   
   `grails.mime.disable.accept.header.userAgents` previously defaulted to 
`['Gecko', 'WebKit', 'Presto', 'Trident']`, which caused Grails to **ignore the 
`Accept` header** for any request whose `User-Agent` matched a major browser 
engine. It now defaults to **unset**, so the `Accept` header is honored for 
every client. The browser blocklist remains available as an explicit opt-in.
   
   ### Why the old default is wrong today
   
   **It works around a problem that no longer exists.** The blocklist dates to 
the Firefox 2/3 era, when browsers sent:
   
   ```
   
text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
   ```
   
   — ranking XML *above* HTML. Modern browsers send a well-formed header that 
ranks HTML highest:
   
   ```
   Chrome / Firefox / Safari:  
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
   ```
   
   `text/html` is q=1.0 and `application/xml` is q=0.9, so ordinary q-value 
negotiation already resolves browser navigations to HTML. No sniffing required.
   
   **It actively breaks modern AJAX.** The old code exempted "XHR" requests — 
but only those carrying `X-Requested-With: XMLHttpRequest`, a header **jQuery** 
adds automatically and **`fetch()` / `axios` do not**. So in a current SPA:
   
   ```js
   fetch('/api/thing', { headers: { Accept: 'application/json' } })
   ```
   
   matched the browser `User-Agent`, the `Accept` header was discarded before 
it was even read, and the response came back as HTML — even though the client 
explicitly asked for JSON. That is the concrete bug this fixes.
   
   ### How Spring Boot / Spring MVC handles it
   
   Spring takes the simpler, opposite approach: **it trusts the client's 
explicit signals and never inspects `User-Agent`.**
   
   - Spring MVC's `ContentNegotiationManager` resolves the representation from 
the `Accept` header (and optionally an explicit `format` parameter). There is 
no User-Agent strategy.
   - A `@RestController` method with `Accept: application/json` selects its 
`HttpMessageConverter` purely from the `Accept` header → JSON, regardless of 
which browser made the request.
   - Spring has moved *further* toward relying only on explicit signals: 
path-extension content negotiation was disabled by default (`favorPathExtension 
= false`) in Spring Framework 5.3+ specifically because deriving the response 
type from an implicit signal (the URL suffix) enabled Reflected File Download 
(RFD) attacks.
   
   Grails 8 now aligns with Spring: honor the explicit `Accept` header; don't 
branch on an ambient, spoofable one.
   
   ### Security / robustness considerations of User-Agent-based negotiation
   
   Beyond correctness, negotiating on `User-Agent` has real downsides:
   
   - **Cache confusion / poisoning.** When a single URL returns different 
representations based on `User-Agent`, a correct implementation must emit 
`Vary: User-Agent`. Grails' UA-based negotiation does not, so a shared / CDN 
cache keyed on the URL can store one representation (say HTML) and serve it to 
a client that needs another (a JSON API consumer), or the reverse. Honoring 
`Accept` removes this class of cache mismatch from the default configuration.
   - **Spoofable, non-contractual signal.** `User-Agent` is entirely 
client-controlled. Gating the response *type* on it makes server behavior 
depend on a value the client can set arbitrarily — fragile to reason about and 
harder to cover with security tooling / WAF rules that expect a stable request 
→ representation contract.
   - **Representation confusion.** Returning HTML to a caller that asked for 
`application/json` can cause client code to mis-handle the body (parsing an 
HTML error page as JSON, or rendering untrusted HTML it never expected).
   
   None of these is a critical vulnerability on its own, but together they are 
concrete reasons the explicit-`Accept` approach is the safer default.
   
   ### Dead-code cleanup
   
   While here, removed a sibling dead path — `if (msie) header = '*/*'` — whose 
guard (`userAgent ==~ /msie(?i)/`) never matches a real Internet Explorer 
`User-Agent` string and so never executed.
   
   ---
   
   ## Compatibility
   
   - **Apps that declare `grails.mime.types`** are unaffected for their 
declared types; previously-omitted types become additionally available via the 
merge, and the first declared entry remains the default format.
   - **Browser page loads** still resolve to HTML, because modern browsers rank 
`text/html` highest — `withFormat { html { … } json { … } }` keeps serving HTML 
to browsers and JSON to clients that ask for it.
   - **To restore the Grails 7 behavior**, set 
`grails.mime.disable.accept.header.userAgents: [Gecko, WebKit, Presto, 
Trident]`.
   - Upgrade notes added (sections 27 and 28 of the Grails 8 upgrade guide).
   
   ## Testing
   
   - Full suites pass for `grails-mimetypes`, `grails-web-common`, and 
`grails-test-suite-web`; Grails Forge view specs pass.
   - New / updated specs cover: the full default MIME set, merge / override / 
ordering of user config, the `Accept` header being honored for non-XHR browser 
requests, and the opt-back-in config restoring the legacy behavior.
   


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