spring-boot icon indicating copy to clipboard operation
spring-boot copied to clipboard

Document additional user configuration that's required after setting `spring.hateoas.use-hal-as-default-json-media-type` to `false`

Open sephiroth-j opened this issue 4 years ago • 18 comments

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]

sephiroth-j avatar Jun 08 '21 10:06 sephiroth-j

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.

wilkinsona avatar Jun 08 '21 11:06 wilkinsona

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.

bclozel avatar Jun 08 '21 12:06 bclozel

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.

sephiroth-j avatar Jun 08 '21 12:06 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.

sephiroth-j avatar Jun 08 '21 13:06 sephiroth-j

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.

wilkinsona avatar Jun 08 '21 17:06 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.

wilkinsona avatar Jun 08 '21 17:06 wilkinsona

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.

sephiroth-j avatar Jun 09 '21 09:06 sephiroth-j

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}"}]}

wilkinsona avatar Jun 09 '21 11:06 wilkinsona

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?

sephiroth-j avatar Jun 09 '21 12:06 sephiroth-j

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.

wilkinsona avatar Jun 10 '21 10:06 wilkinsona

There are a couple of things playing into this here:

  1. 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 serve application/json and application/*+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 the RepresentationModel serialized without media type specific customizations, effectively producing responses that do not match the requested media type.
  2. The representation resulting from serializing RepresentationModel instances with an ObjectMapper that 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 use RepresentationModel, 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/json doesn'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 render RepresentationModel instances without hypermedia customizations applied explicitly. I.e. by opting into RepresentationModel you 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.

odrotbohm avatar Jun 10 '21 11:06 odrotbohm

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.

wilkinsona avatar Jun 14 '21 13:06 wilkinsona

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?

odrotbohm avatar Jun 14 '21 14:06 odrotbohm

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.

wilkinsona avatar Jun 14 '21 19:06 wilkinsona

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

tmc9031 avatar May 29 '22 07:05 tmc9031

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

wilkinsona avatar May 30 '22 07:05 wilkinsona

@wilkinsona anything update or Stack Overflow link about Use JSON instead HAL with spring-data-rest ??? thanks a lot

tmc9031 avatar Aug 06 '22 14:08 tmc9031

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

wilkinsona avatar Aug 08 '22 19:08 wilkinsona