Instrumentation of Reactor Netty

Instrumentation of Reactor Netty, used by Webflux, to get an object that is similar to Jakarta’s HttpServletRequest. I thought this might be a nice example to walk people through how I usually figure out my instrumentation. Be sure to checkout my ByteBuddy tips which I’ll be using here.

Exploration phase

So we want to intercept requests going to Spring Webflux before they reach a filter (AKA Middleware).

Spring Webflux primarily uses the Reactor Project which uses Netty as it’s server. Scouring through the Netty docs for the current stable version (4.1 at time of writing) we quickly find that most servers extend ChannelInboundHandlerAdapter. Due to it’s async nature and piping it looks fairly complex to instrument this, we’ll now take a look at Reactor.

Project Reactor has a couple of parts, but the one we care about is Reactor Netty. We can easily verify that this is used by webflux using a quick github search. Looking through the docs it becomes clear we would want to instrument HttpServer

Running phase

Since I have a sample app with Webflux ready I’ll now use that to look at all transformations and function calls for the HttpServer. I start this process by creating a Wrapper class in our project that looks like this :

public class NettyWrapper implements Wrapper {  
    public String getName() {  
        return MyGenericAdvice.class.getName();  
    }  
    public ElementMatcher<? super MethodDescription> getMatcher() {  
        return ElementMatchers.isDeclaredBy(getTypeMatcher());  
    }  
    @Override  
    public ElementMatcher<? super TypeDescription> getTypeMatcher() {  
        return ElementMatchers.nameContainsIgnoreCase("reactor.netty.http.server.HttpServer");  
    }  
      
    public class MyGenericAdvice {  
        @Advice.OnMethodEnter  
        public static void before(  
                @Advice.This(typing = DYNAMIC, optional = true) Object target,  
                @Advice.Origin Executable method,  
                @Advice.AllArguments(readOnly = false, typing = DYNAMIC) Object[] args  
        ) {  
            System.out.println("[C] >> " + method);  
        }  
  
        @Advice.OnMethodExit  
        public static void after(  
                @Advice.This(typing = DYNAMIC, optional = true) Object target,  
                @Advice.Origin Executable method,  
                @Advice.AllArguments(readOnly = false, typing = DYNAMIC) Object[] args,  
                @Advice.Return(readOnly = false, typing = DYNAMIC) Object returnValue  
        ) {  
            System.out.println("[C] << " + method);  
        }  
    }  
}

Fixing an error

Now when we look at our logs first we got an error :

[Byte Buddy] ERROR reactor.netty.http.server.HttpServer [org.springframework.boot.loader.launch.LaunchedClassLoader@2c58dcb1, unnamed module @4b3a01d8, Thread[#1,main,5,main], loaded=false]
java.lang.IllegalStateException: An illegal stack manipulation must not be applied

A first great debugging step is disabling the advice class :

public ElementMatcher<? super MethodDescription> getMatcher() {  
    return ElementMatchers.none();  
    //return ElementMatchers.isDeclaredBy(getTypeMatcher());  
}

Great that fixed our issue, now we know that something is going wrong for a certain function of the class we are wrapping, we can also see that by using nameEndsWith instead of a contains in our type matcher we only get one transformation :

[Byte Buddy] TRANSFORM reactor.netty.http.server.HttpServer [org.springframework.boot.loader.launch.LaunchedClassLoader@2c58dcb1, unnamed module @4b3a01d8, Thread[#1,main,5,main], loaded=false]

We’ll make the choice of wrapping the handle function, and let’s look if it’s called. And we get the call, very cool!

handle() function, how to get our request?

Okay we have successfully verified the handle() function is wrappable, now we want to figure out how to extract our HttpServerRequest Looking at docs :

public final HttpServer handle(BiFunction<? super HttpServerRequest,? super HttpServerResponse,? extends Publisher<Void» handler)

Since I’m not a Java developer I’ll first have to figure out what a BiFunction is. Looking into the docs we can see this is a function that takes the HttpServerRequest as an argument, in essence we would want to wrap this function and insert our own functionality. Since this is a bit complex we will first also discuss the opportunity of wrapping HttpServerRequest.

Wrapping HttpServerRequest, an interface.

Wrapping an interface is actually very possible, you can check if a given class is an implemenation of that interface using the following type matchers :

hasSuperType(nameContains("reactor.netty.http.server.HttpServerRequest").and(isInterface()));

Using this I came across an internal class called HttpServerOperations, which has a constructor method who gets the HttpRequest from Netty. however this interface is very primitive, it provides support for method, uri & headers. The best course of action is to parse data on exit of this function, and then read from HttpServerOperations object.

Ok, but how do we do extraction?

Luckily Byte Buddy has got us covered, it merges the different ClassLoaders so that we can just use the interface. We do need to add a compileOnly entry so the compiler knows what we are referencing :

compileOnly 'io.projectreactor.netty:reactor-netty-core:1.2.1' // For Spring Webflux

After we do that it’s smooth sailing, we can use @This in the exit advice to get the object after the constructor has finished it’s job. So that results in :

@Advice.OnMethodExit(suppress = Throwable.class)
public static void after(
	@Advice.This(typing = DYNAMIC, optional = true) HttpServerRequest target
) {
	// ...
}

Happy coding! 💜

Written on January 11, 2025