Repository navigation
CloudEvents incompatiable with Spring-framework 7.x #704
Description
Activity
Created fork to update to Spring Framework 7 and JDK 25; so we've a plan on our end how to do this if you were to reject the PR that I will create.
Reacted by Raúl Maté Cinza, Daniel Jipa, Elias Thouant and Sujeet Kumar Singh#739 provided some changes.
What's left for this Issue?
I've got these issues too:
Spring Framework 7
HttpHeadersCompatibilitySpring Framework 7 changed
org.springframework.http.HttpHeadersso it no longer implements theMap/MultiValueMapcontract.Current versions of
io.cloudevents:cloudevents-springpassHttpHeadersdirectly to:HttpMessageFactory.createReaderFromMultimap(headers, body.get());
The CloudEvents HTTP reader expects a
Map<String, List<String>>, so applications running Spring Framework 7 fail while deserializing inbound CloudEvents with:java.lang.IncompatibleClassChangeError: Class org.springframework.http.ReadOnlyHttpHeaders does not implement the requested interface java.util.MapIn WebFlux, Reactor treats this linkage error as fatal. The HTTP exchange can remain incomplete, causing callers such as
WebTestClientto report a timeout instead of the underlying exception.Impact
- Inbound structured and binary CloudEvent handling fails on Spring Framework 7.
- Both Spring MVC and WebFlux CloudEvents HTTP readers are affected because they use the shared
CloudEventHttpUtils.toReader(...)implementation. - Outbound CloudEvent serialization is not affected by this specific issue.
Temporary Application Workaround
Until a CloudEvents SDK release includes the upstream fix, applications can register a compatible
HttpMessageReader<CloudEvent>that copies request headers into a standard mutableMap<String, List<String>>before invokingHttpMessageFactory.createReaderFromMultimap(...).Upstream Status
- Issue: CloudEvents incompatiable with Spring-framework 7.x #704
- Fix: Moving the baseline to Java 17 / Jakarta EE 10 #739
PR #739 updates
CloudEventHttpUtils.toReader(...)to copyHttpHeadersentries into a regular map before passing them to the CloudEvents HTTP message factory.The fix is merged but is not included in the currently released
4.1.1artifact.FYI wanted to share my work around:
Spring Boot 4.x / Spring Framework 7 CloudEvents Workaround (Detailed)
This write-up documents the workaround we applied while running on Spring Boot 4.x (Spring Framework 7) with
io.cloudevents:cloudevents-spring:4.1.1.Problem Summary
Spring Framework 7 changed
HttpHeadersso it no longer implements theMap/MultiValueMapcontract expected by older CloudEvents Spring reader code paths.CloudEvents 4.1.1 still routes inbound requests through logic equivalent to:
HttpMessageFactory.createReaderFromMultimap(headers, body.get());
When
headersis a Spring 7HttpHeadersimplementation, this can fail with:java.lang.IncompatibleClassChangeError: Class org.springframework.http.ReadOnlyHttpHeaders does not implement the requested interface java.util.MapIn WebFlux test clients, this can appear as request timeouts because the exchange never completes cleanly.
Workaround Strategy
Instead of using
CloudEventHttpMessageReaderdirectly, register a compatible reader that:- Reads request body bytes exactly once.
- Copies request headers into a plain mutable
MultiValueMap<String, String>. - Calls
HttpMessageFactory.createReaderFromMultimap(...)with that copied map.
1) Compatible Reader (full snippet)
package com.example.app.cloudevent; import io.cloudevents.CloudEvent; import io.cloudevents.core.message.MessageReader; import io.cloudevents.http.HttpMessageFactory; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpInputMessage; import org.springframework.http.codec.HttpMessageReader; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StreamUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * CloudEvents reader compatible with Spring Framework 7 HttpHeaders behavior. */ public class CompatibleCloudEventHttpMessageReader implements HttpMessageReader<CloudEvent> { @Override public List<MediaType> getReadableMediaTypes() { return Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL); } @Override public boolean canRead(ResolvableType elementType, MediaType mediaType) { return CloudEvent.class.isAssignableFrom(elementType.toClass()); } @Override public Flux<CloudEvent> read( ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) { return Flux.from(readMono(elementType, message, hints)); } @Override public Mono<CloudEvent> readMono( ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) { MultiValueMap<String, String> headers = copyHeaders(message.getHeaders()); return DataBufferUtils.join(message.getBody()) .map( buffer -> { try (InputStream inputStream = buffer.asInputStream(true)) { return StreamUtils.copyToByteArray(inputStream); } catch (Exception ex) { throw new IllegalArgumentException(ex); } }) .map(bytes -> HttpMessageFactory.createReaderFromMultimap(headers, bytes)) .map(MessageReader::toEvent); } private static MultiValueMap<String, String> copyHeaders(HttpHeaders sourceHeaders) { MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); sourceHeaders.forEach((name, values) -> headers.put(name, new ArrayList<>(values))); return headers; } }
2) WebFlux Codec Registration Change
package com.example.app.cloudevent; import io.cloudevents.jackson.JsonFormat; import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.config.WebFluxConfigurer; @Configuration public class CloudEventWebFluxConfiguration implements WebFluxConfigurer { @Override public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { // Use compatible reader for Spring 7 HttpHeaders behavior configurer.customCodecs().register(new CompatibleCloudEventHttpMessageReader()); configurer.customCodecs().register(new CloudEventHttpMessageWriter()); } @Bean public JsonFormat cloudEventJsonFormat() { return new JsonFormat(); } }
Upstream Tracking
- Issue: CloudEvents incompatiable with Spring-framework 7.x #704
- PR: Moving the baseline to Java 17 / Jakarta EE 10 #739
PR #739 includes the same conceptual fix (copying headers to a regular map before reading).
Planned Cleanup Once Released
After a CloudEvents release containing PR #739 is available in your artifact repositories:
- Upgrade
cloudevents.versioninpom.xml. - Restore standard
CloudEventHttpMessageReaderregistration. - Remove
CompatibleCloudEventHttpMessageReader. - Re-run full test suite.
#746 includes Spring Boot 4/Framework 7 support, I'm working on getting it merged.
I know there is a similar issue up already about upgrading to Spring-boot 2.7.18 but this project has lagged so far behind now that you can't even launch it with using an up-to-date ecosystem since Spring Framework 7 introduced a breaking change:
Stacktrace you get when running latest release of cloudevent and you try to receive a message:
"Class org.springframework.http.HttpHeaders does not implement the requested interface java.util.Map","error.stack_trace":"java.lang.IncompatibleClassChangeError: Class org.springframework.http.HttpHeaders does not implement the requested interface java.util.Map\n\tat io.cloudevents.http.HttpMessageFactory.lambda$createReaderFromMultimap$5(HttpMessageFactory.java:112)\n\tat io.cloudevents.http.HttpMessageFactory.createReader(HttpMessageFactory.java:70)\n\tat io.cloudevents.http.HttpMessageFactory.createReaderFromMultimap(HttpMessageFactory.java:114)\n\tat io.cloudevents.spring.http.CloudEventHttpUtils.toReader(CloudEventHttpUtils.java:55)\n\tat io.cloudevents.spring.mvc.CloudEventHttpMessageConverter.readInternal(CloudEventHttpMessageConverter.java:54)\n\tat io.cloudevents.spring.mvc.CloudEventHttpMessageConverter.readInternal(CloudEventHttpMessageConverter.java:39)\n\tat org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:196)\n\tat