Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
sephiroth-j opened this issue Jun 8, 2021 · 18 comments
Assignees
Labels
type: documentation A documentation update
Milestone

Comments

@sephiroth-j
Copy link

sephiroth-j commented Jun 8, 2021

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 5863edf 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]
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 8, 2021
@wilkinsona
Copy link
Member

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 wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Jun 8, 2021
@bclozel
Copy link
Member

bclozel commented Jun 8, 2021

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.

@sephiroth-j
Copy link
Author

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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 8, 2021
@bclozel bclozel added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Jun 8, 2021
@sephiroth-j
Copy link
Author

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.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Jun 8, 2021
@wilkinsona
Copy link
Member

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
Copy link
Member

wilkinsona commented Jun 8, 2021

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.

@sephiroth-j
Copy link
Author

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.

@wilkinsona
Copy link
Member

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

@sephiroth-j
Copy link
Author

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?

@wilkinsona
Copy link
Member

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.

@odrotbohm
Copy link
Member

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.

@wilkinsona
Copy link
Member

wilkinsona commented Jun 14, 2021

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 wilkinsona changed the title Regression in version 2.5.0 with HATEOAS and Webflux Document additional user configuration that's required after setting spring.hateoas.use-hal-as-default-json-media-type to false Jun 14, 2021
@wilkinsona wilkinsona added type: documentation A documentation update and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged labels Jun 14, 2021
@wilkinsona wilkinsona added this to the 2.5.x milestone Jun 14, 2021
@odrotbohm
Copy link
Member

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?

@wilkinsona
Copy link
Member

wilkinsona commented Jun 14, 2021

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.

@tmc9031
Copy link

tmc9031 commented May 29, 2022

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

@wilkinsona
Copy link
Member

wilkinsona commented May 30, 2022

@tmc9031 There hasn't been any changes in this area in Spring Boot 2.7. I see you've also commented on spring-projects/spring-data-rest#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.

@tmc9031
Copy link

tmc9031 commented Aug 6, 2022

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

@wilkinsona
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests

6 participants