Open chevaris opened 1 year ago
cc @mkouba @cescoffier
we will need to debug, but I suspect that the body has been already read by Resteasy reactive. What I don't get is why it's different with HTTP 1.1. I don't see pipelining in the descriptor.
It seems that the io.vertx.core.http.impl.Http1xServerRequest.pause()
method does not call checkEnded()
but io.vertx.core.http.impl.Http2ServerRequest.pause()
does...
good catch @mkouba ! (maybe it should not work with HTTP 1.1 after all :-D)
@vietj any idea?
I tested with a GET endpoint and if I remember well it worked fine
I tested with a GET endpoint and if I remember well it worked fine
Hm yes, because for GET there's no request body that must be read... it could be that for HTTP2 it's not possible to read the body and then call Http2ServerRequest.pause()
.
I tested with a @RouteFilter and works fine for both HTTP 1.1 and HTTP2. This is fine for my use case, because basically I am after intercepting the query and rejecting it under certain circunstances (overload)
Still it is hard for me to assess if the what I explained in the bug (using a @Route) should work or NOT. I was reading Quarkus documentation again, and my understanding is that Routes can mixed with REST Easy. Anyhow looks not consistent that HTTP 1.1 and HTTP2 show different behaviour (leading to confusion). I think you can decide better than me if the bug should be kept open and requires some fixing or not
I tested with a @RouteFilter and works fine for both HTTP 1.1 and HTTP2. This is fine for my use case, because basically I am after intercepting the query and rejecting it under certain circunstances (overload)
Yes, @RouteFilter
s do not have a body handler attached.
Still it is hard for me to assess if the what I explained in the bug (using a @route) should work or NOT. I was reading Quarkus documentation again, and my understanding is that Routes can mixed with REST Easy. Anyhow looks not consistent that HTTP 1.1 and HTTP2 show different behaviour (leading to confusion). I think you can decide better than me if the bug should be kept open and requires some fixing or not
We will definitely need a Vert.x expert here...
I think the main difference comes from the fact that with HTTP/1 we have two HTTP messages and with HTTP/2 we only get one HEADERS frame with the flag ended=true, the request might be paused too late in the case of HTTP/2.
I suggest adding a breakpoint in the pause method of HttpServerRequest and in the end handler callback and check the sequentiality of the events to be sure.
Ok, so the sequence seems to be:
Thread [vert.x-eventloop-thread-0] (Suspended (breakpoint at line 268 in Http2ServerRequest))
owns: Http2ServerConnection (id=265)
Http2ServerRequest.pause() line: 268
VertxHttpRecorder$1.handle(HttpServerRequest) line: 177
VertxHttpRecorder$1.handle(Object) line: 156
EventLoopContext.emit(ContextInternal, T, Handler<T>) line: 55
DuplicatedContext.emit(T, Handler<T>) line: 158
Http2ServerRequest.dispatch(Handler<HttpServerRequest>) line: 108
Http2ServerStream.onHeaders(Http2Headers, StreamPriority) line: 111
Http2ServerConnection.onHeadersRead(int, Http2Headers, StreamPriority, boolean) line: 171
Http2ServerConnection(Http2ConnectionBase).onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean) line: 213
Http2ServerConnection.onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean) line: 44
Thread [vert.x-eventloop-thread-0] (Suspended (breakpoint at line 75 in HttpEventHandler))
HttpEventHandler.handleEnd() line: 75
Http2ServerRequest.handleEnd(MultiMap) line: 198
Http2ServerStream.handleEnd(MultiMap) line: 198
Http2ServerStream(VertxHttp2Stream<C>).lambda$new$1(Http2ConnectionBase, Object) line: 62
0x00000008017375a0.handle(Object) line: not available
InboundBuffer<E>.handleEvent(Handler<T>, T) line: 255
InboundBuffer<E>.drain() line: 242
InboundBuffer<E>.lambda$fetch$0(Void) line: 295
0x000000080174fa38.handle(Object) line: not available
DuplicatedContext(ContextInternal).dispatch(E, Handler<E>) line: 264
DuplicatedContext(ContextInternal).dispatch(Handler<Void>) line: 246
Thread [vert.x-eventloop-thread-0] (Suspended (breakpoint at line 268 in Http2ServerRequest))
Http2ServerRequest.pause() line: 268
ResumingRequestWrapper.pause() line: 30
QuarkusRequestWrapper(HttpServerRequestWrapper).pause() line: 67
HttpServerRequestWrapper(HttpServerRequestWrapper).pause() line: 67
QuarkusResteasyReactiveRequestContext(VertxResteasyReactiveRequestContext).<init>(Deployment, RoutingContext, ThreadSetupAction, ServerRestHandler[], ServerRestHandler[], ClassLoader) line: 83
QuarkusResteasyReactiveRequestContext.<init>(Deployment, RoutingContext, ThreadSetupAction, ServerRestHandler[], ServerRestHandler[], ClassLoader, CurrentIdentityAssociation) line: 31
ResteasyReactiveRecorder$5.createContext(Deployment, Object, ThreadSetupAction, ServerRestHandler[], ServerRestHandler[]) line: 174
RestInitialHandler.beginProcessing(Object) line: 45
ResteasyReactiveVertxHandler.handle(RoutingContext) line: 23
ResteasyReactiveVertxHandler.handle(Object) line: 10
RouteState.handleContext(RoutingContextImplBase) line: 1284
RoutingContextImpl(RoutingContextImplBase).iterateNext() line: 177
RoutingContextImpl.next() line: 141
Something called fetch in step 2 : InboundBuffer<E>.lambda$fetch$0(Void)
which triggers the event to be sent.
Something called fetch in step 2 :
InboundBuffer<E>.lambda$fetch$0(Void)
which triggers the event to be sent.
Yes, it was called here: https://github.com/quarkusio/quarkus/blob/3.1.2.Final/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java#L1408
Thread [vert.x-eventloop-thread-0] (Suspended (breakpoint at line 295 in InboundBuffer))
owns: Http2ServerConnection (id=160)
InboundBuffer<E>.fetch(long) line: 295
Http2ServerStream(VertxHttp2Stream<C>).doFetch(long) line: 160
Http2ServerRequest.fetch(long) line: 284
Http2ServerRequest.resume() line: 277
ResumingRequestWrapper.resume() line: 37
QuarkusRequestWrapper(HttpServerRequestWrapper).resume() line: 72
HttpServerRequestWrapper(HttpServerRequestWrapper).resume() line: 72
VertxHttpRecorder$16.handle(RoutingContext) line: 1408 <<<<<<<<<<<<<<<<<<<<<<<<<<
VertxHttpRecorder$16.handle(Object) line: 1382
RouteState.handleContext(RoutingContextImplBase) line: 1284
RoutingContextImpl(RoutingContextImplBase).iterateNext() line: 177
RoutingContextImpl.next() line: 141
HttpServerCommonHandlers$1.handle(RoutingContext) line: 58
HttpServerCommonHandlers$1.handle(Object) line: 36
RouteState.handleContext(RoutingContextImplBase) line: 1284
RoutingContextImpl(RoutingContextImplBase).iterateNext() line: 177
RoutingContextImpl.next() line: 141
ResteasyReactiveRecorder$13.handle(RoutingContext) line: 380
ResteasyReactiveRecorder$13.handle(Object) line: 373
RouteState.handleContext(RoutingContextImplBase) line: 1284
Hum, we have this code which may be problematic with HTTP/2:
return new Handler<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
if (!Context.isOnEventLoopThread()) {
((ConnectionBase) event.request().connection()).channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
//this can happen if blocking authentication is involved for get requests
if (!event.request().isEnded()) {
event.request().resume();
if (CAN_HAVE_BODY.contains(event.request().method())) {
bodyHandler.handle(event);
} else {
event.next();
}
} else {
event.next();
}
} catch (Throwable t) {
event.fail(t);
}
}
});
} else {
if (!event.request().isEnded()) {
event.request().resume();
}
if (CAN_HAVE_BODY.contains(event.request().method())) {
bodyHandler.handle(event);
} else {
event.next();
}
}
}
};
CC @stuartwdouglas
So this is kinda yuck, but the code is intended to deal with blocking security providers by dispatching them back to the IO thread. Normally HTTP requests are resumed, so this code path also resumes so that downstream code should all work the same way.
Hm, the fetch is triggered by event.request().resume()
which is executed in the else
branch, i.e. when Context.isOnEventLoopThread() == true
. In any case, I have no idea how this works so I can't really help here :shrug:.
The question is : why does the code tries to pause the request while it has been read fully already ?
The question is : why does the code tries to pause the request while it has been read fully already ?
@geoand maybe knows?
I don't remember, I would need to check
I need to check what it breaks (because I'm sure it will), but there is an unconditional call to pause when RestEasy reactive creates its context. This method is called from the event loop, thus, we can safely check if the request has been read (isEnded()) before calling pause()
@cescoffier what you are proposing sounds safe. We always pause before dispatching to a worker thread so that you don't receive IO events on the IO thread at the same time and end up with two threads working on the request. The assumption would have been that calling pause() after everything was read was a noop, which sounds like an incorrect assumption.
Unfortunately, that's not enough. Once I do that (which I believe is correct), resume
is called, while the request is not paused. I would need more time to dig into that (so, won't be this week).
Describe the bug
I have implemented a Reactive REST endpoint that exposes POST HTTP method. The implementation includes a Vertx Route that is applied to POST method calls and the endpoint path.
When sending an HTTP 1.1 request it works fine
the result is
When sending with HTTP 2 (clear text) it does throw an exception
the following exception is thrown:
There are no issues when route is not included
Expected behavior
Both HTTP 1.1 and HTTP2 should work the same and provide the right output (200 OK and the response JSON)
Actual behavior
HTTP 2 does not work (HTTP status code 500)
How to Reproduce?
code-with-quarkus.zip
unzip reproducer
start in dev mode mvn quearkus:dev
Query with HTTP2 curl --http2 -d '{"name": "juan"}' -H "Content-Type: application/json" -X POST http://localhost:8080/hello
Query with HTTP1/1 curl -d '{"name": "juan"}' -H "Content-Type: application/json" -X POST http://localhost:8080/hello
Output of
uname -a
orver
Linux cheva-virtualmachine 5.15.112-1-MANJARO #1 SMP PREEMPT Wed May 17 11:11:32 UTC 2023 x86_64 GNU/Linux
Output of
java -version
openjdk version "17.0.7" 2023-04-18 OpenJDK Runtime Environment (build 17.0.7+7) OpenJDK 64-Bit Server VM (build 17.0.7+7, mixed mode)
GraalVM version (if different from Java)
No response
Quarkus version or git rev
3.1.2.Final
Build tool (ie. output of
mvnw --version
orgradlew --version
)Apache Maven 3.8.7 (b89d5959fcde851dcb1c8946a785a163f14e1e29) Maven home: /opt/maven Java version: 17.0.7, vendor: N/A, runtime: /usr/lib/jvm/java-17-openjdk Default locale: en_US, platform encoding: UTF-8 OS name: "linux", version: "5.15.112-1-manjaro", arch: "amd64", family: "unix"
Additional information
No response