Starting with Spring Boot 2.5.0-M3, Spring HATEOAS sends application/hal+json responses even though the request contains Accept: application/json. This change was introduced with commit 5863edfdda1b572c8e5e25ae20792898cca79f21 and is considered a breaking change and not documented in the release notes for 2.5. This change results in different rendering of "links" (now as object _links and before as array links) and collections (now rendered as object _embedded and before as array content).
Setting spring.hateoas.use-hal-as-default-json-media-type to false will then lead to the following internal server error.
org.springframework.http.converter.HttpMessageNotWritableException: No Encoder for [foo.model.EmptyResponse] with preset Content-Type 'null'
at org.springframework.web.reactive.result.method.annotation.AbstractMessageWriterResultHandler.writeBody(AbstractMessageWriterResultHandler.java:181) ~[spring-webflux-5.3.7.jar:5.3.7]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ? Handler foo.controller.FooController#change(List, Long) [DispatcherHandler]
|_ checkpoint ? org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.web.filter.reactive.ServerWebExchangeContextFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.authorization.AuthorizationWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.authentication.logout.LogoutWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
|_ checkpoint ? foo.auth.JwtAuthFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.web.cors.reactive.CorsWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
|_ checkpoint ? HTTP POST "/vertragsliste" [ExceptionHandlingWebHandler]
Stack trace:
at org.springframework.web.reactive.result.method.annotation.AbstractMessageWriterResultHandler.writeBody(AbstractMessageWriterResultHandler.java:181) ~[spring-webflux-5.3.7.jar:5.3.7]
at org.springframework.web.reactive.result.method.annotation.AbstractMessageWriterResultHandler.writeBody(AbstractMessageWriterResultHandler.java:105) ~[spring-webflux-5.3.7.jar:5.3.7]
at org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler.handleResult(ResponseBodyResultHandler.java:86) ~[spring-webflux-5.3.7.jar:5.3.7]
at org.springframework.web.reactive.DispatcherHandler.handleResult(DispatcherHandler.java:179) ~[spring-webflux-5.3.7.jar:5.3.7]
at org.springframework.web.reactive.DispatcherHandler.lambda$handle$2(DispatcherHandler.java:154) ~[spring-webflux-5.3.7.jar:5.3.7]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:249) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.complete(MonoIgnoreThen.java:284) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onNext(MonoIgnoreThen.java:187) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoZip$ZipCoordinator.signal(MonoZip.java:251) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoZip$ZipInner.onNext(MonoZip.java:336) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onNext(ReactorContextTestExecutionListener.java:120) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:100) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:110) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoCurrentContext.subscribe(MonoCurrentContext.java:36) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:128) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:236) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:203) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onComplete(ReactorContextTestExecutionListener.java:130) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onComplete(MonoFlatMap.java:181) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators.complete(Operators.java:136) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:120) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onNext(ReactorContextTestExecutionListener.java:120) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:139) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:169) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2193) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2067) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onSubscribe(ReactorContextTestExecutionListener.java:115) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onSubscribe(MonoPeekTerminal.java:152) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onComplete(MonoFlatMap.java:181) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onComplete(FluxFilterFuseable.java:171) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onComplete(FluxPeekFuseable.java:595) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2399) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.request(FluxPeekFuseable.java:437) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:110) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onSubscribe(FluxPeekFuseable.java:471) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onComplete(MonoPeekTerminal.java:299) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:148) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:113) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:100) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onNext(ReactorContextTestExecutionListener.java:120) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:249) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:100) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:151) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:110) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoCurrentContext.subscribe(MonoCurrentContext.java:36) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onNext(FluxFilterFuseable.java:118) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.request(FluxFilterFuseable.java:191) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:169) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:110) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableSubscriber.onSubscribe(FluxFilterFuseable.java:87) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekConditionalSubscriber.onComplete(FluxPeekFuseable.java:940) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:84) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onComplete(ReactorContextTestExecutionListener.java:130) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2399) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2193) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2067) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onSubscribe(ReactorContextTestExecutionListener.java:115) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onComplete(FluxSwitchIfEmpty.java:81) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoNext$NextSubscriber.onComplete(MonoNext.java:102) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.checkTerminated(FluxFlatMap.java:846) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.drainLoop(FluxFlatMap.java:608) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.drain(FluxFlatMap.java:588) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onComplete(FluxFlatMap.java:465) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onComplete(FluxPeekFuseable.java:277) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:292) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:228) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:371) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:108) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxFilter$FilterSubscriber.onComplete(FluxFilter.java:166) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxMap$MapConditionalSubscriber.onComplete(FluxMap.java:269) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.signalCached(MonoCacheTime.java:337) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.MonoCacheTime$CoordinatorSubscriber.onNext(MonoCacheTime.java:354) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.6.jar:3.4.6]
at org.springframework.security.test.context.support.ReactorContextTestExecutionListener$DelegateTestExecutionListener$SecuritySubContext.onNext(ReactorContextTestExecutionListener.java:120) ~[spring-security-test-5.5.0.jar:5.5.0]
at reactor.core.publisher.FluxSubscribeOnCallable$CallableSubscribeOnSubscription.run(FluxSubscribeOnCallable.java:251) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:68) ~[reactor-core-3.4.6.jar:3.4.6]
at reactor.core.scheduler.SchedulerTask.call(SchedulerTask.java:28) ~[reactor-core-3.4.6.jar:3.4.6]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Comment From: wilkinsona
Sending HAL-formatted responses when accepting application/json should have been what happened in prior to 2.5. I'm almost certain that was the case with MVC and it was the intention with WebFlux but perhaps it doesn't behave as intended.
To help us to investigate, can you please provide a complete yet minimal sample that works in one way with Spring Boot 2.5 and in another way when downgraded to Spring Boot 2.4? You can share such a sample with us by zipping it up and attaching it to this issue or pushing it to a separate repository on GitHub.
Comment From: bclozel
This could be a unintended behavior introduced with spring-projects/spring-hateoas#1453 (requested in spring-projects/spring-framework#26212) - the custom ObjectMapper might not be properly selected depending on the chosen media type.
A sample application is still very much needed.
Comment From: sephiroth-j
It is okay if this change (sending HAL-formatted responses when accepting application/json) is intended. But having set spring.hateoas.use-hal-as-default-json-media-type=false should not make the application unusable. So the question would be how do we restore the old behavior?
I will try to create a sample application.
Comment From: sephiroth-j
Here is the sample application. spring-boot-issue-26814.zip
With Spring Boot 2.4.6 you can make a GET request such as http://localhost:8080/demo/foo with your browser and it will produce an response like this of type application/json.
{"prop":"foo","links":[{"rel":"self","href":"/demo/{property}"}]}
With Spring Boot 2.5.0 this will produce {"prop":"foo","_links":{"self":{"href":"/demo/{property}","templated":true}}}. With spring.hateoas.use-hal-as-default-json-media-type=false you will get the internal server error mentioned above.
Comment From: wilkinsona
Thanks for the sample. The problem is that Spring HATEOAS has customised the Jackson2JsonEncoder with a type-specific ObjectMapper registration for org.springframework.hateoas.RepresentationModel of which your DemoResponse is a subclass. This registration is for the application/hal+json media type but we're trying to produce application/json.DemoResponse being a RepresentationModel subclass and the registration's media type not including application/json results in Jackson2JsonEncoder returning false when asked if it can encode DemoResponse. This is quite similar to https://github.com/spring-projects/spring-hateoas/issues/1547 that @odrotbohm is already investigating.
Comment From: wilkinsona
In this particular case I think things would work if selectObjectMapper fell back to the default ObjectMapper when none of the registrations match rather than returning null as it does at the moment. I'm pretty sure that would have plenty of unexpected side-effects that may well break other use cases of which I am not aware. I think we may need some help from @rstoyanchev here.
Comment From: sephiroth-j
It is possible to register the default ObjectMapper for application/json using WebFluxConfigurer.configureHttpMessageCodecs(ServerCodecConfigurer) for type org.springframework.hateoas.RepresentationModel. But this is basically the same as if spring.hateoas.use-hal-as-default-json-media-type is set to true. That means: links and collections are rendered in HAL-style. I wonder if it is even possible to get back the Atom-style rendering for application/json.
Comment From: wilkinsona
Spring HATEOAS doesn't register anything with the default object mapper so I'd expect the response to be in its default form rather than customised to match HAL. To verify this, I added the following bean to your sample:
@Bean
public WebFluxConfigurer webFluxConfigurer(ObjectMapper objectMapper) {
return new WebFluxConfigurer() {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().configureDefaultCodec((codec) -> {
if (codec instanceof Jackson2JsonEncoder) {
((Jackson2JsonEncoder)codec).registerObjectMappersForType(RepresentationModel.class,
(registrations) -> registrations.put(MediaType.APPLICATION_JSON, objectMapper));
}
});
}
};
}
This results in the links being an array as they were in 2.4.x:
{"prop":"foo","links":[{"rel":"self","href":"/demo/{property}"}]}
Comment From: sephiroth-j
That's good news, thanks! I tried to mimic what Spring HATEOAS WebfluxCodecCustomizer would do if withGenericJsonTypes where true.
@wilkinsona, would your approach be an option for HypermediaAutoConfiguration for the case spring.hateoas.use-hal-as-default-json-media-type=false?
Comment From: wilkinsona
It's certainly an option, but I'd prefer not to have to take it. This feels like something that should just work by default but I think we'll need some changes in Spring HATEOAS and/or Spring Framework to get there. Let's see what @odrotbohm and @rstoyanchev make of it and then we can take things from there.
Comment From: odrotbohm
There are a couple of things playing into this here:
- There's been a change in the default setup of Spring HATEOAS 1.3 under the new Web(MVC|Flux) configuration application. The very first media type declared in
@EnableHypermediaSupport(…)is going to also serveapplication/jsonandapplication/*+json. The reason the new configuration approach was implemented is that previously, a JSON media type not enabled on the server could have been requested, and Web(MVC|Flux) would have returned theRepresentationModelserialized without media type specific customizations, effectively producing responses that do not match the requested media type. - The representation resulting from serializing
RepresentationModelinstances with anObjectMapperthat is not equipped with media type specific customizations was never supported. Unfortunately, until the latest Spring Framework and Spring HATEOAS releases, there was no way to prevent its usage. If you useRepresentationModel, it is highly recommended to piggy back on an existing hypermedia type, as otherwise all bets are off in multiple ways: clients will not know how to process the document and the hypermedia affordances in particular (application/jsondoesn't tell you how to find links). Also, you're now tied to a completely unsupported serialization format that might change at will whenever we tweak the model, e.g. for API (as in Java API) reasons. Unfortunately Jackson doesn't even know about media types and Web(MVC|Flux) has just been tweaked to make sure, it doesn't actually renderRepresentationModelinstances without hypermedia customizations applied explicitly. I.e. by opting intoRepresentationModelyou express: I want to use a hypermedia media type and then actually do so.
There is a workaround to "solve" your problem: register application/json as hypermedia type with Spring HATEOAS:
@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {
@Override
public List<MediaType> getMediaTypes() {
return List.of(MediaType.APPLICATION_JSON);
}
}
That will register the uncustomized object mapper for use with RepresentationModel instances if application/json is requested, which makes your project's test work again.
Comment From: wilkinsona
Thank you, @odrotbohm. I think we should turn this into a documentation issue. We can add something to this section of the documentation about spring.hateoas.use-hal-as-default-json-media-type and the consequences of setting it to false for requests that accept application/json.
Given that setting spring.hateoas.use-hal-as-default-json-media-type to false will require some additional configuration from the user, either in the form of a HypermediaMappingInformation or a HalConfiguration, I think we should deprecate the property. This will means that users can just define their own HalConfiguration rather than having to define their own HalConfiguration and set the property.
Comment From: odrotbohm
If activating the flag registered the configuration above, it would effectively do the same thing as during the 2.4 times. Does it maybe make sense to change the implementation to just do that?
Comment From: wilkinsona
I'm a little wary of doing that as it'll make it easy to do something that isn't supported, i.e. it's encouraging people to serialise a RepresentationModel as plain JSON. From what you've said above, that seems like a bad idea. It's also at odds with the approach we agreed a while ago when considering how to evolve the HATEOAS auto-configuration. A lot has probably changed since then so we can certainly reconsider if a different approach now makes more sense.
Comment From: tmc9031
please any update documentation for spring.hateoas.use-hal-as-default-json-media-type NOT effect ???
try again new springboot 2.7 the application.properties also INVALID to disable hateoas for spring-data-rest
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes
Comment From: wilkinsona
@tmc9031 There hasn't been any changes in this area in Spring Boot 2.7. I see you've also commented on https://github.com/spring-projects/spring-data-rest/issues/2132. Please don't ask what is essentially the same question on multiple issues. If the discussion above isn't enough to help you, I would ask a question on Stack Overflow, taking the time to provide a complete and minimal example of your problem.
Comment From: tmc9031
@wilkinsona anything update or Stack Overflow link about Use JSON instead HAL with spring-data-rest ??? thanks a lot
Comment From: wilkinsona
@tmc9031 The comments above describe your options. You should set the property and then define your own HypermediaMappingInformation or HalConfiguration. There's an example of the former in this comment.
If you have any further questions, please follow up on Stack Overflow or Gitter. As mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.