0

Background:

We are implementing a signed request mechanism for communication between services. Part of that process generates a digest on the contents of the request body. To validate the body on receipt, we re-generate the digest at the receiver and compare. It's pretty straight-forward stuff.

@PreMatching
@Priority(Priorities.ENTITY_CODER)
public class DigestValidationFilter implements ContainerRequestFilter {

   private final DigestGenerator generator;

   @Inject
   public DigestValidationFilter(DigestGenerator generator) {
      this.generator = generator;
   }

   @Override
   public void filter(ContainerRequestContext context) throws IOException {
      if (context.hasEntity() && context.getHeaderString(Headers.DIGEST) != null) {
         String digest = context.getHeaderString(Headers.DIGEST);

         ByteArrayOutputStream body = new ByteArrayOutputStream();
         try (InputStream stream = context.getEntityStream()) {
             stream.transferTo(body); // <-- This is line 36 from the provided stack-trace
         }

         String algorithm = digest.split("=", 2)[0];
         try {
             String calculated = generator.generate(algorithm, body.toByteArray());

             if (digest.equals(calculated)) {
                 context.setEntityStream(new ByteArrayInputStream(body.toByteArray()));
             } else {
                 throw new InvalidDigestException("Calculated digest does not match supplied digest. Request body may have been tampered with.");
             }
         } catch (NoSuchAlgorithmException e) {
             throw new InvalidDigestException(String.format("Unsupported hash algorithm: %s", algorithm), e);
         }
      }
   }
}

The above filter is made available to services as a java-lib. We also supply a set of RequestFilters that can be used with various Http clients, i.e., okhttp3, apache-httpclient, etc. These clients only generate digests when the body is "repeatable", i.e., not streaming.

The Issue:

In Jersey services and Spring Boot services, we do not run into issues. However, when we use Quarkus, we receive the following stack-trace:

2022-09-02 15:18:25 5.13.0 ERROR A blocking operation occurred on the IO thread. This likely means you need to use the @io.smallrye.common.annotation.Blocking annotation on the Resource method, class or javax.ws.rs.core.Application class. 
2022-09-02 15:18:25 5.13.0 ERROR HTTP Request to /v1/policy/internal/policies/72575947-45ac-4358-bc40-b5c7ffbd3f35/target-resources failed, error id: c79aa557-c742-43d7-93d9-0e362b2dff79-1 
org.jboss.resteasy.reactive.common.core.BlockingNotAllowedException: Attempting a blocking read on io thread
    at org.jboss.resteasy.reactive.server.vertx.VertxInputStream$VertxBlockingInput.readBlocking(VertxInputStream.java:242)
    at org.jboss.resteasy.reactive.server.vertx.VertxInputStream.readIntoBuffer(VertxInputStream.java:120)
    at org.jboss.resteasy.reactive.server.vertx.VertxInputStream.read(VertxInputStream.java:82)
    at java.base/java.io.InputStream.transferTo(InputStream.java:782)
    at com.###.ciam.jaxrs.DigestValidationFilter.filter(DigestValidationFilter.java:36)
    at org.jboss.resteasy.reactive.server.handlers.ResourceRequestFilterHandler.handle(ResourceRequestFilterHandler.java:47)
    at org.jboss.resteasy.reactive.server.handlers.ResourceRequestFilterHandler.handle(ResourceRequestFilterHandler.java:8)
    at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:141)
    at org.jboss.resteasy.reactive.server.handlers.RestInitialHandler.beginProcessing(RestInitialHandler.java:49)
    at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveVertxHandler.handle(ResteasyReactiveVertxHandler.java:17)
    at org.jboss.resteasy.reactive.server.vertx.ResteasyReactiveVertxHandler.handle(ResteasyReactiveVertxHandler.java:7)
    at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1212)
    at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:163)
    at io.vertx.ext.web.impl.RoutingContextImpl.next(RoutingContextImpl.java:141)
    at io.quarkus.vertx.http.runtime.StaticResourcesRecorder$2.handle(StaticResourcesRecorder.java:67) ... elided ...

I completely understand why Vert.x would like to prevent long-running I/O operations on the request processing threads. That said, the advice provided in the exception only accounts for I/O operations at the end of the request processing, i.e., it assumes the I/O is happening in the endpoint. Although we do control the filter code, it is in an external library, making it almost like a 3rd party library.

My Question:

What is the right way to handle this?

I've been scouring documentation, but haven't stumbled on the answer yet (or haven't recognized the answer). Is there a set of recommended docs I should review?

3 Answers3

0

https://quarkus.io/guides/resteasy-reactive#request-or-response-filters

https://smallrye.io/smallrye-mutiny/1.7.0/guides/framework-integration/

    @RequestScoped
    class Filter(
        private val vertx: Vertx
    ) {
    
    //  you can run blocking code on mutiny's Infrastructure defaultWorkerPool
        @ServerRequestFilter
        fun filter(requestContext: ContainerRequestContext): Uni<RestResponse<*>> {
            return Uni.createFrom().item { work() }
                .map<RestResponse<*>> { null }
                .runSubscriptionOn(Infrastructure.getDefaultWorkerPool())
        }
    // or use vertx.executeBlocking api
        @ServerRequestFilter
        fun filter(requestContext: ContainerRequestContext): Uni<RestResponse<*>> {
            return vertx.executeBlocking(
                Uni.createFrom().item { work() }
                    .map { null }
            )
        }
    
    
    
        private fun work(){
            Log.info("filter")
            Thread.sleep(3000)
        }
    }
yazinnnn
  • 1
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 10 '22 at 14:13
0

In the end, the advice in the exception lead me to simply annotating a delegate ContainerRequestFilter:

public class DigestValidationFilterBlocking implements ContainerRequestFilter {

    private final DigestValidationFilter delegate;

    public DigestValidationFilterBlocking(DigestValidationFilter delegate) {
        this.delegate = delegate;
    }

    @Blocking // <-- This annotation allowed Vert.x to accept the I/O operation
    @Override
    public void filter(ContainerRequestContext context) throws IOException {
        delegate.filter(context);
    }
}
0

I had the same problem. You can try using this in your @ServerRequestFilter:

@Context
HttpServerRequest request;