Spring boot version: 2.3.1.RELEASE
Description: BlockHound detects blocking code when Spring Boot Data (Hibernate) is used together with Validation API for WebFlux project.
To reproduce run the application that contains these 4 files: - build.gradle - DemoApplication.java - Body.java - MyController.java
And run the curl command:
curl --location --request POST 'localhost:8080/body' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": ""
}'
Actual result: Blocking code is detected (full log check below in the ticket)
reactor.blockhound.BlockingOperationError: Blocking call! java.io.RandomAccessFile#readBytes
at java.base/java.io.RandomAccessFile.readBytes(RandomAccessFile.java) ~[na:na]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
build.gradle
plugins {
id 'org.springframework.boot' version '2.3.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.projectreactor.tools:blockhound:1.0.4.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
DemoApplication.java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import reactor.blockhound.BlockHound;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
BlockHound.install();
SpringApplication.run(DemoApplication.class, args);
}
}
Body.java
package com.example.demo;
import javax.validation.constraints.NotBlank;
public class Body {
@NotBlank(message = "should not be blank")
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
MyController.java
package com.example.demo;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
@RestController
public class MyController {
@PostMapping(path = "/body")
public Mono<ResponseEntity<Body>> post(@Valid @RequestBody Body body) {
return Mono.just(ResponseEntity.status(HttpStatus.OK).body(body));
}
}
Log:
2020-07-04 15:40:19.871 DEBUG 19986 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x9a0e29ca, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:65367] Increasing pending responses, now 1
2020-07-04 15:40:19.871 DEBUG 19986 --- [ctor-http-nio-3] reactor.netty.http.server.HttpServer : [id: 0x9a0e29ca, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:65367] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter@522dd5db
2020-07-04 15:40:19.871 DEBUG 19986 --- [ctor-http-nio-3] o.s.w.s.adapter.HttpWebHandlerAdapter : [9a0e29ca-2] HTTP POST "/body"
2020-07-04 15:40:19.872 DEBUG 19986 --- [ctor-http-nio-3] s.w.r.r.m.a.RequestMappingHandlerMapping : [9a0e29ca-2] Mapped to com.example.demo.MyController#post(Body)
2020-07-04 15:40:19.872 DEBUG 19986 --- [ctor-http-nio-3] .r.m.a.RequestBodyMethodArgumentResolver : [9a0e29ca-2] Content-Type:application/json
2020-07-04 15:40:19.872 DEBUG 19986 --- [ctor-http-nio-3] .r.m.a.RequestBodyMethodArgumentResolver : [9a0e29ca-2] 0..1 [com.example.demo.Body]
2020-07-04 15:40:19.872 DEBUG 19986 --- [ctor-http-nio-3] reactor.netty.channel.FluxReceive : [id: 0x9a0e29ca, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:65367] Subscribing inbound receiver [pending: 0, cancelled:false, inboundDone: false]
2020-07-04 15:40:19.872 DEBUG 19986 --- [ctor-http-nio-3] o.s.http.codec.json.Jackson2JsonDecoder : [9a0e29ca-2] Decoded [com.example.demo.Body@63d1d635]
2020-07-04 15:40:19.873 DEBUG 19986 --- [ctor-http-nio-3] o.s.w.r.r.method.InvocableHandlerMethod : [9a0e29ca-2] Could not resolve parameter [0] in public reactor.core.publisher.Mono<org.springframework.http.ResponseEntity<com.example.demo.Body>> com.example.demo.MyController.post(com.example.demo.Body): Blocking call! java.io.RandomAccessFile#readBytes
2020-07-04 15:40:19.874 DEBUG 19986 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [9a0e29ca-2] Resolved [BlockingOperationError: Blocking call! java.io.RandomAccessFile#readBytes] for HTTP POST /body
2020-07-04 15:40:19.875 ERROR 19986 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [9a0e29ca-2] 500 Server Error for HTTP POST "/body"
reactor.blockhound.BlockingOperationError: Blocking call! java.io.RandomAccessFile#readBytes
at java.base/java.io.RandomAccessFile.readBytes(RandomAccessFile.java) ~[na:na]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ HTTP POST "/body" [ExceptionHandlingWebHandler]
Stack trace:
at java.base/java.io.RandomAccessFile.readBytes(RandomAccessFile.java) ~[na:na]
at java.base/java.io.RandomAccessFile.read(RandomAccessFile.java:406) ~[na:na]
at java.base/java.io.RandomAccessFile.readFully(RandomAccessFile.java:470) ~[na:na]
at java.base/java.util.zip.ZipFile$Source.readFullyAt(ZipFile.java:1298) ~[na:na]
at java.base/java.util.zip.ZipFile$ZipFileInputStream.initDataOffset(ZipFile.java:997) ~[na:na]
at java.base/java.util.zip.ZipFile$ZipFileInputStream.read(ZipFile.java:1012) ~[na:na]
at java.base/java.util.zip.ZipFile$ZipFileInflaterInputStream.fill(ZipFile.java:467) ~[na:na]
at java.base/java.util.zip.InflaterInputStream.read(InflaterInputStream.java:159) ~[na:na]
at java.base/java.io.InputStream.readNBytes(InputStream.java:396) ~[na:na]
at java.base/java.io.InputStream.readAllBytes(InputStream.java:333) ~[na:na]
at java.base/java.util.jar.JarFile.getBytes(JarFile.java:807) ~[na:na]
at java.base/java.util.jar.JarFile.checkForSpecialAttributes(JarFile.java:1005) ~[na:na]
at java.base/java.util.jar.JarFile.isMultiRelease(JarFile.java:388) ~[na:na]
at java.base/java.util.jar.JarFile.getEntry(JarFile.java:507) ~[na:na]
at java.base/sun.net.www.protocol.jar.URLJarFile.getEntry(URLJarFile.java:131) ~[na:na]
at java.base/sun.net.www.protocol.jar.JarURLConnection.connect(JarURLConnection.java:137) ~[na:na]
at java.base/sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java:155) ~[na:na]
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.parse(ServiceLoader.java:1164) ~[na:na]
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1205) ~[na:na]
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(ServiceLoader.java:1220) ~[na:na]
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(ServiceLoader.java:1264) ~[na:na]
at java.base/java.util.ServiceLoader$2.hasNext(ServiceLoader.java:1299) ~[na:na]
at java.base/java.util.ServiceLoader$3.hasNext(ServiceLoader.java:1384) ~[na:na]
at javax.persistence.spi.PersistenceProviderResolverHolder$DefaultPersistenceProviderResolver.getPersistenceProviders(PersistenceProviderResolverHolder.java:110) ~[jakarta.persistence-api-2.2.3.jar:2.2.3]
at javax.persistence.Persistence$PersistenceUtilImpl.isLoaded(Persistence.java:142) ~[jakarta.persistence-api-2.2.3.jar:2.2.3]
at org.hibernate.validator.internal.engine.resolver.JPATraversableResolver.isReachable(JPATraversableResolver.java:52) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.resolver.CachingJPATraversableResolverForSingleValidation.lambda$isReachable$0(CachingJPATraversableResolverForSingleValidation.java:43) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1133) ~[na:na]
at org.hibernate.validator.internal.engine.resolver.CachingJPATraversableResolverForSingleValidation.isReachable(CachingJPATraversableResolverForSingleValidation.java:43) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.isReachable(ValidatorImpl.java:1307) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.isValidationRequired(ValidatorImpl.java:1291) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateMetaConstraint(ValidatorImpl.java:549) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForSingleDefaultGroupElement(ValidatorImpl.java:518) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:488) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:450) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:400) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:172) ~[hibernate-validator-6.1.5.Final.jar:6.1.5.Final]
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:117) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.boot.autoconfigure.validation.ValidatorAdapter.validate(ValidatorAdapter.java:70) ~[spring-boot-autoconfigure-2.3.1.RELEASE.jar:2.3.1.RELEASE]
at org.springframework.validation.DataBinder.validate(DataBinder.java:889) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.validate(AbstractMessageReaderArgumentResolver.java:256) ~[spring-webflux-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.lambda$readBody$6(AbstractMessageReaderArgumentResolver.java:198) ~[spring-webflux-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:177) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:144) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onNext(FluxContextStart.java:96) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:287) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:330) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:152) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:252) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136) ~[reactor-core-3.3.6.RELEASE.jar:3.3.6.RELEASE]
at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:365) ~[reactor-netty-0.9.8.RELEASE.jar:0.9.8.RELEASE]
at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:368) ~[reactor-netty-0.9.8.RELEASE.jar:0.9.8.RELEASE]
at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:495) ~[reactor-netty-0.9.8.RELEASE.jar:0.9.8.RELEASE]
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:96) ~[reactor-netty-0.9.8.RELEASE.jar:0.9.8.RELEASE]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:214) ~[reactor-netty-0.9.8.RELEASE.jar:0.9.8.RELEASE]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324) ~[netty-codec-4.1.50.Final.jar:4.1.50.Final]
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) ~[netty-codec-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) ~[netty-transport-4.1.50.Final.jar:4.1.50.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) ~[netty-common-4.1.50.Final.jar:4.1.50.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.50.Final.jar:4.1.50.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.50.Final.jar:4.1.50.Final]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
2020-07-04 15:40:19.876 DEBUG 19986 --- [ctor-http-nio-3] o.s.http.codec.json.Jackson2JsonEncoder : [9a0e29ca-2] Encoding [{timestamp=Sat Jul 04 15:40:19 CEST 2020, path=/body, status=500, error=Internal Server Error, messa (truncated)...]
2020-07-04 15:40:19.878 DEBUG 19986 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x9a0e29ca, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:65367] Decreasing pending responses, now 0
2020-07-04 15:40:19.879 DEBUG 19986 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x9a0e29ca, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:65367] Last HTTP packet was sent, terminating the channel
2020-07-04 15:40:19.879 DEBUG 19986 --- [ctor-http-nio-3] o.s.w.s.adapter.HttpWebHandlerAdapter : [9a0e29ca-2] Completed 500 INTERNAL_SERVER_ERROR
2020-07-04 15:40:19.879 DEBUG 19986 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x9a0e29ca, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:65367] Last HTTP response frame
2020-07-04 15:40:20.846 DEBUG 19986 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Pool stats (total=10, active=0, idle=10, waiting=0)
2020-07-04 15:40:20.847 DEBUG 19986 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Fill pool skipped, pool is at sufficient level.
Comment From: wilkinsona
Thanks for the report but this is out of Spring Boot’s control.
Hibernate Validator is triggering the use of the JVM’s ServiceLoader
. It is performing some file IO which may be blocking. Bean Validation isn’t a reactive API so there’s no guarantee that it can be safely used in a reactive application. You may want to look at using something like Yavi instead.
I also noticed that you have Spring Data JPA and H2 in your dependencies. Please note that neither of these can be safely used in a reactive application either unless you jump through some hoops to move the work onto a separate thread. You may want to look at R2DBC instead.
/cc @bsideup who maintains BlockHound