HTTP/2 enforce HTTP message flow
Motivation: codec-http2 currently does not strictly enforce the HTTP/1.x semantics with respect to the number of headers defined in RFC 7540 Section 8.1 [1]. We currently don't validate the number of headers nor do we validate that the trailing headers should indicate EOS. [1] https://tools.ietf.org/html/rfc7540#section-8.1 Modifications: - DefaultHttp2ConnectionDecoder should only allow decoding of a single headers and a single trailers - DefaultHttp2ConnectionEncoder should only allow encoding of a single headers and optionally a single trailers Result: Constraints of RFC 7540 restricting the number of headers/trailers is enforced.
This commit is contained in:
parent
4af47f0ced
commit
a91df58ca1
@ -74,6 +74,27 @@ public enum HttpStatusClass {
|
|||||||
return UNKNOWN;
|
return UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the class of the specified HTTP status code.
|
||||||
|
* @param code Just the numeric portion of the http status code.
|
||||||
|
*/
|
||||||
|
public static HttpStatusClass valueOf(CharSequence code) {
|
||||||
|
if (code != null && code.length() == 3) {
|
||||||
|
char c0 = code.charAt(0);
|
||||||
|
return isDigit(c0) && isDigit(code.charAt(1)) && isDigit(code.charAt(2)) ? valueOf(digit(c0) * 100)
|
||||||
|
: UNKNOWN;
|
||||||
|
}
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int digit(char c) {
|
||||||
|
return c - '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isDigit(char c) {
|
||||||
|
return c >= '0' && c <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
private final int min;
|
private final int min;
|
||||||
private final int max;
|
private final int max;
|
||||||
private final AsciiString defaultReasonPhrase;
|
private final AsciiString defaultReasonPhrase;
|
||||||
|
@ -373,13 +373,16 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
* Simple stream implementation. Streams can be compared to each other by priority.
|
* Simple stream implementation. Streams can be compared to each other by priority.
|
||||||
*/
|
*/
|
||||||
private class DefaultStream implements Http2Stream {
|
private class DefaultStream implements Http2Stream {
|
||||||
private static final byte SENT_STATE_RST = 0x1;
|
private static final byte META_STATE_SENT_RST = 1;
|
||||||
private static final byte SENT_STATE_HEADERS = 0x2;
|
private static final byte META_STATE_SENT_HEADERS = 1 << 1;
|
||||||
private static final byte SENT_STATE_PUSHPROMISE = 0x4;
|
private static final byte META_STATE_SENT_TRAILERS = 1 << 2;
|
||||||
|
private static final byte META_STATE_SENT_PUSHPROMISE = 1 << 3;
|
||||||
|
private static final byte META_STATE_RECV_HEADERS = 1 << 4;
|
||||||
|
private static final byte META_STATE_RECV_TRAILERS = 1 << 5;
|
||||||
private final int id;
|
private final int id;
|
||||||
private final PropertyMap properties = new PropertyMap();
|
private final PropertyMap properties = new PropertyMap();
|
||||||
private State state;
|
private State state;
|
||||||
private byte sentState;
|
private byte metaState;
|
||||||
|
|
||||||
DefaultStream(int id, State state) {
|
DefaultStream(int id, State state) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@ -398,35 +401,60 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isResetSent() {
|
public boolean isResetSent() {
|
||||||
return (sentState & SENT_STATE_RST) != 0;
|
return (metaState & META_STATE_SENT_RST) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Http2Stream resetSent() {
|
public Http2Stream resetSent() {
|
||||||
sentState |= SENT_STATE_RST;
|
metaState |= META_STATE_SENT_RST;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Http2Stream headersSent() {
|
public Http2Stream headersSent(boolean isInformational) {
|
||||||
sentState |= SENT_STATE_HEADERS;
|
if (!isInformational) {
|
||||||
|
metaState |= isHeadersSent() ? META_STATE_SENT_TRAILERS : META_STATE_SENT_HEADERS;
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isHeadersSent() {
|
public boolean isHeadersSent() {
|
||||||
return (sentState & SENT_STATE_HEADERS) != 0;
|
return (metaState & META_STATE_SENT_HEADERS) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTrailersSent() {
|
||||||
|
return (metaState & META_STATE_SENT_TRAILERS) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Http2Stream headersReceived(boolean isInformational) {
|
||||||
|
if (!isInformational) {
|
||||||
|
metaState |= isHeadersReceived() ? META_STATE_RECV_TRAILERS : META_STATE_RECV_HEADERS;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isHeadersReceived() {
|
||||||
|
return (metaState & META_STATE_RECV_HEADERS) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTrailersReceived() {
|
||||||
|
return (metaState & META_STATE_RECV_TRAILERS) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Http2Stream pushPromiseSent() {
|
public Http2Stream pushPromiseSent() {
|
||||||
sentState |= SENT_STATE_PUSHPROMISE;
|
metaState |= META_STATE_SENT_PUSHPROMISE;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPushPromiseSent() {
|
public boolean isPushPromiseSent() {
|
||||||
return (sentState & SENT_STATE_PUSHPROMISE) != 0;
|
return (metaState & META_STATE_SENT_PUSHPROMISE) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -599,7 +627,7 @@ public class DefaultHttp2Connection implements Http2Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Http2Stream headersSent() {
|
public Http2Stream headersSent(boolean isInformational) {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ package io.netty.handler.codec.http2;
|
|||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.http.HttpStatusClass;
|
||||||
import io.netty.handler.codec.http2.Http2Connection.Endpoint;
|
import io.netty.handler.codec.http2.Http2Connection.Endpoint;
|
||||||
import io.netty.util.internal.UnstableApi;
|
import io.netty.util.internal.UnstableApi;
|
||||||
import io.netty.util.internal.logging.InternalLogger;
|
import io.netty.util.internal.logging.InternalLogger;
|
||||||
@ -23,6 +24,7 @@ import io.netty.util.internal.logging.InternalLoggerFactory;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL;
|
||||||
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
|
||||||
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
|
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
|
||||||
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
|
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
|
||||||
@ -282,6 +284,14 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isInformational = !connection.isServer() &&
|
||||||
|
HttpStatusClass.valueOf(headers.status()) == INFORMATIONAL;
|
||||||
|
if ((isInformational || !endOfStream) && stream.isHeadersReceived() || stream.isTrailersReceived()) {
|
||||||
|
throw streamError(streamId, PROTOCOL_ERROR,
|
||||||
|
"Stream %d received too many headers EOS: %s state: %s",
|
||||||
|
streamId, endOfStream, stream.state());
|
||||||
|
}
|
||||||
|
|
||||||
switch (stream.state()) {
|
switch (stream.state()) {
|
||||||
case RESERVED_REMOTE:
|
case RESERVED_REMOTE:
|
||||||
stream.open(endOfStream);
|
stream.open(endOfStream);
|
||||||
@ -305,6 +315,7 @@ public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
|
|||||||
stream.state());
|
stream.state());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream.headersReceived(isInformational);
|
||||||
encoder.flowController().updateDependencyTree(streamId, streamDependency, weight, exclusive);
|
encoder.flowController().updateDependencyTree(streamId, streamDependency, weight, exclusive);
|
||||||
|
|
||||||
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream);
|
||||||
|
@ -21,10 +21,12 @@ import io.netty.channel.ChannelFutureListener;
|
|||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.CoalescingBufferQueue;
|
import io.netty.channel.CoalescingBufferQueue;
|
||||||
|
import io.netty.handler.codec.http.HttpStatusClass;
|
||||||
import io.netty.util.internal.UnstableApi;
|
import io.netty.util.internal.UnstableApi;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
|
||||||
|
import static io.netty.handler.codec.http.HttpStatusClass.INFORMATIONAL;
|
||||||
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
|
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
|
||||||
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
|
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
|
||||||
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
|
||||||
@ -145,6 +147,15 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
return writeHeaders(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endStream, promise);
|
return writeHeaders(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endStream, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean validateHeadersSentState(Http2Stream stream, Http2Headers headers, boolean isServer,
|
||||||
|
boolean endOfStream) {
|
||||||
|
boolean isInformational = isServer && HttpStatusClass.valueOf(headers.status()) == INFORMATIONAL;
|
||||||
|
if ((isInformational || !endOfStream) && stream.isHeadersSent() || stream.isTrailersSent()) {
|
||||||
|
throw new IllegalStateException("Stream " + stream.id() + " sent too many headers EOS: " + endOfStream);
|
||||||
|
}
|
||||||
|
return isInformational;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId,
|
public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId,
|
||||||
final Http2Headers headers, final int streamDependency, final short weight,
|
final Http2Headers headers, final int streamDependency, final short weight,
|
||||||
@ -180,6 +191,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
// for this stream.
|
// for this stream.
|
||||||
Http2RemoteFlowController flowController = flowController();
|
Http2RemoteFlowController flowController = flowController();
|
||||||
if (!endOfStream || !flowController.hasFlowControlled(stream)) {
|
if (!endOfStream || !flowController.hasFlowControlled(stream)) {
|
||||||
|
boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
|
||||||
if (endOfStream) {
|
if (endOfStream) {
|
||||||
final Http2Stream finalStream = stream;
|
final Http2Stream finalStream = stream;
|
||||||
final ChannelFutureListener closeStreamLocalListener = new ChannelFutureListener() {
|
final ChannelFutureListener closeStreamLocalListener = new ChannelFutureListener() {
|
||||||
@ -190,6 +202,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
};
|
};
|
||||||
promise = promise.unvoid().addListener(closeStreamLocalListener);
|
promise = promise.unvoid().addListener(closeStreamLocalListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency,
|
ChannelFuture future = frameWriter.writeHeaders(ctx, streamId, headers, streamDependency,
|
||||||
weight, exclusive, padding, endOfStream, promise);
|
weight, exclusive, padding, endOfStream, promise);
|
||||||
// Writing headers may fail during the encode state if they violate HPACK limits.
|
// Writing headers may fail during the encode state if they violate HPACK limits.
|
||||||
@ -197,7 +210,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
if (failureCause == null) {
|
if (failureCause == null) {
|
||||||
// Synchronously set the headersSent flag to ensure that we do not subsequently write
|
// Synchronously set the headersSent flag to ensure that we do not subsequently write
|
||||||
// other headers containing pseudo-header fields.
|
// other headers containing pseudo-header fields.
|
||||||
stream.headersSent();
|
stream.headersSent(isInformational);
|
||||||
} else {
|
} else {
|
||||||
lifecycleManager.onError(ctx, failureCause);
|
lifecycleManager.onError(ctx, failureCause);
|
||||||
}
|
}
|
||||||
@ -451,6 +464,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(ChannelHandlerContext ctx, int allowedBytes) {
|
public void write(ChannelHandlerContext ctx, int allowedBytes) {
|
||||||
|
boolean isInformational = validateHeadersSentState(stream, headers, connection.isServer(), endOfStream);
|
||||||
if (promise.isVoid()) {
|
if (promise.isVoid()) {
|
||||||
promise = ctx.newPromise();
|
promise = ctx.newPromise();
|
||||||
}
|
}
|
||||||
@ -461,7 +475,7 @@ public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
|
|||||||
// Writing headers may fail during the encode state if they violate HPACK limits.
|
// Writing headers may fail during the encode state if they violate HPACK limits.
|
||||||
Throwable failureCause = f.cause();
|
Throwable failureCause = f.cause();
|
||||||
if (failureCause == null) {
|
if (failureCause == null) {
|
||||||
stream.headersSent();
|
stream.headersSent(isInformational);
|
||||||
} else {
|
} else {
|
||||||
lifecycleManager.onError(ctx, failureCause);
|
lifecycleManager.onError(ctx, failureCause);
|
||||||
}
|
}
|
||||||
|
@ -130,15 +130,41 @@ public interface Http2Stream {
|
|||||||
<V> V removeProperty(Http2Connection.PropertyKey key);
|
<V> V removeProperty(Http2Connection.PropertyKey key);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that headers has been sent to the remote on this stream.
|
* Indicates that headers have been sent to the remote endpoint on this stream. The first call to this method would
|
||||||
|
* be for the initial headers (see {@link #isHeadersSent()}} and the second call would indicate the trailers
|
||||||
|
* (see {@link #isTrailersReceived()}).
|
||||||
|
* @param isInformational {@code true} if the headers contain an informational status code (for responses only).
|
||||||
*/
|
*/
|
||||||
Http2Stream headersSent();
|
Http2Stream headersSent(boolean isInformational);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether or not headers was sent to the remote endpoint.
|
* Indicates whether or not headers were sent to the remote endpoint.
|
||||||
*/
|
*/
|
||||||
boolean isHeadersSent();
|
boolean isHeadersSent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether or not trailers were sent to the remote endpoint.
|
||||||
|
*/
|
||||||
|
boolean isTrailersSent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that headers have been received. The first call to this method would be for the initial headers
|
||||||
|
* (see {@link #isHeadersReceived()}} and the second call would indicate the trailers
|
||||||
|
* (see {@link #isTrailersReceived()}).
|
||||||
|
* @param isInformational {@code true} if the headers contain an informational status code (for responses only).
|
||||||
|
*/
|
||||||
|
Http2Stream headersReceived(boolean isInformational);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether or not the initial headers have been received.
|
||||||
|
*/
|
||||||
|
boolean isHeadersReceived();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether or not the trailers have been received.
|
||||||
|
*/
|
||||||
|
boolean isTrailersReceived();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that a push promise was sent to the remote endpoint.
|
* Indicates that a push promise was sent to the remote endpoint.
|
||||||
*/
|
*/
|
||||||
|
@ -21,6 +21,7 @@ import io.netty.channel.ChannelFuture;
|
|||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.DefaultChannelPromise;
|
import io.netty.channel.DefaultChannelPromise;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import junit.framework.AssertionFailedError;
|
import junit.framework.AssertionFailedError;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -50,10 +51,10 @@ import static org.mockito.Mockito.anyBoolean;
|
|||||||
import static org.mockito.Mockito.anyInt;
|
import static org.mockito.Mockito.anyInt;
|
||||||
import static org.mockito.Mockito.anyLong;
|
import static org.mockito.Mockito.anyLong;
|
||||||
import static org.mockito.Mockito.anyShort;
|
import static org.mockito.Mockito.anyShort;
|
||||||
import static org.mockito.Mockito.eq;
|
|
||||||
import static org.mockito.Mockito.isNull;
|
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.eq;
|
||||||
|
import static org.mockito.Mockito.isNull;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@ -67,6 +68,8 @@ public class DefaultHttp2ConnectionDecoderTest {
|
|||||||
private static final int STREAM_ID = 3;
|
private static final int STREAM_ID = 3;
|
||||||
private static final int PUSH_STREAM_ID = 2;
|
private static final int PUSH_STREAM_ID = 2;
|
||||||
private static final int STREAM_DEPENDENCY_ID = 5;
|
private static final int STREAM_DEPENDENCY_ID = 5;
|
||||||
|
private static final int STATE_RECV_HEADERS = 1;
|
||||||
|
private static final int STATE_RECV_TRAILERS = 1 << 1;
|
||||||
|
|
||||||
private Http2ConnectionDecoder decoder;
|
private Http2ConnectionDecoder decoder;
|
||||||
private ChannelPromise promise;
|
private ChannelPromise promise;
|
||||||
@ -122,11 +125,49 @@ public class DefaultHttp2ConnectionDecoderTest {
|
|||||||
|
|
||||||
promise = new DefaultChannelPromise(channel);
|
promise = new DefaultChannelPromise(channel);
|
||||||
|
|
||||||
|
final AtomicInteger headersReceivedState = new AtomicInteger();
|
||||||
when(channel.isActive()).thenReturn(true);
|
when(channel.isActive()).thenReturn(true);
|
||||||
when(stream.id()).thenReturn(STREAM_ID);
|
when(stream.id()).thenReturn(STREAM_ID);
|
||||||
when(stream.state()).thenReturn(OPEN);
|
when(stream.state()).thenReturn(OPEN);
|
||||||
when(stream.open(anyBoolean())).thenReturn(stream);
|
when(stream.open(anyBoolean())).thenReturn(stream);
|
||||||
when(pushStream.id()).thenReturn(PUSH_STREAM_ID);
|
when(pushStream.id()).thenReturn(PUSH_STREAM_ID);
|
||||||
|
doAnswer(new Answer<Boolean>() {
|
||||||
|
@Override
|
||||||
|
public Boolean answer(InvocationOnMock in) throws Throwable {
|
||||||
|
return (headersReceivedState.get() & STATE_RECV_HEADERS) != 0;
|
||||||
|
}
|
||||||
|
}).when(stream).isHeadersReceived();
|
||||||
|
doAnswer(new Answer<Boolean>() {
|
||||||
|
@Override
|
||||||
|
public Boolean answer(InvocationOnMock in) throws Throwable {
|
||||||
|
return (headersReceivedState.get() & STATE_RECV_TRAILERS) != 0;
|
||||||
|
}
|
||||||
|
}).when(stream).isTrailersReceived();
|
||||||
|
doAnswer(new Answer<Http2Stream>() {
|
||||||
|
@Override
|
||||||
|
public Http2Stream answer(InvocationOnMock in) throws Throwable {
|
||||||
|
boolean isInformational = in.getArgument(0);
|
||||||
|
if (isInformational) {
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
for (;;) {
|
||||||
|
int current = headersReceivedState.get();
|
||||||
|
int next = current;
|
||||||
|
if ((current & STATE_RECV_HEADERS) != 0) {
|
||||||
|
if ((current & STATE_RECV_TRAILERS) != 0) {
|
||||||
|
throw new IllegalStateException("already sent headers!");
|
||||||
|
}
|
||||||
|
next |= STATE_RECV_TRAILERS;
|
||||||
|
} else {
|
||||||
|
next |= STATE_RECV_HEADERS;
|
||||||
|
}
|
||||||
|
if (headersReceivedState.compareAndSet(current, next)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}).when(stream).headersReceived(anyBoolean());
|
||||||
doAnswer(new Answer<Http2Stream>() {
|
doAnswer(new Answer<Http2Stream>() {
|
||||||
@Override
|
@Override
|
||||||
public Http2Stream answer(InvocationOnMock in) throws Throwable {
|
public Http2Stream answer(InvocationOnMock in) throws Throwable {
|
||||||
@ -452,7 +493,65 @@ public class DefaultHttp2ConnectionDecoderTest {
|
|||||||
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = Http2Exception.class)
|
||||||
|
public void trailersDoNotEndStreamThrows() throws Exception {
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
|
||||||
|
// Trailers must end the stream!
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = Http2Exception.class)
|
||||||
|
public void tooManyHeadersEOSThrows() throws Exception {
|
||||||
|
tooManyHeaderThrows(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = Http2Exception.class)
|
||||||
|
public void tooManyHeadersNoEOSThrows() throws Exception {
|
||||||
|
tooManyHeaderThrows(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tooManyHeaderThrows(boolean eos) throws Exception {
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true);
|
||||||
|
// We already received the trailers!
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, eos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Http2Headers informationalHeaders() {
|
||||||
|
Http2Headers headers = new DefaultHttp2Headers();
|
||||||
|
headers.status(HttpResponseStatus.CONTINUE.codeAsText());
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
public void infoHeadersAndTrailersAllowed() throws Exception {
|
||||||
|
infoHeadersAndTrailersAllowed(true, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleInfoHeadersAndTrailersAllowed() throws Exception {
|
||||||
|
infoHeadersAndTrailersAllowed(true, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = Http2Exception.class)
|
||||||
|
public void infoHeadersAndTrailersNoEOSThrows() throws Exception {
|
||||||
|
infoHeadersAndTrailersAllowed(false, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = Http2Exception.class)
|
||||||
|
public void multipleInfoHeadersAndTrailersNoEOSThrows() throws Exception {
|
||||||
|
infoHeadersAndTrailersAllowed(false, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void infoHeadersAndTrailersAllowed(boolean eos, int infoHeaderCount) throws Exception {
|
||||||
|
for (int i = 0; i < infoHeaderCount; ++i) {
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, informationalHeaders(), 0, false);
|
||||||
|
}
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
|
||||||
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, eos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
public void headersReadForPromisedStreamShouldCloseStream() throws Exception {
|
public void headersReadForPromisedStreamShouldCloseStream() throws Exception {
|
||||||
when(stream.state()).thenReturn(RESERVED_REMOTE);
|
when(stream.state()).thenReturn(RESERVED_REMOTE);
|
||||||
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true);
|
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true);
|
||||||
|
@ -24,6 +24,7 @@ import io.netty.channel.ChannelHandlerContext;
|
|||||||
import io.netty.channel.ChannelPipeline;
|
import io.netty.channel.ChannelPipeline;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.DefaultChannelPromise;
|
import io.netty.channel.DefaultChannelPromise;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.codec.http2.Http2RemoteFlowController.FlowControlled;
|
import io.netty.handler.codec.http2.Http2RemoteFlowController.FlowControlled;
|
||||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||||
import junit.framework.AssertionFailedError;
|
import junit.framework.AssertionFailedError;
|
||||||
@ -59,11 +60,12 @@ import static org.mockito.Mockito.anyBoolean;
|
|||||||
import static org.mockito.Mockito.anyInt;
|
import static org.mockito.Mockito.anyInt;
|
||||||
import static org.mockito.Mockito.anyLong;
|
import static org.mockito.Mockito.anyLong;
|
||||||
import static org.mockito.Mockito.anyShort;
|
import static org.mockito.Mockito.anyShort;
|
||||||
import static org.mockito.Mockito.eq;
|
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@ -342,11 +344,208 @@ public class DefaultHttp2ConnectionEncoderTest {
|
|||||||
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void trailersDoNotEndStreamThrows() {
|
||||||
|
writeAllFlowControlledFrames();
|
||||||
|
final int streamId = 6;
|
||||||
|
ChannelPromise promise = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
|
||||||
|
|
||||||
|
ChannelPromise promise2 = newPromise();
|
||||||
|
ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2);
|
||||||
|
assertTrue(future.isDone());
|
||||||
|
assertFalse(future.isSuccess());
|
||||||
|
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void trailersDoNotEndStreamWithDataThrows() {
|
||||||
|
writeAllFlowControlledFrames();
|
||||||
|
final int streamId = 6;
|
||||||
|
ChannelPromise promise = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
|
||||||
|
|
||||||
|
Http2Stream stream = connection.stream(streamId);
|
||||||
|
when(remoteFlow.hasFlowControlled(eq(stream))).thenReturn(true);
|
||||||
|
|
||||||
|
ChannelPromise promise2 = newPromise();
|
||||||
|
ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2);
|
||||||
|
assertTrue(future.isDone());
|
||||||
|
assertFalse(future.isSuccess());
|
||||||
|
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tooManyHeadersNoEOSThrows() {
|
||||||
|
tooManyHeadersThrows(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tooManyHeadersEOSThrows() {
|
||||||
|
tooManyHeadersThrows(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tooManyHeadersThrows(boolean eos) {
|
||||||
|
writeAllFlowControlledFrames();
|
||||||
|
final int streamId = 6;
|
||||||
|
ChannelPromise promise = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
|
||||||
|
ChannelPromise promise2 = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, true, promise2);
|
||||||
|
|
||||||
|
ChannelPromise promise3 = newPromise();
|
||||||
|
ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3);
|
||||||
|
assertTrue(future.isDone());
|
||||||
|
assertFalse(future.isSuccess());
|
||||||
|
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void infoHeadersAndTrailersAllowed() throws Exception {
|
||||||
|
infoHeadersAndTrailers(true, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleInfoHeadersAndTrailersAllowed() throws Exception {
|
||||||
|
infoHeadersAndTrailers(true, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void infoHeadersAndTrailersNoEOSThrows() throws Exception {
|
||||||
|
infoHeadersAndTrailers(false, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleInfoHeadersAndTrailersNoEOSThrows() throws Exception {
|
||||||
|
infoHeadersAndTrailers(false, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void infoHeadersAndTrailers(boolean eos, int infoHeaderCount) {
|
||||||
|
writeAllFlowControlledFrames();
|
||||||
|
final int streamId = 6;
|
||||||
|
Http2Headers infoHeaders = informationalHeaders();
|
||||||
|
for (int i = 0; i < infoHeaderCount; ++i) {
|
||||||
|
encoder.writeHeaders(ctx, streamId, infoHeaders, 0, false, newPromise());
|
||||||
|
}
|
||||||
|
ChannelPromise promise2 = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2);
|
||||||
|
|
||||||
|
ChannelPromise promise3 = newPromise();
|
||||||
|
ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3);
|
||||||
|
assertTrue(future.isDone());
|
||||||
|
assertEquals(eos, future.isSuccess());
|
||||||
|
|
||||||
|
verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class));
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2));
|
||||||
|
if (eos) {
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Http2Headers informationalHeaders() {
|
||||||
|
Http2Headers headers = new DefaultHttp2Headers();
|
||||||
|
headers.status(HttpResponseStatus.CONTINUE.codeAsText());
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tooManyHeadersWithDataNoEOSThrows() {
|
||||||
|
tooManyHeadersWithDataThrows(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void tooManyHeadersWithDataEOSThrows() {
|
||||||
|
tooManyHeadersWithDataThrows(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tooManyHeadersWithDataThrows(boolean eos) {
|
||||||
|
writeAllFlowControlledFrames();
|
||||||
|
final int streamId = 6;
|
||||||
|
ChannelPromise promise = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
|
||||||
|
|
||||||
|
Http2Stream stream = connection.stream(streamId);
|
||||||
|
when(remoteFlow.hasFlowControlled(eq(stream))).thenReturn(true);
|
||||||
|
|
||||||
|
ChannelPromise promise2 = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, true, promise2);
|
||||||
|
|
||||||
|
ChannelPromise promise3 = newPromise();
|
||||||
|
ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3);
|
||||||
|
assertTrue(future.isDone());
|
||||||
|
assertFalse(future.isSuccess());
|
||||||
|
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void infoHeadersAndTrailersWithDataAllowed() {
|
||||||
|
infoHeadersAndTrailersWithData(true, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleInfoHeadersAndTrailersWithDataAllowed() {
|
||||||
|
infoHeadersAndTrailersWithData(true, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void infoHeadersAndTrailersWithDataNoEOSThrows() {
|
||||||
|
infoHeadersAndTrailersWithData(false, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void multipleInfoHeadersAndTrailersWithDataNoEOSThrows() {
|
||||||
|
infoHeadersAndTrailersWithData(false, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void infoHeadersAndTrailersWithData(boolean eos, int infoHeaderCount) {
|
||||||
|
writeAllFlowControlledFrames();
|
||||||
|
final int streamId = 6;
|
||||||
|
Http2Headers infoHeaders = informationalHeaders();
|
||||||
|
for (int i = 0; i < infoHeaderCount; ++i) {
|
||||||
|
encoder.writeHeaders(ctx, streamId, infoHeaders, 0, false, newPromise());
|
||||||
|
}
|
||||||
|
|
||||||
|
Http2Stream stream = connection.stream(streamId);
|
||||||
|
when(remoteFlow.hasFlowControlled(eq(stream))).thenReturn(true);
|
||||||
|
|
||||||
|
ChannelPromise promise2 = newPromise();
|
||||||
|
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise2);
|
||||||
|
|
||||||
|
ChannelPromise promise3 = newPromise();
|
||||||
|
ChannelFuture future = encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, eos, promise3);
|
||||||
|
assertTrue(future.isDone());
|
||||||
|
assertEquals(eos, future.isSuccess());
|
||||||
|
|
||||||
|
verify(writer, times(infoHeaderCount)).writeHeaders(eq(ctx), eq(streamId), eq(infoHeaders), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), any(ChannelPromise.class));
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise2));
|
||||||
|
if (eos) {
|
||||||
|
verify(writer, times(1)).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
|
||||||
|
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void pushPromiseWriteAfterGoAwayReceivedShouldFail() throws Exception {
|
public void pushPromiseWriteAfterGoAwayReceivedShouldFail() throws Exception {
|
||||||
createStream(STREAM_ID, false);
|
createStream(STREAM_ID, false);
|
||||||
goAwayReceived(0);
|
goAwayReceived(0);
|
||||||
ChannelFuture future = encoder.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0,
|
ChannelFuture future = encoder.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0,
|
||||||
newPromise());
|
newPromise());
|
||||||
assertTrue(future.isDone());
|
assertTrue(future.isDone());
|
||||||
assertFalse(future.isSuccess());
|
assertFalse(future.isSuccess());
|
||||||
|
@ -360,49 +360,6 @@ public class InboundHttp2ToHttpAdapterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void clientRequestMultipleHeaders() throws Exception {
|
|
||||||
boostrapEnv(1, 1, 1);
|
|
||||||
// writeHeaders will implicitly add an END_HEADERS tag each time and so this test does not follow the HTTP
|
|
||||||
// message flow. We currently accept this message flow and just add the second headers to the trailing headers.
|
|
||||||
final String text = "";
|
|
||||||
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
|
|
||||||
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
|
|
||||||
"/some/path/resource2", content, true);
|
|
||||||
try {
|
|
||||||
HttpHeaders httpHeaders = request.headers();
|
|
||||||
httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
|
|
||||||
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
|
|
||||||
httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
|
|
||||||
HttpHeaders trailingHeaders = request.trailingHeaders();
|
|
||||||
trailingHeaders.set(of("FoO"), of("goo"));
|
|
||||||
trailingHeaders.set(of("foO2"), of("goo2"));
|
|
||||||
trailingHeaders.add(of("fOo2"), of("goo3"));
|
|
||||||
final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
|
|
||||||
new AsciiString("/some/path/resource2"));
|
|
||||||
final Http2Headers http2Headers2 = new DefaultHttp2Headers()
|
|
||||||
.set(new AsciiString("foo"), new AsciiString("goo"))
|
|
||||||
.set(new AsciiString("foo2"), new AsciiString("goo2"))
|
|
||||||
.add(new AsciiString("foo2"), new AsciiString("goo3"));
|
|
||||||
runInChannel(clientChannel, new Http2Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() throws Http2Exception {
|
|
||||||
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
|
|
||||||
clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers2, 0, false, newPromiseClient());
|
|
||||||
clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
|
|
||||||
clientChannel.flush();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
awaitRequests();
|
|
||||||
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
|
|
||||||
verify(serverListener).messageReceived(requestCaptor.capture());
|
|
||||||
capturedRequests = requestCaptor.getAllValues();
|
|
||||||
assertEquals(request, capturedRequests.get(0));
|
|
||||||
} finally {
|
|
||||||
request.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void clientRequestTrailingHeaders() throws Exception {
|
public void clientRequestTrailingHeaders() throws Exception {
|
||||||
boostrapEnv(1, 1, 1);
|
boostrapEnv(1, 1, 1);
|
||||||
|
Loading…
Reference in New Issue
Block a user