Trigger user event when H2 conn preface & SETTINGS frame are sent
Motivation: Previously client Http2ConnectionHandler trigger a user event immediately when the HTTP/2 connection preface is sent. Any attempt to immediately send a new request could cause the server to terminate the connection, as it might not have received the SETTINGS frame from the client. Per RFC7540 Section 3.5, the preface "MUST be followed by a SETTINGS frame (Section 6.5), which MAY be empty." (https://tools.ietf.org/html/rfc7540#section-3.5) This event could be made more meaningful if it also indicates that the initial client SETTINGS frame has been sent to signal that the channel is ready to send new requests. Modification: - Renamed event to Http2ConnectionPrefaceAndSettingsFrameWrittenEvent. - Modified Http2ConnectionHandler to trigger the user event only if it is a client and it has sent both the preface and SETTINGS frame. Result: It is now safe to use the event as an indicator that the HTTP/2 connection is ready to send new requests.
This commit is contained in:
parent
55b501d0d4
commit
baf273aea8
@ -364,15 +364,20 @@ public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http
|
|||||||
|
|
||||||
prefaceSent = true;
|
prefaceSent = true;
|
||||||
|
|
||||||
if (!connection().isServer()) {
|
final boolean isClient = !connection().isServer();
|
||||||
|
if (isClient) {
|
||||||
// Clients must send the preface string as the first bytes on the connection.
|
// Clients must send the preface string as the first bytes on the connection.
|
||||||
ctx.write(connectionPrefaceBuf()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
|
ctx.write(connectionPrefaceBuf()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
|
||||||
ctx.fireUserEventTriggered(Http2ConnectionPrefaceWrittenEvent.INSTANCE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both client and server must send their initial settings.
|
// Both client and server must send their initial settings.
|
||||||
encoder.writeSettings(ctx, initialSettings, ctx.newPromise()).addListener(
|
encoder.writeSettings(ctx, initialSettings, ctx.newPromise()).addListener(
|
||||||
ChannelFutureListener.CLOSE_ON_FAILURE);
|
ChannelFutureListener.CLOSE_ON_FAILURE);
|
||||||
|
|
||||||
|
if (isClient) {
|
||||||
|
ctx.fireUserEventTriggered(
|
||||||
|
Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,14 +17,15 @@ package io.netty.handler.codec.http2;
|
|||||||
import io.netty.util.internal.UnstableApi;
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signifies that the <a href="https://tools.ietf.org/html/rfc7540#section-3.5">connection preface</a> has been sent.
|
* Signifies that the <a href="https://tools.ietf.org/html/rfc7540#section-3.5">connection preface</a> and
|
||||||
* The client sends the preface, and the server receives the preface. The client shouldn't write any data until this
|
* the initial SETTINGS frame have been sent. The client sends the preface, and the server receives the preface.
|
||||||
* event has been processed.
|
* The client shouldn't write any data until this event has been processed.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class Http2ConnectionPrefaceWrittenEvent {
|
public final class Http2ConnectionPrefaceAndSettingsFrameWrittenEvent {
|
||||||
static final Http2ConnectionPrefaceWrittenEvent INSTANCE = new Http2ConnectionPrefaceWrittenEvent();
|
static final Http2ConnectionPrefaceAndSettingsFrameWrittenEvent INSTANCE =
|
||||||
|
new Http2ConnectionPrefaceAndSettingsFrameWrittenEvent();
|
||||||
|
|
||||||
private Http2ConnectionPrefaceWrittenEvent() {
|
private Http2ConnectionPrefaceAndSettingsFrameWrittenEvent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -231,7 +231,7 @@ public class Http2FrameCodec extends Http2ConnectionHandler {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
|
if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
|
||||||
// The user event implies that we are on the client.
|
// The user event implies that we are on the client.
|
||||||
tryExpandConnectionFlowControlWindow(connection());
|
tryExpandConnectionFlowControlWindow(connection());
|
||||||
} else if (evt instanceof UpgradeEvent) {
|
} else if (evt instanceof UpgradeEvent) {
|
||||||
|
@ -347,7 +347,7 @@ public class DataCompressionHttp2Test {
|
|||||||
p.addLast(clientHandler);
|
p.addLast(clientHandler);
|
||||||
p.addLast(new ChannelInboundHandlerAdapter() {
|
p.addLast(new ChannelInboundHandlerAdapter() {
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
|
if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
|
||||||
prefaceWrittenLatch.countDown();
|
prefaceWrittenLatch.countDown();
|
||||||
ctx.pipeline().remove(this);
|
ctx.pipeline().remove(this);
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ import org.mockito.stubbing.Answer;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import static io.netty.buffer.Unpooled.copiedBuffer;
|
import static io.netty.buffer.Unpooled.copiedBuffer;
|
||||||
import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf;
|
import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf;
|
||||||
@ -240,6 +241,34 @@ public class Http2ConnectionHandlerTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void clientShouldveSentPrefaceAndSettingsFrameWhenUserEventIsTriggered() throws Exception {
|
||||||
|
when(connection.isServer()).thenReturn(false);
|
||||||
|
when(channel.isActive()).thenReturn(false);
|
||||||
|
handler = newHandler();
|
||||||
|
when(channel.isActive()).thenReturn(true);
|
||||||
|
|
||||||
|
final Http2ConnectionPrefaceAndSettingsFrameWrittenEvent evt =
|
||||||
|
Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE;
|
||||||
|
|
||||||
|
final AtomicBoolean verified = new AtomicBoolean(false);
|
||||||
|
final Answer verifier = new Answer() {
|
||||||
|
@Override
|
||||||
|
public Object answer(final InvocationOnMock in) throws Throwable {
|
||||||
|
assertTrue(in.getArgument(0).equals(evt)); // sanity check...
|
||||||
|
verify(ctx).write(eq(connectionPrefaceBuf()));
|
||||||
|
verify(encoder).writeSettings(eq(ctx), any(Http2Settings.class), any(ChannelPromise.class));
|
||||||
|
verified.set(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
doAnswer(verifier).when(ctx).fireUserEventTriggered(evt);
|
||||||
|
|
||||||
|
handler.channelActive(ctx);
|
||||||
|
assertTrue(verified.get());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void clientShouldSendClientPrefaceStringWhenActive() throws Exception {
|
public void clientShouldSendClientPrefaceStringWhenActive() throws Exception {
|
||||||
when(connection.isServer()).thenReturn(false);
|
when(connection.isServer()).thenReturn(false);
|
||||||
|
@ -933,7 +933,7 @@ public class Http2ConnectionRoundtripTest {
|
|||||||
p.addLast(new ChannelInboundHandlerAdapter() {
|
p.addLast(new ChannelInboundHandlerAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
|
if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
|
||||||
prefaceWrittenLatch.countDown();
|
prefaceWrittenLatch.countDown();
|
||||||
ctx.pipeline().remove(this);
|
ctx.pipeline().remove(this);
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,6 @@ import static io.netty.handler.codec.http.HttpMethod.POST;
|
|||||||
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
|
||||||
import static io.netty.handler.codec.http2.Http2TestUtil.of;
|
import static io.netty.handler.codec.http2.Http2TestUtil.of;
|
||||||
import static io.netty.util.CharsetUtil.UTF_8;
|
import static io.netty.util.CharsetUtil.UTF_8;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
@ -541,7 +540,7 @@ public class HttpToHttp2ConnectionHandlerTest {
|
|||||||
p.addLast(new ChannelInboundHandlerAdapter() {
|
p.addLast(new ChannelInboundHandlerAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
|
if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
|
||||||
prefaceWrittenLatch.countDown();
|
prefaceWrittenLatch.countDown();
|
||||||
ctx.pipeline().remove(this);
|
ctx.pipeline().remove(this);
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,6 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Except
|
|||||||
import static io.netty.handler.codec.http2.Http2Exception.isStreamError;
|
import static io.netty.handler.codec.http2.Http2Exception.isStreamError;
|
||||||
import static io.netty.handler.codec.http2.Http2TestUtil.of;
|
import static io.netty.handler.codec.http2.Http2TestUtil.of;
|
||||||
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
|
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@ -725,7 +724,7 @@ public class InboundHttp2ToHttpAdapterTest {
|
|||||||
});
|
});
|
||||||
p.addLast(new ChannelInboundHandlerAdapter() {
|
p.addLast(new ChannelInboundHandlerAdapter() {
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent) {
|
if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
|
||||||
prefaceWrittenLatch.countDown();
|
prefaceWrittenLatch.countDown();
|
||||||
ctx.pipeline().remove(this);
|
ctx.pipeline().remove(this);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user