jamesfredley opened a new issue, #16127:
URL: https://github.com/apache/grails-core/issues/16127
### Summary
Grails 7 plugins compiled against Spring Framework 6 fail at runtime on
Grails 8 with `IncompatibleClassChangeError` when they pass an `HttpHeaders`
into any API whose parameter is typed `MultiValueMap`. Spring Framework 7
removed `MultiValueMap` from the interfaces implemented by
`org.springframework.http.HttpHeaders`, so bytecode that was valid under Spring
6 is now invalid.
The plugin resolves, compiles, loads, and produces a working `bootJar`. The
failure appears only when the plugin's public API is actually called.
Found while auditing released Grails 7 plugins against `8.0.0-M5`, using
`io.github.gpc:grails-datastore-rest-client-legacy:7.0.3` (latest release,
catalog constraint `7.0.0 > *`).
### Grails Version
8.0.0-M5 (Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8, JDK
21.0.11)
### Steps to Reproduce
In a stock `create-app --profile=web` application on 8.0.0-M5:
```groovy
implementation "io.github.gpc:grails-datastore-rest-client-legacy:7.0.3"
```
Add a trivial controller action that returns `rest-client-ok`, then call the
plugin's documented public API against the running embedded server:
```groovy
@Integration
class RestBuilderIntegrationSpec extends Specification {
@Value('${local.server.port}')
int serverPort
void 'RestBuilder GET invokes the local controller'() {
when:
def response = new
RestBuilder().get("http://127.0.0.1:${serverPort}/audit/ping")
then:
response.status == 200
response.text == 'rest-client-ok'
}
}
```
### Actual Behaviour
```text
java.lang.IncompatibleClassChangeError: Class
org.springframework.http.HttpHeaders
does not implement the requested interface
org.springframework.util.MultiValueMap
at org.springframework.http.HttpHeaders.isEmpty(HttpHeaders.java:1902)
at
org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:947)
at
org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:752)
at
org.springframework.web.client.RestTemplate.execute(RestTemplate.java:697)
at
org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:571)
at
grails.plugins.rest.client.RestBuilder.invokeRestTemplate(RestBuilder.groovy:335)
at
grails.plugins.rest.client.RestBuilder.doRequestInternal(RestBuilder.groovy:299)
at
grails.plugins.rest.client.RestBuilder.doRequestInternal(RestBuilder.groovy:287)
at grails.plugins.rest.client.RestBuilder.get(RestBuilder.groovy:132)
at restclient.RestBuilderIntegrationSpec.RestBuilder GET invokes the
local controller(RestBuilderIntegrationSpec.groovy:19)
```
The application itself started normally - `Tomcat started on port 51899`,
and the plugin appears in the load order as `grailsDatastoreRestClientLegacy
(7.0.3)`.
### Root Cause
This is **not** a mixed-Spring-version problem. I checked both:
- The built `bootJar` contains exactly one Spring Web:
`BOOT-INF/lib/spring-web-7.0.8.jar` (plus `spring-core-7.0.8`,
`spring-webmvc-7.0.8`).
- The isolated Gradle cache for this build only ever downloaded
`spring-web-7.0.8.jar`.
- The plugin jar ships no `org/springframework/**` classes of its own.
The cause is in the plugin's own compiled bytecode.
`grails.plugins.rest.client.RequestCustomizer` holds the headers as
`HttpHeaders`:
```text
private org.springframework.http.HttpHeaders headers;
private org.springframework.util.MultiValueMap<java.lang.String,
java.lang.Object> mvm;
public org.springframework.http.HttpEntity createEntity();
```
and `createEntity()` passes that `HttpHeaders` field into the
`MultiValueMap` parameter of `HttpEntity`:
```text
30: getfield #35 // Field
headers:Lorg/springframework/http/HttpHeaders;
33: invokespecial #270 // Method
org/springframework/http/HttpEntity."<init>":(Ljava/lang/Object;Lorg/springframework/util/MultiValueMap;)V
```
Under Spring 6 this was legal, because `HttpHeaders implements
MultiValueMap<String, String>`. Under Spring 7 it is not: `HttpHeaders` no
longer implements `MultiValueMap`, so the `HttpEntity`'s `MultiValueMap`-typed
header slot now holds an object that does not implement that interface. When
`RestTemplate` later asks the entity for its headers and calls `isEmpty()`, the
interface dispatch fails with `IncompatibleClassChangeError`.
The constructor `HttpEntity(Object, MultiValueMap)` still exists in Spring
7, so nothing fails at resolution, load, or class-verification time - only when
the code path actually runs.
### Expected Behaviour
At minimum this should be a documented Grails 8 upgrade note: `HttpHeaders`
no longer implements `MultiValueMap`, so any plugin or application code passing
`HttpHeaders` where a `MultiValueMap` is expected must be recompiled against
Spring 7, and any such code that is only distributed as a compiled artifact
will fail at runtime rather than at build time.
### Why this is worth tracking here
The blast radius is larger than one plugin. `new HttpEntity(body, headers)`
with an `HttpHeaders` argument is an extremely common Spring idiom, and it was
the natural way to write it for the entire Spring 4/5/6 era. Any Grails 7
plugin that performs an outbound HTTP call is a candidate.
The failure mode is also unusually unhelpful:
- It survives dependency resolution, compilation, plugin loading, and
`bootJar`.
- The error names two Spring classes and no Grails or plugin component.
- It only appears when that specific code path executes, so a smoke test
that merely boots the application will pass.
### Distinct from previously reported issues
This does not match #16122 (Metadata dynamic key access), #16123 (generic
trait with fields / `$Trait$FieldHelper`), #16124 (partial Jackson 2
classpath), #16125 (pre-Apache `org.grails` coordinates), or #16126 (AST
transformation targeting a private trait method). It is a Spring 6 -> 7 binary
incompatibility rather than a Groovy or Grails one.
### Notes
Part of a compatibility sweep of released Grails 7 plugins against 8.0.0-M5.
--
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]