HTTP/2 draft 14 HTTP message flow

Motivation:

HTTP/2 draft 14 came out a couple of weeks ago and we need to keep up
with the spec.

Modifications:
-Revert back to dispatching FullHttpMessage objects instead of individual HttpObjects
-Corrections to HttpObject comparitors to support test cases
-New test cases to support sending headers immediatley
-Bug fixes cleaned up to ensure the message flow is terminated properly

Result:
Netty HTTP/2 to HTTP/1.x translation layer will support the HTTP/2 draft message flow.
This commit is contained in:
Scott Mitchell 2014-08-22 17:22:18 -04:00 committed by nmittler
parent 5b1d50fa7c
commit 892944eba8
23 changed files with 1153 additions and 753 deletions

View File

@ -17,11 +17,13 @@ package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.util.internal.StringUtil;
/** /**
* Default implementation of {@link FullHttpRequest}. * Default implementation of {@link FullHttpRequest}.
*/ */
public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHttpRequest { public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHttpRequest {
private static final int HASH_CODE_PRIME = 31;
private final ByteBuf content; private final ByteBuf content;
private final HttpHeaders trailingHeader; private final HttpHeaders trailingHeader;
private final boolean validateHeaders; private final boolean validateHeaders;
@ -34,6 +36,10 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
this(httpVersion, method, uri, content, true); this(httpVersion, method, uri, content, true);
} }
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, boolean validateHeaders) {
this(httpVersion, method, uri, Unpooled.buffer(0), true);
}
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri,
ByteBuf content, boolean validateHeaders) { ByteBuf content, boolean validateHeaders) {
super(httpVersion, method, uri, validateHeaders); super(httpVersion, method, uri, validateHeaders);
@ -112,15 +118,41 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
return this; return this;
} }
@Override /**
public FullHttpRequest copy() { * Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpRequest copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpRequest copy = new DefaultFullHttpRequest( DefaultFullHttpRequest copy = new DefaultFullHttpRequest(
protocolVersion(), method(), uri(), content().copy(), validateHeaders); protocolVersion(), method(), uri(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers()); copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders()); copy.trailingHeaders().set(trailingHeaders());
return copy; return copy;
} }
@Override
public FullHttpRequest copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpRequest copy() {
return copy(true, null);
}
@Override @Override
public FullHttpRequest duplicate() { public FullHttpRequest duplicate() {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest( DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(
@ -129,4 +161,38 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
duplicate.trailingHeaders().set(trailingHeaders()); duplicate.trailingHeaders().set(trailingHeaders());
return duplicate; return duplicate;
} }
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + content().hashCode();
result = HASH_CODE_PRIME * result + trailingHeaders().hashCode();
result = HASH_CODE_PRIME * result + super.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultFullHttpRequest)) {
return false;
}
DefaultFullHttpRequest other = (DefaultFullHttpRequest) o;
return super.equals(other) &&
content().equals(other.content()) &&
trailingHeaders().equals(other.trailingHeaders());
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
appendAll(buf);
buf.append(StringUtil.NEWLINE);
appendHeaders(buf, trailingHeaders());
// Remove the last newline.
buf.setLength(buf.length() - StringUtil.NEWLINE.length());
return buf.toString();
}
} }

View File

@ -17,13 +17,14 @@ package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.util.internal.StringUtil;
/** /**
* Default implementation of a {@link FullHttpResponse}. * Default implementation of a {@link FullHttpResponse}.
*/ */
public class DefaultFullHttpResponse extends DefaultHttpResponse implements FullHttpResponse { public class DefaultFullHttpResponse extends DefaultHttpResponse implements FullHttpResponse {
private static final int HASH_CODE_PRIME = 31;
private final ByteBuf content; private final ByteBuf content;
private final HttpHeaders trailingHeaders; private final HttpHeaders trailingHeaders;
private final boolean validateHeaders; private final boolean validateHeaders;
@ -36,6 +37,10 @@ public class DefaultFullHttpResponse extends DefaultHttpResponse implements Full
this(version, status, content, true); this(version, status, content, true);
} }
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders) {
this(version, status, Unpooled.buffer(0), validateHeaders);
}
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status,
ByteBuf content, boolean validateHeaders) { ByteBuf content, boolean validateHeaders) {
super(version, status, validateHeaders); super(version, status, validateHeaders);
@ -108,15 +113,41 @@ public class DefaultFullHttpResponse extends DefaultHttpResponse implements Full
return this; return this;
} }
@Override /**
public FullHttpResponse copy() { * Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpResponse copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpResponse copy = new DefaultFullHttpResponse( DefaultFullHttpResponse copy = new DefaultFullHttpResponse(
protocolVersion(), status(), content().copy(), validateHeaders); protocolVersion(), status(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers()); copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders()); copy.trailingHeaders().set(trailingHeaders());
return copy; return copy;
} }
@Override
public FullHttpResponse copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpResponse copy() {
return copy(true, null);
}
@Override @Override
public FullHttpResponse duplicate() { public FullHttpResponse duplicate() {
DefaultFullHttpResponse duplicate = new DefaultFullHttpResponse(protocolVersion(), status(), DefaultFullHttpResponse duplicate = new DefaultFullHttpResponse(protocolVersion(), status(),
@ -125,4 +156,38 @@ public class DefaultFullHttpResponse extends DefaultHttpResponse implements Full
duplicate.trailingHeaders().set(trailingHeaders()); duplicate.trailingHeaders().set(trailingHeaders());
return duplicate; return duplicate;
} }
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + content().hashCode();
result = HASH_CODE_PRIME * result + trailingHeaders().hashCode();
result = HASH_CODE_PRIME * result + super.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultFullHttpResponse)) {
return false;
}
DefaultFullHttpResponse other = (DefaultFullHttpResponse) o;
return super.equals(other) &&
content().equals(other.content()) &&
trailingHeaders().equals(other.trailingHeaders());
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
appendAll(buf);
buf.append(StringUtil.NEWLINE);
appendHeaders(buf, trailingHeaders());
// Remove the last newline.
buf.setLength(buf.length() - StringUtil.NEWLINE.length());
return buf.toString();
}
} }

View File

@ -104,6 +104,10 @@ public abstract class DefaultHttpMessage extends DefaultHttpObject implements Ht
} }
void appendHeaders(StringBuilder buf) { void appendHeaders(StringBuilder buf) {
appendHeaders(buf, headers());
}
void appendHeaders(StringBuilder buf, HttpHeaders headers) {
for (Map.Entry<String, String> e: headers()) { for (Map.Entry<String, String> e: headers()) {
buf.append(e.getKey()); buf.append(e.getKey());
buf.append(": "); buf.append(": ");

View File

@ -21,7 +21,7 @@ import io.netty.util.internal.StringUtil;
* The default {@link HttpRequest} implementation. * The default {@link HttpRequest} implementation.
*/ */
public class DefaultHttpRequest extends DefaultHttpMessage implements HttpRequest { public class DefaultHttpRequest extends DefaultHttpMessage implements HttpRequest {
private static final int HASH_CODE_PRIME = 31;
private HttpMethod method; private HttpMethod method;
private String uri; private String uri;
@ -90,9 +90,39 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
return this; return this;
} }
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + method.hashCode();
result = HASH_CODE_PRIME * result + uri.hashCode();
result = HASH_CODE_PRIME * result + super.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultHttpRequest)) {
return false;
}
DefaultHttpRequest other = (DefaultHttpRequest) o;
return method().equals(other.method()) &&
uri().equalsIgnoreCase(other.uri()) &&
super.equals(o);
}
@Override @Override
public String toString() { public String toString() {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
appendAll(buf);
// Remove the last newline.
buf.setLength(buf.length() - StringUtil.NEWLINE.length());
return buf.toString();
}
void appendAll(StringBuilder buf) {
buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.simpleClassName(this));
buf.append("(decodeResult: "); buf.append("(decodeResult: ");
buf.append(decoderResult()); buf.append(decoderResult());
@ -105,9 +135,5 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
buf.append(protocolVersion().text()); buf.append(protocolVersion().text());
buf.append(StringUtil.NEWLINE); buf.append(StringUtil.NEWLINE);
appendHeaders(buf); appendHeaders(buf);
// Remove the last newline.
buf.setLength(buf.length() - StringUtil.NEWLINE.length());
return buf.toString();
} }
} }

View File

@ -21,7 +21,7 @@ import io.netty.util.internal.StringUtil;
* The default {@link HttpResponse} implementation. * The default {@link HttpResponse} implementation.
*/ */
public class DefaultHttpResponse extends DefaultHttpMessage implements HttpResponse { public class DefaultHttpResponse extends DefaultHttpMessage implements HttpResponse {
private static final int HASH_CODE_PRIME = 31;
private HttpResponseStatus status; private HttpResponseStatus status;
/** /**
@ -71,10 +71,9 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31;
int result = 1; int result = 1;
result = prime * result + status.hashCode(); result = HASH_CODE_PRIME * result + status.hashCode();
result = prime * result + super.hashCode(); result = HASH_CODE_PRIME * result + super.hashCode();
return result; return result;
} }
@ -92,6 +91,14 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
@Override @Override
public String toString() { public String toString() {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
appendAll(buf);
// Remove the last newline.
buf.setLength(buf.length() - StringUtil.NEWLINE.length());
return buf.toString();
}
void appendAll(StringBuilder buf) {
buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.simpleClassName(this));
buf.append("(decodeResult: "); buf.append("(decodeResult: ");
buf.append(decoderResult()); buf.append(decoderResult());
@ -102,9 +109,5 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
buf.append(status()); buf.append(status());
buf.append(StringUtil.NEWLINE); buf.append(StringUtil.NEWLINE);
appendHeaders(buf); appendHeaders(buf);
// Remove the last newline.
buf.setLength(buf.length() - StringUtil.NEWLINE.length());
return buf.toString();
} }
} }

View File

@ -15,11 +15,23 @@
*/ */
package io.netty.handler.codec.http; package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/** /**
* Combines {@link HttpMessage} and {@link LastHttpContent} into one * Combines {@link HttpMessage} and {@link LastHttpContent} into one
* message. So it represent a <i>complete</i> http message. * message. So it represent a <i>complete</i> http message.
*/ */
public interface FullHttpMessage extends HttpMessage, LastHttpContent { public interface FullHttpMessage extends HttpMessage, LastHttpContent {
/**
* Create a copy of this {@link FullHttpMessage} with alternative content.
*
* @param newContent The buffer to use instead of this {@link FullHttpMessage}'s content in the copy operation.
* <p>
* NOTE: retain will NOT be called on this buffer. {@code null} results in an empty default choice buffer.
* @return The result of the copy operation
*/
FullHttpMessage copy(ByteBuf newContent);
@Override @Override
FullHttpMessage copy(); FullHttpMessage copy();

View File

@ -15,11 +15,15 @@
*/ */
package io.netty.handler.codec.http; package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/** /**
* Combinate the {@link HttpRequest} and {@link FullHttpMessage}, so the request is a <i>complete</i> HTTP * Combinate the {@link HttpRequest} and {@link FullHttpMessage}, so the request is a <i>complete</i> HTTP request.
* request.
*/ */
public interface FullHttpRequest extends HttpRequest, FullHttpMessage { public interface FullHttpRequest extends HttpRequest, FullHttpMessage {
@Override
FullHttpRequest copy(ByteBuf newContent);
@Override @Override
FullHttpRequest copy(); FullHttpRequest copy();

View File

@ -15,11 +15,15 @@
*/ */
package io.netty.handler.codec.http; package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/** /**
* Combination of a {@link HttpResponse} and {@link FullHttpMessage}. * Combination of a {@link HttpResponse} and {@link FullHttpMessage}. So it represent a <i>complete</i> http response.
* So it represent a <i>complete</i> http response.
*/ */
public interface FullHttpResponse extends HttpResponse, FullHttpMessage { public interface FullHttpResponse extends HttpResponse, FullHttpMessage {
@Override
FullHttpResponse copy(ByteBuf newContent);
@Override @Override
FullHttpResponse copy(); FullHttpResponse copy();

View File

@ -263,15 +263,41 @@ public class HttpObjectAggregator
super(request, content, trailingHeaders); super(request, content, trailingHeaders);
} }
@Override /**
public FullHttpRequest copy() { * Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpRequest copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpRequest copy = new DefaultFullHttpRequest( DefaultFullHttpRequest copy = new DefaultFullHttpRequest(
protocolVersion(), method(), uri(), content().copy()); protocolVersion(), method(), uri(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers()); copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders()); copy.trailingHeaders().set(trailingHeaders());
return copy; return copy;
} }
@Override
public FullHttpRequest copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpRequest copy() {
return copy(true, null);
}
@Override @Override
public FullHttpRequest duplicate() { public FullHttpRequest duplicate() {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest( DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(
@ -340,15 +366,41 @@ public class HttpObjectAggregator
super(message, content, trailingHeaders); super(message, content, trailingHeaders);
} }
@Override /**
public FullHttpResponse copy() { * Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpResponse copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpResponse copy = new DefaultFullHttpResponse( DefaultFullHttpResponse copy = new DefaultFullHttpResponse(
protocolVersion(), status(), content().copy()); protocolVersion(), status(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers()); copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders()); copy.trailingHeaders().set(trailingHeaders());
return copy; return copy;
} }
@Override
public FullHttpResponse copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpResponse copy() {
return copy(true, null);
}
@Override @Override
public FullHttpResponse duplicate() { public FullHttpResponse duplicate() {
DefaultFullHttpResponse duplicate = new DefaultFullHttpResponse(protocolVersion(), status(), DefaultFullHttpResponse duplicate = new DefaultFullHttpResponse(protocolVersion(), status(),

View File

@ -540,6 +540,13 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
return reasonPhrase; return reasonPhrase;
} }
/**
* Determine if the status code is of the informational class (1xx)
*/
public boolean isInformational() {
return code >= 100 && code < 200;
}
@Override @Override
public int hashCode() { public int hashCode() {
return code(); return code();

View File

@ -16,6 +16,7 @@
package io.netty.handler.codec.http.multipart; package io.netty.handler.codec.http.multipart;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString; import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.DecoderResult;
@ -1203,15 +1204,41 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
return this; return this;
} }
@Override /**
public FullHttpRequest copy() { * Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpRequest copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpRequest copy = new DefaultFullHttpRequest( DefaultFullHttpRequest copy = new DefaultFullHttpRequest(
protocolVersion(), method(), uri(), content().copy()); protocolVersion(), method(), uri(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers()); copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders()); copy.trailingHeaders().set(trailingHeaders());
return copy; return copy;
} }
@Override
public FullHttpRequest copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpRequest copy() {
return copy(true, null);
}
@Override @Override
public FullHttpRequest duplicate() { public FullHttpRequest duplicate() {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest( DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(

View File

@ -128,6 +128,24 @@ public final class DefaultHttp2Headers extends Http2Headers {
return new HeaderIterator(); return new HeaderIterator();
} }
@Override
public String forEach(HeaderVisitor visitor) {
if (isEmpty()) {
return null;
}
HeaderEntry e = head.after;
do {
if (visitor.visit(e)) {
e = e.after;
} else {
return e.getKey();
}
} while (e != head);
return null;
}
/** /**
* Short cut for {@code new DefaultHttp2Headers.Builder()}. * Short cut for {@code new DefaultHttp2Headers.Builder()}.
*/ */

View File

@ -68,17 +68,17 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
} }
// Consume the Authority extension header if present // Consume the Authority extension header if present
value = httpHeaders.get(Http2HttpHeaders.Names.AUTHORITY); value = httpHeaders.get(Http2ToHttpHeaders.Names.AUTHORITY);
if (value != null) { if (value != null) {
http2Headers.authority(value); http2Headers.authority(value);
httpHeaders.remove(Http2HttpHeaders.Names.AUTHORITY); httpHeaders.remove(Http2ToHttpHeaders.Names.AUTHORITY);
} }
// Consume the Scheme extension header if present // Consume the Scheme extension header if present
value = httpHeaders.get(Http2HttpHeaders.Names.SCHEME); value = httpHeaders.get(Http2ToHttpHeaders.Names.SCHEME);
if (value != null) { if (value != null) {
http2Headers.scheme(value); http2Headers.scheme(value);
httpHeaders.remove(Http2HttpHeaders.Names.SCHEME); httpHeaders.remove(Http2ToHttpHeaders.Names.SCHEME);
} }
} }
@ -114,7 +114,7 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
*/ */
private int getStreamId(HttpHeaders httpHeaders) throws Http2Exception { private int getStreamId(HttpHeaders httpHeaders) throws Http2Exception {
int streamId = 0; int streamId = 0;
String value = httpHeaders.get(Http2HttpHeaders.Names.STREAM_ID); String value = httpHeaders.get(Http2ToHttpHeaders.Names.STREAM_ID);
if (value == null) { if (value == null) {
streamId = nextStreamId(); streamId = nextStreamId();
} else { } else {
@ -124,7 +124,7 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect
throw Http2Exception.format(Http2Error.INTERNAL_ERROR, throw Http2Exception.format(Http2Error.INTERNAL_ERROR,
"Invalid user-specified stream id value '%s'", value); "Invalid user-specified stream id value '%s'", value);
} }
httpHeaders.remove(Http2HttpHeaders.Names.STREAM_ID); httpHeaders.remove(Http2ToHttpHeaders.Names.STREAM_ID);
} }
return streamId; return streamId;

View File

@ -25,6 +25,17 @@ public interface Http2FrameObserver extends Http2DataObserver {
/** /**
* Handles an inbound HEADERS frame. * Handles an inbound HEADERS frame.
* <p>
* Only one of the following methods will be called for each HEADERS frame sequence.
* One will be called when the END_HEADERS flag has been received.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* To say it another way; the {@link Http2Headers} will contain all of the headers
* for the current message exchange step (additional queuing is not necessary).
* *
* @param ctx the context from the handler where the frame was read. * @param ctx the context from the handler where the frame was read.
* @param streamId the subject stream for the frame. * @param streamId the subject stream for the frame.
@ -37,7 +48,19 @@ public interface Http2FrameObserver extends Http2DataObserver {
boolean endStream) throws Http2Exception; boolean endStream) throws Http2Exception;
/** /**
* Handles an inbound HEADERS frame with priority information specified. * Handles an inbound HEADERS frame with priority information specified. Only called if END_HEADERS encountered.
*
* <p>
* Only one of the following methods will be called for each HEADERS frame sequence.
* One will be called when the END_HEADERS flag has been received.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* To say it another way; the {@link Http2Headers} will contain all of the headers
* for the current message exchange step (additional queuing is not necessary).
* *
* @param ctx the context from the handler where the frame was read. * @param ctx the context from the handler where the frame was read.
* @param streamId the subject stream for the frame. * @param streamId the subject stream for the frame.
@ -109,7 +132,19 @@ public interface Http2FrameObserver extends Http2DataObserver {
void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception; void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception;
/** /**
* Handles an inbound PUSH_PROMISE frame. * Handles an inbound PUSH_PROMISE frame. Only called if END_HEADERS encountered.
*
* <p>
* Only one of the following methods will be called for each HEADERS frame sequence.
* One will be called when the END_HEADERS flag has been received.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* To say it another way; the {@link Http2Headers} will contain all of the headers
* for the current message exchange step (additional queuing is not necessary).
* *
* @param ctx the context from the handler where the frame was read. * @param ctx the context from the handler where the frame was read.
* @param streamId the stream the frame was sent on. * @param streamId the stream the frame was sent on.

View File

@ -70,6 +70,11 @@ public abstract class Http2Headers implements Iterable<Entry<String, String>> {
public Iterator<Entry<String, String>> iterator() { public Iterator<Entry<String, String>> iterator() {
return entries().iterator(); return entries().iterator();
} }
@Override
public String forEach(HeaderVisitor visitor) {
return null;
}
}; };
/** /**
@ -180,6 +185,27 @@ public abstract class Http2Headers implements Iterable<Entry<String, String>> {
*/ */
public abstract int size(); public abstract int size();
/**
* Allows a means to reduce GC pressure while iterating over a collection
*/
public interface HeaderVisitor {
/**
* @return
* <ul>
* <li>{@code true} if the processor wants to continue the loop and handle the entry.</li>
* <li>{@code false} if the processor wants to stop handling headers and abort the loop.</li>
* </ul>
*/
boolean visit(Map.Entry<String, String> entry);
}
/**
* Iterates over the entries contained within this header object in no guaranteed order
* @return {@code null} if the visitor iterated to or beyond the end of the headers.
* The last-visited header name If the {@link HeaderVisitor#visit(Entry)} returned {@code false}.
*/
public abstract String forEach(HeaderVisitor visitor);
/** /**
* Gets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header * Gets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/ */

View File

@ -15,12 +15,14 @@
package io.netty.handler.codec.http2; package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString; import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.http.HttpResponseStatus;
/** /**
* Provides the constants for the header names used by * Provides the constants for the header names used by
* {@link InboundHttp2ToHttpAdapter} and {@link DelegatingHttp2HttpConnectionHandler} * {@link InboundHttp2ToHttpAdapter} and {@link DelegatingHttp2HttpConnectionHandler}
*/ */
public final class Http2HttpHeaders { public final class Http2ToHttpHeaders {
private Http2ToHttpHeaders() { }
public static final class Names { public static final class Names {
/** /**
@ -59,4 +61,27 @@ public final class Http2HttpHeaders {
private Names() { private Names() {
} }
} }
/**
* Apply HTTP/2 rules while translating status code to {@link HttpResponseStatus}
*
* @param status The status from an HTTP/2 frame
* @return The HTTP/1.x status
* @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x
*/
public static HttpResponseStatus parseStatus(String status) throws Http2Exception {
HttpResponseStatus result = null;
try {
result = HttpResponseStatus.parseLine(status);
if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
throw Http2Exception.protocolError("Invalid HTTP/2 status code '%d'", result.code());
}
} catch (Http2Exception e) {
throw e;
} catch (Exception e) {
throw Http2Exception.protocolError(
"Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
}
return result;
}
} }

View File

@ -12,17 +12,16 @@
* or implied. See the License for the specific language governing permissions and limitations under * or implied. See the License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package io.netty.handler.codec.http2; package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderUtil; import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
@ -30,13 +29,12 @@ import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http2.Http2Headers.HeaderVisitor;
import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap; import io.netty.util.collection.IntObjectMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
@ -46,10 +44,11 @@ import java.util.Set;
*/ */
public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements Http2FrameObserver { public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements Http2FrameObserver {
private final long maxContentLength; private final int maxContentLength;
private final boolean validateHttpHeaders; private final boolean validateHttpHeaders;
private final Http2Connection connection; private final Http2Connection connection;
private final IntObjectMap<Http2HttpMessageAccumulator> messageMap; private final ImmediateSendDetector sendDetector;
private final IntObjectMap<FullHttpMessage> messageMap;
private static final Set<String> HEADERS_TO_EXCLUDE; private static final Set<String> HEADERS_TO_EXCLUDE;
private static final Map<String, String> HEADER_NAME_TRANSLATIONS_REQUEST; private static final Map<String, String> HEADER_NAME_TRANSLATIONS_REQUEST;
@ -64,12 +63,12 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
} }
HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(), HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
Http2HttpHeaders.Names.AUTHORITY.toString()); Http2ToHttpHeaders.Names.AUTHORITY.toString());
HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.SCHEME.value(), HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.SCHEME.value(),
Http2HttpHeaders.Names.SCHEME.toString()); Http2ToHttpHeaders.Names.SCHEME.toString());
HEADER_NAME_TRANSLATIONS_REQUEST.putAll(HEADER_NAME_TRANSLATIONS_RESPONSE); HEADER_NAME_TRANSLATIONS_REQUEST.putAll(HEADER_NAME_TRANSLATIONS_RESPONSE);
HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.PATH.value(), HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.PATH.value(),
Http2HttpHeaders.Names.PATH.toString()); Http2ToHttpHeaders.Names.PATH.toString());
} }
/** /**
@ -81,9 +80,10 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
* @throws NullPointerException If {@code connection} is null * @throws NullPointerException If {@code connection} is null
* @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0} * @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0}
*/ */
public static InboundHttp2ToHttpAdapter newInstance(Http2Connection connection, long maxContentLength) public static InboundHttp2ToHttpAdapter newInstance(Http2Connection connection, int maxContentLength)
throws NullPointerException, IllegalArgumentException { throws NullPointerException, IllegalArgumentException {
InboundHttp2ToHttpAdapter adapter = new InboundHttp2ToHttpAdapter(connection, maxContentLength); InboundHttp2ToHttpAdapter adapter = new InboundHttp2ToHttpAdapter(connection, maxContentLength,
DefaultImmediateSendDetector.getInstance());
connection.addListener(adapter); connection.addListener(adapter);
return adapter; return adapter;
} }
@ -98,12 +98,10 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
* @throws NullPointerException If {@code connection} is null * @throws NullPointerException If {@code connection} is null
* @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0} * @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0}
*/ */
public static InboundHttp2ToHttpAdapter newInstance(Http2Connection connection, long maxContentLength, public static InboundHttp2ToHttpAdapter newInstance(Http2Connection connection, int maxContentLength,
boolean validateHttpHeaders) boolean validateHttpHeaders) throws NullPointerException, IllegalArgumentException {
throws NullPointerException, IllegalArgumentException { InboundHttp2ToHttpAdapter adapter = new InboundHttp2ToHttpAdapter(connection, maxContentLength,
InboundHttp2ToHttpAdapter adapter = new InboundHttp2ToHttpAdapter(connection, DefaultImmediateSendDetector.getInstance(), validateHttpHeaders);
maxContentLength,
validateHttpHeaders);
connection.addListener(adapter); connection.addListener(adapter);
return adapter; return adapter;
} }
@ -114,12 +112,14 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
* @param connection The object which will provide connection notification events for the current connection * @param connection The object which will provide connection notification events for the current connection
* @param maxContentLength the maximum length of the message content. If the length of the message content exceeds * @param maxContentLength the maximum length of the message content. If the length of the message content exceeds
* this value, a {@link TooLongFrameException} will be raised. * this value, a {@link TooLongFrameException} will be raised.
* @param sendDetector Decides when HTTP messages must be sent before the typically HTTP message events are detected
* @throws NullPointerException If {@code connection} is null * @throws NullPointerException If {@code connection} is null
* @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0} * @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0}
*/ */
protected InboundHttp2ToHttpAdapter(Http2Connection connection, long maxContentLength) throws NullPointerException, protected InboundHttp2ToHttpAdapter(Http2Connection connection, int maxContentLength,
ImmediateSendDetector sendDetector) throws NullPointerException,
IllegalArgumentException { IllegalArgumentException {
this(connection, maxContentLength, true); this(connection, maxContentLength, sendDetector, true);
} }
/** /**
@ -128,11 +128,13 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
* @param connection The object which will provide connection notification events for the current connection * @param connection The object which will provide connection notification events for the current connection
* @param maxContentLength the maximum length of the message content. If the length of the message content exceeds * @param maxContentLength the maximum length of the message content. If the length of the message content exceeds
* this value, a {@link TooLongFrameException} will be raised. * this value, a {@link TooLongFrameException} will be raised.
* @param sendDetector Decides when HTTP messages must be sent before the typically HTTP message events are detected
* @param validateHeaders {@code true} if http headers should be validated * @param validateHeaders {@code true} if http headers should be validated
* @throws NullPointerException If {@code connection} is null * @throws NullPointerException If {@code connection} is null
* @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0} * @throws IllegalArgumentException If {@code maxContentLength} is less than or equal to {@code 0}
*/ */
protected InboundHttp2ToHttpAdapter(Http2Connection connection, long maxContentLength, boolean validateHttpHeaders) protected InboundHttp2ToHttpAdapter(Http2Connection connection, int maxContentLength,
ImmediateSendDetector sendDetector, boolean validateHttpHeaders)
throws NullPointerException, IllegalArgumentException { throws NullPointerException, IllegalArgumentException {
if (connection == null) { if (connection == null) {
throw new NullPointerException("connection"); throw new NullPointerException("connection");
@ -140,170 +142,172 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
if (maxContentLength <= 0) { if (maxContentLength <= 0) {
throw new IllegalArgumentException("maxContentLength must be a positive integer: " + maxContentLength); throw new IllegalArgumentException("maxContentLength must be a positive integer: " + maxContentLength);
} }
if (sendDetector == null) {
throw new NullPointerException("sendDetector");
}
this.maxContentLength = maxContentLength; this.maxContentLength = maxContentLength;
this.validateHttpHeaders = validateHttpHeaders; this.validateHttpHeaders = validateHttpHeaders;
this.connection = connection; this.connection = connection;
this.messageMap = new IntObjectHashMap<Http2HttpMessageAccumulator>(); this.sendDetector = sendDetector;
this.messageMap = new IntObjectHashMap<FullHttpMessage>();
} }
@Override @Override
public void streamRemoved(Http2Stream stream) { public void streamRemoved(Http2Stream stream) {
removeMessage(stream.id()); messageMap.remove(stream.id());
}
@Override
public void onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception {
// Padding is already stripped out of data by super class
Http2HttpMessageAccumulator msgAccumulator = getMessage(streamId);
if (msgAccumulator == null) {
throw Http2Exception.protocolError("Data Frame recieved for unknown stream id %d", streamId);
}
try {
msgAccumulator.add(endOfStream ? new DefaultLastHttpContent(data, validateHttpHeaders)
: new DefaultHttpContent(data), ctx);
} catch (TooLongFrameException e) {
removeMessage(streamId);
throw Http2Exception.format(Http2Error.INTERNAL_ERROR,
"Content length exceeded max of %d for stream id %d", maxContentLength, streamId);
}
if (endOfStream) {
msgAccumulator.endOfStream(ctx);
removeMessage(streamId);
}
} }
/** /**
* Extracts the common initial header processing and internal tracking * Set final headers and fire a channel read event
* *
* @param ctx The context to fire the event on
* @param msg The message to send
* @param streamId the streamId of the message which is being fired
*/
private void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, int streamId) {
messageMap.remove(streamId);
HttpHeaderUtil.setContentLength(msg, msg.content().readableBytes());
ctx.fireChannelRead(msg);
}
/**
* Provides translation between HTTP/2 and HTTP header objects while ensuring the stream
* is in a valid state for additional headers.
*
* @param ctx The context for which this message has been received. Used to send informational header if detected.
* @param streamId The stream id the {@code headers} apply to * @param streamId The stream id the {@code headers} apply to
* @param headers The headers to process * @param headers The headers to process
* @param endOfStream {@code true} if the {@code streamId} has received the end of stream flag
* @param allowAppend {@code true} if headers will be appended if the stream already exists. if {@code false} and * @param allowAppend {@code true} if headers will be appended if the stream already exists. if {@code false} and
* the stream already exists this method returns {@code null}. * the stream already exists this method returns {@code null}.
* @param appendToTrailer {@code true} if a message {@code streamId} already exists then the headers
* should be added to the trailing headers.
* {@code false} then appends will be done to the initial headers.
* @return The object used to track the stream corresponding to {@code streamId}. {@code null} if * @return The object used to track the stream corresponding to {@code streamId}. {@code null} if
* {@code allowAppend} is {@code false} and the stream already exists. * {@code allowAppend} is {@code false} and the stream already exists.
* @throws Http2Exception If the stream id is not in the correct state to process the headers request * @throws Http2Exception If the stream id is not in the correct state to process the headers request
*/ */
private Http2HttpMessageAccumulator processHeadersBegin(int streamId, Http2Headers headers, boolean allowAppend) private FullHttpMessage processHeadersBegin(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
throws Http2Exception { boolean endOfStream, boolean allowAppend, boolean appendToTrailer) throws Http2Exception {
Http2HttpMessageAccumulator msgAccumulator = getMessage(streamId); FullHttpMessage msg = messageMap.get(streamId);
try { if (msg == null) {
if (msgAccumulator == null) { msg = connection.isServer() ? newHttpRequest(streamId, headers, validateHttpHeaders) :
msgAccumulator = connection.isServer() ? newHttpRequestAccumulator(headers) newHttpResponse(streamId, headers, validateHttpHeaders);
: newHttpResponseAccumulator(headers); } else if (allowAppend) {
} else if (allowAppend) { try {
if (msgAccumulator.headerConsumed()) { addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer);
if (msgAccumulator.trailerConsumed()) { } catch (Http2Exception e) {
throw new IllegalStateException("Header received and trailer already consumed"); messageMap.remove(streamId);
} else { throw e;
msgAccumulator.trailer(new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHttpHeaders));
}
}
msgAccumulator.add(headers);
} else {
return null;
} }
msgAccumulator.setHeader(Http2HttpHeaders.Names.STREAM_ID, streamId); } else {
} catch (IllegalStateException e) { msg = null;
removeMessage(streamId);
throw Http2Exception.protocolError("Headers Frame recieved for stream id %d which is in an invalid state",
streamId);
} catch (Http2Exception e) {
removeMessage(streamId);
throw e;
} }
return msgAccumulator; if (sendDetector.mustSendImmediately(msg)) {
fireChannelRead(ctx, msg, streamId);
return endOfStream ? null : sendDetector.copyIfNeeded(msg);
}
return msg;
} }
/** /**
* Extracts the common final header processing and internal tracking * After HTTP/2 headers have been processed by {@link #processHeadersBegin} this method either
* sends the result up the pipeline or retains the message for future processing.
* *
* @param ctx The context for which this message has been received * @param ctx The context for which this message has been received
* @param streamId The stream id the {@code msgAccumulator} corresponds to * @param streamId The stream id the {@code objAccumulator} corresponds to
* @param msgAccumulator The object which represents all data for corresponding to {@code streamId} * @param msg The object which represents all headers/data for corresponding to {@code streamId}
* @param endOfStream {@code true} if this is the last event for the stream * @param endOfStream {@code true} if this is the last event for the stream
*/ */
private void processHeadersEnd(ChannelHandlerContext ctx, int streamId, private void processHeadersEnd(ChannelHandlerContext ctx, int streamId, FullHttpMessage msg, boolean endOfStream) {
Http2HttpMessageAccumulator msgAccumulator, boolean endOfStream) {
if (endOfStream) { if (endOfStream) {
msgAccumulator.endOfStream(ctx); fireChannelRead(ctx, msg, streamId);
removeMessage(streamId);
} else { } else {
putMessage(streamId, msgAccumulator); messageMap.put(streamId, msg);
}
}
@Override
public void onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
throws Http2Exception {
FullHttpMessage msg = messageMap.get(streamId);
if (msg == null) {
throw Http2Exception.protocolError("Data Frame recieved for unknown stream id %d", streamId);
}
ByteBuf content = msg.content();
if (content.readableBytes() > maxContentLength - data.readableBytes()) {
throw Http2Exception.format(Http2Error.INTERNAL_ERROR,
"Content length exceeded max of %d for stream id %d", maxContentLength, streamId);
}
// TODO: provide hooks to a HttpContentDecoder type interface
// Preferably provide these hooks in the HTTP2 codec so even non-translation layer use-cases benefit
// (and then data will already be decoded here)
content.writeBytes(data, data.readerIndex(), data.readableBytes());
if (endOfStream) {
fireChannelRead(ctx, msg, streamId);
} }
} }
@Override @Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endOfStream) throws Http2Exception { boolean endOfStream) throws Http2Exception {
Http2HttpMessageAccumulator msgAccumulator = processHeadersBegin(streamId, headers, true); FullHttpMessage msg = processHeadersBegin(ctx, streamId, headers, endOfStream, true, true);
processHeadersEnd(ctx, streamId, msgAccumulator, endOfStream); if (msg != null) {
} processHeadersEnd(ctx, streamId, msg, endOfStream);
/**
* Set/clear all HTTP/1.x headers related to stream dependencies
*
* @param msgAccumulator The object which caches the HTTP/1.x headers
* @param streamDependency The stream id for which the {@code msgAccumulator} is dependent on
* @param weight The dependency weight
* @param exclusive The exlusive HTTP/2 flag
* @throws IllegalStateException If the {@code msgAccumulator} is not in a valid state to change headers
*/
private void setDependencyHeaders(Http2HttpMessageAccumulator msgAccumulator, int streamDependency, short weight,
boolean exclusive) throws IllegalStateException {
if (streamDependency != 0) {
msgAccumulator.setHeader(Http2HttpHeaders.Names.STREAM_DEPENDENCY_ID, streamDependency);
msgAccumulator.setHeader(Http2HttpHeaders.Names.STREAM_EXCLUSIVE, exclusive);
msgAccumulator.setHeader(Http2HttpHeaders.Names.STREAM_WEIGHT, weight);
} else {
msgAccumulator.removeHeader(Http2HttpHeaders.Names.STREAM_DEPENDENCY_ID);
msgAccumulator.removeHeader(Http2HttpHeaders.Names.STREAM_EXCLUSIVE);
msgAccumulator.removeHeader(Http2HttpHeaders.Names.STREAM_WEIGHT);
} }
} }
@Override @Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
Http2HttpMessageAccumulator msgAccumulator = processHeadersBegin(streamId, headers, true); FullHttpMessage msg = processHeadersBegin(ctx, streamId, headers, endOfStream, true, true);
try { if (msg != null) {
setDependencyHeaders(msgAccumulator, streamDependency, weight, exclusive); // TODO: fix me...consume priority tree listener interface
} catch (IllegalStateException e) { setDependencyHeaders(msg.headers(), streamDependency, weight, exclusive);
removeMessage(streamId); processHeadersEnd(ctx, streamId, msg, endOfStream);
throw Http2Exception.protocolError("Headers Frame recieved for stream id %d which is in an invalid state",
streamId);
} }
processHeadersEnd(ctx, streamId, msgAccumulator, endOfStream);
} }
@Override @Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight, public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception { boolean exclusive) throws Http2Exception {
Http2HttpMessageAccumulator msgAccumulator = getMessage(streamId); FullHttpMessage msg = messageMap.get(streamId);
if (msgAccumulator == null) { if (msg == null) {
throw Http2Exception.protocolError("Priority Frame recieved for unknown stream id %d", streamId); throw Http2Exception.protocolError("Priority Frame recieved for unknown stream id %d", streamId);
} }
try { // TODO: fix me...consume priority tree listener interface
setDependencyHeaders(msgAccumulator, streamDependency, weight, exclusive); setDependencyHeaders(msg.headers(), streamDependency, weight, exclusive);
} catch (IllegalStateException e) {
removeMessage(streamId);
throw Http2Exception.protocolError("Priority Frame recieved for stream id %d which is in an invalid state",
streamId);
}
} }
@Override @Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
Http2HttpMessageAccumulator msgAccumulator = getMessage(streamId); FullHttpMessage msg = messageMap.get(streamId);
if (msgAccumulator != null) { if (msg != null) {
removeMessage(streamId); fireChannelRead(ctx, msg, streamId);
} }
} }
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers,
int padding) throws Http2Exception {
// A push promise should not be allowed to add headers to an existing stream
FullHttpMessage msg = processHeadersBegin(ctx, promisedStreamId, headers, false, false, false);
if (msg == null) {
throw Http2Exception.protocolError("Push Promise Frame recieved for pre-existing stream id %d",
promisedStreamId);
}
msg.headers().set(Http2ToHttpHeaders.Names.STREAM_PROMISE_ID, streamId);
processHeadersEnd(ctx, promisedStreamId, msg, false);
}
@Override @Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception { public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
// NOOP // NOOP
@ -324,30 +328,6 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
// NOOP // NOOP
} }
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers,
int padding) throws Http2Exception {
// Do not allow adding of headers to existing Http2HttpMessageAccumulator
// according to spec (http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-6.6) there must
// be a CONTINUATION frame for more headers
Http2HttpMessageAccumulator msgAccumulator = processHeadersBegin(promisedStreamId, headers, false);
if (msgAccumulator == null) {
throw Http2Exception.protocolError("Push Promise Frame recieved for pre-existing stream id %d",
promisedStreamId);
}
try {
msgAccumulator.setHeader(Http2HttpHeaders.Names.STREAM_PROMISE_ID, streamId);
} catch (IllegalStateException e) {
removeMessage(streamId);
throw Http2Exception.protocolError(
"Push Promise Frame recieved for stream id %d which is in an invalid state",
promisedStreamId);
}
processHeadersEnd(ctx, promisedStreamId, msgAccumulator, false);
}
@Override @Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception { throws Http2Exception {
@ -366,278 +346,226 @@ public class InboundHttp2ToHttpAdapter extends Http2ConnectionAdapter implements
// NOOP // NOOP
} }
private Http2HttpMessageAccumulator putMessage(int streamId, Http2HttpMessageAccumulator message) { /**
return messageMap.put(streamId, message); * Allows messages to be sent up the pipeline before the next phase in the
} * HTTP message flow is detected.
*/
private interface ImmediateSendDetector {
/**
* Determine if the response should be sent immediately, or wait for the end of the stream
*
* @param msg The response to test
* @return {@code true} if the message should be sent immediately
* {@code false) if we should wait for the end of the stream
*/
boolean mustSendImmediately(FullHttpMessage msg);
private Http2HttpMessageAccumulator getMessage(int streamId) { /**
return messageMap.get(streamId); * Determine if a copy must be made after an immediate send happens.
} * <p>
* An example of this use case is if a request is received
private Http2HttpMessageAccumulator removeMessage(int streamId) { * with a 'Expect: 100-continue' header. The message will be sent immediately,
return messageMap.remove(streamId); * and the data will be queued and sent at the end of the stream.
*
* @param msg The message which has just been sent due to {@link #mustSendImmediatley}
* @return A modified copy of the {@code msg} or {@code null} if a copy is not needed.
*/
FullHttpMessage copyIfNeeded(FullHttpMessage msg);
} }
/** /**
* Translate HTTP/2 headers into an object which can build the corresponding HTTP/1.x response objects * Default implementation of {@link ImmediateSendDetector}
*
* @param http2Headers The HTTP/2 headers corresponding to a new stream
* @return Collector for HTTP/1.x objects
* @throws Http2Exception If any of the HTTP/2 headers to not translate to valid HTTP/1.x headers
* @throws IllegalStateException If this object is not in the correct state to accept additional headers
*/ */
private Http2HttpMessageAccumulator newHttpResponseAccumulator(Http2Headers http2Headers) throws Http2Exception, private static final class DefaultImmediateSendDetector implements ImmediateSendDetector {
IllegalStateException { private static DefaultImmediateSendDetector instance;
HttpResponseStatus status = null;
try {
status = HttpResponseStatus.parseLine(http2Headers.status());
} catch (Exception e) {
throw Http2Exception.protocolError(
"Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x",
http2Headers.status());
}
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an HTTP/1.1
// status line.
Http2HttpMessageAccumulator messageAccumulator = new Http2HttpMessageAccumulator(new DefaultHttpResponse(
HttpVersion.HTTP_1_1, status, validateHttpHeaders));
messageAccumulator.add(http2Headers);
return messageAccumulator;
}
/** private DefaultImmediateSendDetector() {
* Translate HTTP/2 headers into an object which can build the corresponding HTTP/1.x request objects
*
* @param http2Headers The HTTP/2 headers corresponding to a new stream
* @return Collector for HTTP/1.x objects
* @throws Http2Exception If any of the HTTP/2 headers to not translate to valid HTTP/1.x headers
* @throws IllegalStateException If this object is not in the correct state to accept additional headers
*/
private Http2HttpMessageAccumulator newHttpRequestAccumulator(Http2Headers http2Headers) throws Http2Exception,
IllegalStateException {
// HTTP/2 does not define a way to carry the version identifier that is
// included in the HTTP/1.1 request line.
Http2HttpMessageAccumulator messageAccumulator = new Http2HttpMessageAccumulator(new DefaultHttpRequest(
HttpVersion.HTTP_1_1, HttpMethod.valueOf(http2Headers.method()), http2Headers.path(),
validateHttpHeaders));
messageAccumulator.add(http2Headers);
return messageAccumulator;
}
/**
* Provides a container to collect HTTP/1.x objects until the end of the stream has been reached
*/
private final class Http2HttpMessageAccumulator {
private HttpMessage message;
private LastHttpContent trailer;
private long contentLength;
/**
* Creates a new instance
*
* @param message The HTTP/1.x object which represents the headers
*/
public Http2HttpMessageAccumulator(HttpMessage message) {
if (message == null) {
throw new NullPointerException("message");
}
this.message = message;
this.trailer = null;
this.contentLength = 0;
} }
/** public static DefaultImmediateSendDetector getInstance() {
* Set a HTTP/1.x header if (instance == null) {
* instance = new DefaultImmediateSendDetector();
* @param name The name of the header
* @param value The value of the header
* @return The headers object after the set operation
* @throws IllegalStateException If this object is not in the correct state to accept additional headers
*/
public HttpHeaders setHeader(CharSequence name, Object value) throws IllegalStateException {
HttpHeaders headers = currentHeaders();
if (headers == null) {
throw new IllegalStateException("Headers object is null");
} }
return headers.set(name, value); return instance;
} }
/** @Override
* Removes the header with the specified name. public boolean mustSendImmediately(FullHttpMessage msg) {
* if (msg instanceof FullHttpResponse) {
* @param name The name of the header to remove return ((FullHttpResponse) msg).status().isInformational();
* @return {@code true} if and only if at least one entry has been removed } else if (msg instanceof FullHttpRequest) {
* @throws IllegalStateException If this object is not in the correct state to accept additional headers return ((FullHttpRequest) msg).headers().contains(HttpHeaders.Names.EXPECT);
*/
public boolean removeHeader(CharSequence name) throws IllegalStateException {
HttpHeaders headers = currentHeaders();
if (headers == null) {
throw new IllegalStateException("Headers object is null");
}
return headers.remove(name);
}
/**
* Send the headers that have been accumulated so far, if they have not already been sent
*
* @param ctx The channel context for which to propagate events
* @param setContentLength {@code true} to set the Content-Length header
* @return {@code true} If a non-trailer header was fired to {@code ctx}, {@code false} otherwise
*/
private boolean sendHeaders(ChannelHandlerContext ctx, boolean setContentLength) {
HttpHeaders headers = currentHeaders();
if (headers == null) {
return false;
}
// Transfer-Encoding header is not valid
headers.remove(HttpHeaders.Names.TRANSFER_ENCODING);
headers.remove(HttpHeaders.Names.TRAILER);
// Initial header and trailer header will never need to be sent at the same time
// the behavior is that all header events will be appended to the initial header
// until some data is received, and only then will the trailer be used
if (!headerConsumed()) {
// The Connection and Keep-Alive headers are no longer valid
HttpHeaderUtil.setKeepAlive(message, true);
if (setContentLength) {
HttpHeaderUtil.setContentLength(message, contentLength);
}
ctx.fireChannelRead(message);
message = null;
return true;
} else if (trailerExists()) {
ctx.fireChannelRead(trailer);
trailer = LastHttpContent.EMPTY_LAST_CONTENT;
} }
return false; return false;
} }
/** @Override
* Called when the end of the stream is encountered public FullHttpMessage copyIfNeeded(FullHttpMessage msg) {
* if (msg instanceof FullHttpRequest) {
* @param ctx The channel context for which to propagate events FullHttpRequest copy = ((FullHttpRequest) msg).copy(null);
*/ copy.headers().remove(HttpHeaders.Names.EXPECT);
public void endOfStream(ChannelHandlerContext ctx) { return copy;
if (sendHeaders(ctx, true)) {
ctx.fireChannelRead(LastHttpContent.EMPTY_LAST_CONTENT);
} }
return null;
}
}
/**
* Set/clear all HTTP/1.x headers related to stream dependencies
*
* @param headers The headers to set
* @param streamDependency The stream id for which the {@code objAccumulator} is dependent on
* @param weight The dependency weight
* @param exclusive The exclusive HTTP/2 flag
*/
private static void setDependencyHeaders(HttpHeaders headers, int streamDependency,
short weight, boolean exclusive) {
if (streamDependency != 0) {
headers.set(Http2ToHttpHeaders.Names.STREAM_DEPENDENCY_ID, streamDependency);
headers.set(Http2ToHttpHeaders.Names.STREAM_EXCLUSIVE, exclusive);
headers.set(Http2ToHttpHeaders.Names.STREAM_WEIGHT, weight);
} else {
headers.set(Http2ToHttpHeaders.Names.STREAM_DEPENDENCY_ID);
headers.set(Http2ToHttpHeaders.Names.STREAM_EXCLUSIVE);
headers.set(Http2ToHttpHeaders.Names.STREAM_WEIGHT);
}
}
/**
* Create a new object to contain the response data
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param validateHttpHeaders Used by http-codec to validate headers
* @return A new response object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
private static FullHttpMessage newHttpResponse(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
HttpResponseStatus status = Http2ToHttpHeaders.parseStatus(http2Headers.status());
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an HTTP/1.1
// status line.
FullHttpMessage msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false, HEADER_NAME_TRANSLATIONS_RESPONSE);
return msg;
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param validateHttpHeaders Used by http-codec to validate headers
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
private static FullHttpMessage newHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is
// included in the HTTP/1.1 request line.
FullHttpMessage msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.valueOf(http2Headers.method()), http2Headers.path(), validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false, HEADER_NAME_TRANSLATIONS_REQUEST);
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers
*
* @param streamId The stream associated with {@code sourceHeaders}
* @param sourceHeaders The HTTP/2 headers to convert
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)}
*/
private static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
addHttp2ToHttpHeaders(streamId, sourceHeaders, destinationMessage, addToTrailer,
(destinationMessage instanceof FullHttpRequest) ? HEADER_NAME_TRANSLATIONS_REQUEST
: HEADER_NAME_TRANSLATIONS_RESPONSE);
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers
*
* @param streamId The stream associated with {@code sourceHeaders}
* @param sourceHeaders The HTTP/2 headers to convert
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
* @param translations A map used to help translate HTTP/2 headers to HTTP/1.x headers
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x
*/
private static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer, Map<String, String> translations)
throws Http2Exception {
HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers();
HttpForEachVisitor visitor = new HttpForEachVisitor(headers, translations);
sourceHeaders.forEach(visitor);
if (visitor.exception() != null) {
throw visitor.exception();
} }
/** headers.remove(HttpHeaders.Names.TRANSFER_ENCODING);
* Add a HTTP/1.x object which represents part of the message body headers.remove(HttpHeaders.Names.TRAILER);
* if (!addToTrailer) {
* @param httpContent The content to add headers.set(Http2ToHttpHeaders.Names.STREAM_ID, streamId);
* @param ctx The channel context for which to propagate events HttpHeaderUtil.setKeepAlive(destinationMessage, true);
* @throws TooLongFrameException If the {@code contentLength} is exceeded with the addition of the }
* {@code httpContent} }
*/
public void add(HttpContent httpContent, ChannelHandlerContext ctx) throws TooLongFrameException {
ByteBuf content = httpContent.content();
if (contentLength > maxContentLength - content.readableBytes()) {
throw new TooLongFrameException("HTTP/2 content length exceeded " + maxContentLength + " bytes.");
}
contentLength += content.readableBytes(); /**
sendHeaders(ctx, false); * A visitor which translates HTTP/2 headers to HTTP/1 headers
ctx.fireChannelRead(httpContent.retain()); */
private static final class HttpForEachVisitor implements HeaderVisitor {
private Map<String, String> translations;
private HttpHeaders headers;
private Http2Exception e;
/**
* Create a new instance
*
* @param headers The HTTP/1.x headers object to store the results of the translation
* @param translations A map used to help translate HTTP/2 headers to HTTP/1.x headers
*/
public HttpForEachVisitor(HttpHeaders headers, Map<String, String> translations) {
this.translations = translations;
this.headers = headers;
this.e = null;
} }
/** @Override
* Extend the current set of HTTP/1.x headers public boolean visit(Entry<String, String> entry) {
* String translatedName = translations.get(entry.getKey());
* @param http2Headers The HTTP/2 headers to be added if (translatedName != null || !HEADERS_TO_EXCLUDE.contains(entry.getKey())) {
* @throws Http2Exception If any HTTP/2 headers do not map to HTTP/1.x headers if (translatedName == null) {
* @throws IllegalStateException If this object is not in the correct state to accept additional headers translatedName = entry.getKey();
*/ }
public void add(Http2Headers http2Headers) throws Http2Exception, IllegalStateException {
add(http2Headers, InboundHttp2ToHttpAdapter.this.connection.isServer() ? HEADER_NAME_TRANSLATIONS_REQUEST
: HEADER_NAME_TRANSLATIONS_RESPONSE);
}
/** // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.2.3
* Extend the current set of HTTP/1.x headers // All headers that start with ':' are only valid in HTTP/2 context
* if (translatedName.isEmpty() || translatedName.charAt(0) == ':') {
* @param http2Headers The HTTP/2 headers to be added e = Http2Exception
* @param headerTranslations Translation map from HTTP/2 headers to HTTP/1.x headers .protocolError("Unknown HTTP/2 header '%s' encountered in translation to HTTP/1.x",
* @throws Http2Exception If any HTTP/2 headers do not map to HTTP/1.x headers translatedName);
* @throws IllegalStateException If this object is not in the correct state to accept additional headers return false;
*/ } else {
private void add(Http2Headers http2Headers, Map<String, String> headerTranslations) throws Http2Exception, headers.add(translatedName, entry.getValue());
IllegalStateException {
HttpHeaders headers = currentHeaders();
if (headers == null) {
throw new IllegalStateException("Headers object is null");
}
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
Iterator<Entry<String, String>> itr = http2Headers.iterator();
while (itr.hasNext()) {
Entry<String, String> entry = itr.next();
String translatedName = headerTranslations.get(entry.getKey());
if (translatedName != null || !HEADERS_TO_EXCLUDE.contains(entry.getKey())) {
if (translatedName == null) {
translatedName = entry.getKey();
}
if (translatedName.isEmpty() || translatedName.charAt(0) == ':') {
throw Http2Exception.protocolError(
"Unknown HTTP/2 header '%s' encountered in translation to HTTP/1.x",
translatedName);
} else {
headers.add(translatedName, entry.getValue());
}
} }
} }
return true;
} }
/** /**
* Set the current trailer header * Get any exceptions encountered while translating HTTP/2 headers to HTTP/1.x headers
* *
* @param trailer The object which represents the trailing headers * @return
* <ul>
* <li>{@code null} if no exceptions where encountered</li>
* <li>Otherwise an exception describing what went wrong</li>
* </ul>
*/ */
public void trailer(LastHttpContent trailer) { public Http2Exception exception() {
this.trailer = trailer; return e;
}
/**
* Determine if the initial header and continuations have been processed and sent up the pipeline
*
* @return {@code true} if the initial header and continuations have been processed and sent up the pipeline
*/
public boolean headerConsumed() {
return message == null;
}
/**
* Determine if the trailing header and continuations have been processed and sent up the pipeline
*
* @return {@code true} if the trailing header and continuations have been processed and sent up the pipeline
*/
public boolean trailerConsumed() {
return trailer != null && trailer.equals(LastHttpContent.EMPTY_LAST_CONTENT);
}
/**
* Determine if there is a trailing header that has not yet been sent up the pipeline
*
* @return {@code true} if there is a trailing header that has not yet been sent up the pipeline
*/
public boolean trailerExists() {
return trailer != null && !trailerConsumed();
}
/**
* Obtain the current object for accumulating headers
*
* @return The primary (non-trailer) header if it exists, otherwise the trailer header
*/
private HttpHeaders currentHeaders() {
if (headerConsumed()) {
return trailerExists() ? trailer.trailingHeaders() : null;
}
return message.headers();
} }
} }
} }

View File

@ -124,10 +124,10 @@ public class DelegatingHttp2HttpConnectionHandlerTest {
public void testJustHeadersRequest() throws Exception { public void testJustHeadersRequest() throws Exception {
final HttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/example"); final HttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/example");
final HttpHeaders httpHeaders = request.headers(); final HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 5); httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 5);
httpHeaders.set(HttpHeaders.Names.HOST, "http://my-user_name@www.example.org:5555/example"); httpHeaders.set(HttpHeaders.Names.HOST, "http://my-user_name@www.example.org:5555/example");
httpHeaders.set(Http2HttpHeaders.Names.AUTHORITY, "www.example.org:5555"); httpHeaders.set(Http2ToHttpHeaders.Names.AUTHORITY, "www.example.org:5555");
httpHeaders.set(Http2HttpHeaders.Names.SCHEME, "http"); httpHeaders.set(Http2ToHttpHeaders.Names.SCHEME, "http");
httpHeaders.add("foo", "goo"); httpHeaders.add("foo", "goo");
httpHeaders.add("foo", "goo2"); httpHeaders.add("foo", "goo2");
httpHeaders.add("foo2", "goo2"); httpHeaders.add("foo2", "goo2");

View File

@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.reset;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@ -36,16 +37,17 @@ import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable; import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.NetUtil; import io.netty.util.NetUtil;
@ -66,25 +68,33 @@ import org.mockito.MockitoAnnotations;
public class InboundHttp2ToHttpAdapterTest { public class InboundHttp2ToHttpAdapterTest {
@Mock @Mock
private HttpResponseListener messageObserver; private HttpResponseListener serverObserver;
@Mock @Mock
private Http2FrameObserver clientObserver; private HttpResponseListener clientObserver;
private Http2FrameWriter frameWriter; private Http2FrameWriter frameWriter;
private ServerBootstrap sb; private ServerBootstrap sb;
private Bootstrap cb; private Bootstrap cb;
private Channel serverChannel; private Channel serverChannel;
private Channel serverConnectedChannel;
private Channel clientChannel; private Channel clientChannel;
private CountDownLatch serverLatch; private CountDownLatch serverLatch;
private long maxContentLength; private CountDownLatch clientLatch;
private int maxContentLength;
private HttpResponseDelegator serverDelegator;
private HttpResponseDelegator clientDelegator;
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
maxContentLength = 1 << 16; clientDelegator = null;
serverLatch = new CountDownLatch(1); serverDelegator = null;
serverConnectedChannel = null;
maxContentLength = 1024;
setServerLatch(1);
setClientLatch(1);
frameWriter = new DefaultHttp2FrameWriter(); frameWriter = new DefaultHttp2FrameWriter();
sb = new ServerBootstrap(); sb = new ServerBootstrap();
@ -97,10 +107,12 @@ public class InboundHttp2ToHttpAdapterTest {
protected void initChannel(Channel ch) throws Exception { protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline(); ChannelPipeline p = ch.pipeline();
Http2Connection connection = new DefaultHttp2Connection(true); Http2Connection connection = new DefaultHttp2Connection(true);
p.addLast("reader", new FrameAdapter(InboundHttp2ToHttpAdapter.newInstance( p.addLast("reader",
connection, maxContentLength), new FrameAdapter(InboundHttp2ToHttpAdapter.newInstance(connection, maxContentLength),
new CountDownLatch(10))); new CountDownLatch(10)));
p.addLast(new HttpResponseDelegator(messageObserver)); serverDelegator = new HttpResponseDelegator(serverObserver, serverLatch);
p.addLast(serverDelegator);
serverConnectedChannel = ch;
} }
}); });
@ -110,7 +122,12 @@ public class InboundHttp2ToHttpAdapterTest {
@Override @Override
protected void initChannel(Channel ch) throws Exception { protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline(); ChannelPipeline p = ch.pipeline();
p.addLast("reader", new FrameAdapter(clientObserver, new CountDownLatch(10))); Http2Connection connection = new DefaultHttp2Connection(false);
p.addLast("reader",
new FrameAdapter(InboundHttp2ToHttpAdapter.newInstance(connection, maxContentLength),
new CountDownLatch(10)));
clientDelegator = new HttpResponseDelegator(clientObserver, clientLatch);
p.addLast(clientDelegator);
} }
}); });
@ -127,315 +144,393 @@ public class InboundHttp2ToHttpAdapterTest {
serverChannel.close().sync(); serverChannel.close().sync();
sb.group().shutdownGracefully(); sb.group().shutdownGracefully();
cb.group().shutdownGracefully(); cb.group().shutdownGracefully();
clientDelegator = null;
serverDelegator = null;
clientChannel = null;
serverChannel = null;
serverConnectedChannel = null;
} }
@Test @Test
public void clientRequestSingleHeaderNoDataFrames() throws Exception { public void clientRequestSingleHeaderNoDataFrames() throws Exception {
serverLatch = new CountDownLatch(2); final HttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, "/some/path/resource2", true);
HttpMethod.GET, "/some/path/resource2", true);
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.SCHEME, "https"); httpHeaders.set(Http2ToHttpHeaders.Names.SCHEME, "https");
httpHeaders.set(Http2HttpHeaders.Names.AUTHORITY, "example.org"); httpHeaders.set(Http2ToHttpHeaders.Names.AUTHORITY, "example.org");
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3); httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder() final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https")
.method("GET").scheme("https").authority("example.org") .authority("example.org").path("/some/path/resource2").build();
.path("/some/path/resource2").build();
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, true, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
ctx().flush(); ctxClient().flush();
} }
}); });
awaitRequests(); awaitRequests();
verify(messageObserver).messageReceived(eq(request)); verify(serverObserver).messageReceived(eq(request));
verify(messageObserver).messageReceived(eq(LastHttpContent.EMPTY_LAST_CONTENT));
} }
@Test @Test
public void clientRequestOneDataFrame() throws Exception { public void clientRequestOneDataFrame() throws Exception {
serverLatch = new CountDownLatch(2);
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, "/some/path/resource2", true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder()
.method("GET").path("/some/path/resource2").build();
final String text = "hello world"; final String text = "hello world";
final HttpContent content = new DefaultLastHttpContent(Unpooled.copiedBuffer(text.getBytes()), final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
true); final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path/resource2")
.build();
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctx(), 3, frameWriter.writeData(ctxClient(), 3, Unpooled.copiedBuffer(text.getBytes()), 0, true,
Unpooled.copiedBuffer(text.getBytes()), 0, true, newPromise()); newPromiseClient());
ctx().flush(); ctxClient().flush();
} }
}); });
awaitRequests(); awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class); verify(serverObserver).messageReceived(eq(request));
verify(messageObserver, times(2)).messageReceived(httpObjectCaptor.capture()); request.release();
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1));
} }
@Test @Test
public void clientRequestMultipleDataFrames() throws Exception { public void clientRequestMultipleDataFrames() throws Exception {
serverLatch = new CountDownLatch(3); final String text = "hello world big time data!";
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
HttpMethod.GET, "/some/path/resource2", true); final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3); httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder() httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
.method("GET").path("/some/path/resource2").build(); final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path/resource2")
final String text = "hello world"; .build();
final String text2 = "hello world2"; final int midPoint = text.length() / 2;
final HttpContent content = new DefaultHttpContent(Unpooled.copiedBuffer(text.getBytes()));
final HttpContent content2 = new DefaultLastHttpContent(Unpooled.copiedBuffer(text2.getBytes()),
true);
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctx(), 3, frameWriter.writeData(ctxClient(), 3, content.slice(0, midPoint).retain(), 0,
Unpooled.copiedBuffer(text.getBytes()), 0, false, newPromise()); false, newPromiseClient());
frameWriter.writeData(ctx(), 3, frameWriter.writeData(ctxClient(), 3, content.slice(midPoint, text.length() - midPoint).retain(), 0,
Unpooled.copiedBuffer(text2.getBytes()), 0, true, newPromise()); true, newPromiseClient());
ctx().flush(); ctxClient().flush();
} }
}); });
awaitRequests(); awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class); verify(serverObserver).messageReceived(eq(request));
verify(messageObserver, times(3)).messageReceived(httpObjectCaptor.capture()); request.release();
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1));
assertEquals(content2, capturedHttpObjects.get(2));
} }
@Test @Test
public void clientRequestMultipleEmptyDataFrames() throws Exception { public void clientRequestMultipleEmptyDataFrames() throws Exception {
serverLatch = new CountDownLatch(4);
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, "/some/path/resource2", true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder()
.method("GET").path("/some/path/resource2").build();
final String text = ""; final String text = "";
final HttpContent content = new DefaultHttpContent(Unpooled.copiedBuffer(text.getBytes())); final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final HttpContent content2 = new DefaultLastHttpContent(Unpooled.copiedBuffer(text.getBytes()), final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
true); "/some/path/resource2", content, true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path/resource2")
.build();
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctx(), 3, Unpooled.copiedBuffer(text.getBytes()), 0, false, frameWriter.writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
newPromise()); frameWriter.writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
frameWriter.writeData(ctx(), 3, Unpooled.copiedBuffer(text.getBytes()), 0, false, frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
newPromise()); ctxClient().flush();
frameWriter.writeData(ctx(), 3, Unpooled.copiedBuffer(text.getBytes()), 0, true, newPromise());
ctx().flush();
} }
}); });
awaitRequests(); awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class); verify(serverObserver).messageReceived(eq(request));
verify(messageObserver, times(4)).messageReceived(httpObjectCaptor.capture()); request.release();
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1));
assertEquals(content, capturedHttpObjects.get(2));
assertEquals(content2, capturedHttpObjects.get(3));
} }
@Test @Test
public void clientRequestHeaderContinuation() throws Exception { public void clientRequestMultipleHeaders() throws Exception {
serverLatch = new CountDownLatch(2); // writeHeaders will implicitly add an END_HEADERS tag each time and so this test does not follow the HTTP
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, // message flow. We currently accept this message flow and just add the second headers to the trailing headers.
HttpMethod.GET, "/some/path/resource2", true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set("foo", "goo");
httpHeaders.set("foo2", "goo2");
httpHeaders.add("foo2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder()
.method("GET").path("/some/path/resource2").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo")
.set("foo2", "goo2").add("foo2", "goo3").build();
final String text = ""; final String text = "";
final HttpContent content = new DefaultLastHttpContent(Unpooled.copiedBuffer(text.getBytes()), final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
true); final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set("FoO", "goo");
trailingHeaders.set("foO2", "goo2");
trailingHeaders.add("fOo2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path/resource2")
.build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo").set("foo2", "goo2")
.add("foo2", "goo3").build();
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeHeaders(ctx(), 3, http2Headers2, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers2, 0, false, newPromiseClient());
frameWriter.writeData(ctx(), 3, frameWriter.writeData(ctxClient(), 3, Unpooled.copiedBuffer(text.getBytes()), 0, true,
Unpooled.copiedBuffer(text.getBytes()), 0, true, newPromise()); newPromiseClient());
ctx().flush(); ctxClient().flush();
} }
}); });
awaitRequests(); awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class); verify(serverObserver).messageReceived(eq(request));
verify(messageObserver, times(2)).messageReceived(httpObjectCaptor.capture()); request.release();
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1));
} }
@Test @Test
public void clientRequestTrailingHeaders() throws Exception { public void clientRequestTrailingHeaders() throws Exception {
serverLatch = new CountDownLatch(3); final String text = "some data";
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
HttpMethod.GET, "/some/path/resource2", true); final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3); httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
final LastHttpContent trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, true); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpHeaders trailingHeaders = trailer.trailingHeaders(); HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set("foo", "goo"); trailingHeaders.set("Foo", "goo");
trailingHeaders.set("foo2", "goo2"); trailingHeaders.set("fOo2", "goo2");
trailingHeaders.add("foo2", "goo3"); trailingHeaders.add("foO2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder() final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path/resource2")
.method("GET").path("/some/path/resource2").build(); .build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo") final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo").set("foo2", "goo2")
.set("foo2", "goo2").add("foo2", "goo3").build(); .add("foo2", "goo3").build();
final String text = "not empty!";
final HttpContent content = new DefaultLastHttpContent(Unpooled.copiedBuffer(text.getBytes()),
true);
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctx(), 3, frameWriter.writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
Unpooled.copiedBuffer(text.getBytes()), 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers2, 0, true, newPromiseClient());
frameWriter.writeHeaders(ctx(), 3, http2Headers2, 0, true, newPromise()); ctxClient().flush();
ctx().flush();
} }
}); });
awaitRequests(); awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class); verify(serverObserver).messageReceived(eq(request));
verify(messageObserver, times(3)).messageReceived(httpObjectCaptor.capture()); request.release();
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1));
assertEquals(trailer, capturedHttpObjects.get(2));
}
@Test
public void clientRequestPushPromise() throws Exception {
serverLatch = new CountDownLatch(4);
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, "/some/path/resource", true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3);
final HttpMessage request2 = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.GET, "/some/path/resource2", true);
HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.set(Http2HttpHeaders.Names.SCHEME, "https");
httpHeaders2.set(Http2HttpHeaders.Names.AUTHORITY, "example.org");
httpHeaders2.set(Http2HttpHeaders.Names.STREAM_ID, 5);
httpHeaders2.set(Http2HttpHeaders.Names.STREAM_PROMISE_ID, 3);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder()
.method("GET").path("/some/path/resource").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder()
.method("GET").path("/some/path/resource2").scheme("https")
.authority("example.org").build();
final String text = "hello 1!**";
final HttpContent content = new DefaultLastHttpContent(Unpooled.copiedBuffer(text.getBytes()),
true);
final String text2 = "hello 2!><";
final HttpContent content2 = new DefaultLastHttpContent(Unpooled.copiedBuffer(text2.getBytes()),
true);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise());
frameWriter.writePushPromise(ctx(), 3, 5, http2Headers2, 0, newPromise());
frameWriter.writeData(ctx(), 3,
Unpooled.copiedBuffer(text.getBytes()), 0, true, newPromise());
frameWriter.writeData(ctx(), 5,
Unpooled.copiedBuffer(text2.getBytes()), 0, true, newPromise());
ctx().flush();
}
});
awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class);
verify(messageObserver, times(4)).messageReceived(httpObjectCaptor.capture());
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1));
assertEquals(request2, capturedHttpObjects.get(2));
assertEquals(content2, capturedHttpObjects.get(3));
} }
@Test @Test
public void clientRequestStreamDependency() throws Exception { public void clientRequestStreamDependency() throws Exception {
serverLatch = new CountDownLatch(4); setServerLatch(2);
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, final String text = "hello world big time data!";
HttpMethod.GET, "/some/path/resource", true); final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final String text2 = "hello world big time data...number 2!!";
final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
"/some/path/resource", content, true);
HttpHeaders httpHeaders = request.headers(); HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2HttpHeaders.Names.STREAM_ID, 3); httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
final HttpMessage request2 = new DefaultHttpRequest(HttpVersion.HTTP_1_1, httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
HttpMethod.GET, "/some/path/resource2", true); final FullHttpMessage request2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
"/some/path/resource2", content2, true);
HttpHeaders httpHeaders2 = request2.headers(); HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.set(Http2HttpHeaders.Names.STREAM_ID, 5); httpHeaders2.set(Http2ToHttpHeaders.Names.STREAM_ID, 5);
httpHeaders2.set(Http2HttpHeaders.Names.STREAM_DEPENDENCY_ID, 3); httpHeaders2.set(Http2ToHttpHeaders.Names.STREAM_DEPENDENCY_ID, 3);
httpHeaders2.set(Http2HttpHeaders.Names.STREAM_EXCLUSIVE, true); httpHeaders2.set(Http2ToHttpHeaders.Names.STREAM_EXCLUSIVE, true);
httpHeaders2.set(Http2HttpHeaders.Names.STREAM_WEIGHT, 256); httpHeaders2.set(Http2ToHttpHeaders.Names.STREAM_WEIGHT, 256);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder() httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length());
.method("GET").path("/some/path/resource").build(); final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT").path("/some/path/resource")
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder() .build();
.method("GET").path("/some/path/resource2").build(); final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().method("PUT").path("/some/path/resource2")
final String text = "hello 1!**"; .build();
final HttpContent content = new DefaultLastHttpContent(Unpooled.copiedBuffer(text.getBytes()),
true);
final String text2 = "hello 2!><";
final HttpContent content2 = new DefaultLastHttpContent(Unpooled.copiedBuffer(text2.getBytes()),
true);
runInChannel(clientChannel, new Http2Runnable() { runInChannel(clientChannel, new Http2Runnable() {
@Override @Override
public void run() { public void run() {
frameWriter.writeHeaders(ctx(), 3, http2Headers, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeHeaders(ctx(), 5, http2Headers2, 0, false, newPromise()); frameWriter.writeHeaders(ctxClient(), 5, http2Headers2, 0, false, newPromiseClient());
frameWriter.writePriority(ctx(), 5, 3, (short) 256, true, newPromise()); frameWriter.writePriority(ctxClient(), 5, 3, (short) 256, true, newPromiseClient());
frameWriter.writeData(ctx(), 3, frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
Unpooled.copiedBuffer(text.getBytes()), 0, true, newPromise()); frameWriter.writeData(ctxClient(), 5, content2.retain(), 0, true, newPromiseClient());
frameWriter.writeData(ctx(), 5, ctxClient().flush();
Unpooled.copiedBuffer(text2.getBytes()), 0, true, newPromise());
ctx().flush();
} }
}); });
awaitRequests(); awaitRequests();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class); ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class);
verify(messageObserver, times(4)).messageReceived(httpObjectCaptor.capture()); verify(serverObserver, times(2)).messageReceived(httpObjectCaptor.capture());
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues(); List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(request, capturedHttpObjects.get(0)); assertEquals(request, capturedHttpObjects.get(0));
assertEquals(content, capturedHttpObjects.get(1)); assertEquals(request2, capturedHttpObjects.get(1));
assertEquals(request2, capturedHttpObjects.get(2)); request.release();
assertEquals(content2, capturedHttpObjects.get(3)); request2.release();
}
@Test
public void serverRequestPushPromise() throws Exception {
setClientLatch(2);
final String text = "hello world big time data!";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final String text2 = "hello world smaller data?";
final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
content, true);
HttpHeaders httpHeaders = response.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED,
content2, true);
HttpHeaders httpHeaders2 = response2.headers();
httpHeaders2.set(Http2ToHttpHeaders.Names.SCHEME, "https");
httpHeaders2.set(Http2ToHttpHeaders.Names.AUTHORITY, "example.org");
httpHeaders2.set(Http2ToHttpHeaders.Names.STREAM_ID, 5);
httpHeaders2.set(Http2ToHttpHeaders.Names.STREAM_PROMISE_ID, 3);
httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length());
final HttpMessage request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/push/test", true);
httpHeaders = request.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2Headers3 = new DefaultHttp2Headers.Builder().method("GET").path("/push/test").build();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
verify(serverObserver).messageReceived(eq(request));
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().status("200").build();
final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().status("201").scheme("https")
.authority("example.org").build();
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxServer(), 3, http2Headers, 0, false, newPromiseServer());
frameWriter.writePushPromise(ctxServer(), 3, 5, http2Headers2, 0, newPromiseServer());
frameWriter.writeData(ctxServer(), 3, content.retain(), 0, true, newPromiseServer());
frameWriter.writeData(ctxServer(), 5, content2.retain(), 0, true, newPromiseServer());
ctxServer().flush();
}
});
awaitResponses();
ArgumentCaptor<HttpObject> httpObjectCaptor = ArgumentCaptor.forClass(HttpObject.class);
verify(clientObserver, times(2)).messageReceived(httpObjectCaptor.capture());
List<HttpObject> capturedHttpObjects = httpObjectCaptor.getAllValues();
assertEquals(response, capturedHttpObjects.get(0));
assertEquals(response2, capturedHttpObjects.get(1));
response.release();
response2.release();
}
@Test
public void serverResponseHeaderInformational() throws Exception {
final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/info/test",
true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.EXPECT, HttpHeaders.Values.CONTINUE);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT").path("/info/test")
.set(HttpHeaders.Names.EXPECT.toString(), HttpHeaders.Values.CONTINUE).build();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
verify(serverObserver).messageReceived(eq(request));
reset(serverObserver);
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
httpHeaders = response.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2HeadersResponse = new DefaultHttp2Headers.Builder().status("100").build();
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxServer(), 3, http2HeadersResponse, 0, false, newPromiseServer());
ctxServer().flush();
}
});
awaitResponses();
verify(clientObserver).messageReceived(eq(response));
reset(clientObserver);
setServerLatch(1);
final String text = "a big payload";
final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes());
final FullHttpMessage request2 = request.copy(payload);
httpHeaders = request2.headers();
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length());
httpHeaders.remove(HttpHeaders.Names.EXPECT);
request.release();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeData(ctxClient(), 3, payload.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
verify(serverObserver).messageReceived(eq(request2));
request2.release();
setClientLatch(1);
final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
httpHeaders = response2.headers();
httpHeaders.set(Http2ToHttpHeaders.Names.STREAM_ID, 3);
httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0);
final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers.Builder().status("200").build();
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxServer(), 3, http2HeadersResponse2, 0, true, newPromiseServer());
ctxServer().flush();
}
});
awaitResponses();
verify(clientObserver).messageReceived(eq(response2));
}
private void setServerLatch(int count) {
serverLatch = new CountDownLatch(count);
if (serverDelegator != null) {
serverDelegator.latch(serverLatch);
}
}
private void setClientLatch(int count) {
clientLatch = new CountDownLatch(count);
if (clientDelegator != null) {
clientDelegator.latch(clientLatch);
}
} }
private void awaitRequests() throws Exception { private void awaitRequests() throws Exception {
serverLatch.await(2, SECONDS); serverLatch.await(200, SECONDS);
} }
private ChannelHandlerContext ctx() { private void awaitResponses() throws Exception {
clientLatch.await(200, SECONDS);
}
private ChannelHandlerContext ctxClient() {
return clientChannel.pipeline().firstContext(); return clientChannel.pipeline().firstContext();
} }
private ChannelPromise newPromise() { private ChannelPromise newPromiseClient() {
return ctx().newPromise(); return ctxClient().newPromise();
}
private ChannelHandlerContext ctxServer() {
return serverConnectedChannel.pipeline().firstContext();
}
private ChannelPromise newPromiseServer() {
return ctxServer().newPromise();
} }
private interface HttpResponseListener { private interface HttpResponseListener {
@ -444,130 +539,127 @@ public class InboundHttp2ToHttpAdapterTest {
private final class HttpResponseDelegator extends SimpleChannelInboundHandler<HttpObject> { private final class HttpResponseDelegator extends SimpleChannelInboundHandler<HttpObject> {
private final HttpResponseListener listener; private final HttpResponseListener listener;
private CountDownLatch latch;
public HttpResponseDelegator(HttpResponseListener listener) { public HttpResponseDelegator(HttpResponseListener listener, CountDownLatch latch) {
super(false);
this.listener = listener; this.listener = listener;
this.latch = latch;
} }
@Override @Override
protected void messageReceived(ChannelHandlerContext ctx, HttpObject msg) throws Exception { protected void messageReceived(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
this.listener.messageReceived(msg); this.listener.messageReceived(msg);
serverLatch.countDown(); this.latch.countDown();
}
public void latch(CountDownLatch latch) {
this.latch = latch;
} }
} }
private final class FrameAdapter extends ByteToMessageDecoder { private final class FrameAdapter extends ByteToMessageDecoder {
private final Http2FrameObserver observer; private final Http2FrameObserver observer;
private final DefaultHttp2FrameReader reader; private final DefaultHttp2FrameReader reader;
private final CountDownLatch requestLatch; private final CountDownLatch latch;
FrameAdapter(Http2FrameObserver observer, CountDownLatch requestLatch) { FrameAdapter(Http2FrameObserver observer, CountDownLatch latch) {
this.observer = observer; this.observer = observer;
reader = new DefaultHttp2FrameReader(); reader = new DefaultHttp2FrameReader();
this.requestLatch = requestLatch; this.latch = latch;
} }
@Override @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
throws Exception {
reader.readFrame(ctx, in, new Http2FrameObserver() { reader.readFrame(ctx, in, new Http2FrameObserver() {
@Override @Override
public void onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, public void onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
int padding, boolean endOfStream) boolean endOfStream) throws Http2Exception {
throws Http2Exception {
observer.onDataRead(ctx, streamId, copy(data), padding, endOfStream); observer.onDataRead(ctx, streamId, copy(data), padding, endOfStream);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
Http2Headers headers, int padding, boolean endStream) boolean endStream) throws Http2Exception {
throws Http2Exception {
observer.onHeadersRead(ctx, streamId, headers, padding, endStream); observer.onHeadersRead(ctx, streamId, headers, padding, endStream);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
Http2Headers headers, int streamDependency, short weight, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream)
boolean exclusive, int padding, boolean endStream) throws Http2Exception {
throws Http2Exception { observer.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding,
observer.onHeadersRead(ctx, streamId, headers, streamDependency, weight, endStream);
exclusive, padding, endStream); latch.countDown();
requestLatch.countDown();
} }
@Override @Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
int streamDependency, short weight, boolean exclusive) boolean exclusive) throws Http2Exception {
throws Http2Exception {
observer.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive); observer.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
throws Http2Exception { throws Http2Exception {
observer.onRstStreamRead(ctx, streamId, errorCode); observer.onRstStreamRead(ctx, streamId, errorCode);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception { public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
observer.onSettingsAckRead(ctx); observer.onSettingsAckRead(ctx);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
throws Http2Exception {
observer.onSettingsRead(ctx, settings); observer.onSettingsRead(ctx, settings);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
throws Http2Exception {
observer.onPingRead(ctx, copy(data)); observer.onPingRead(ctx, copy(data));
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
throws Http2Exception {
observer.onPingAckRead(ctx, copy(data)); observer.onPingAckRead(ctx, copy(data));
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
int promisedStreamId, Http2Headers headers, int padding) Http2Headers headers, int padding) throws Http2Exception {
throws Http2Exception {
observer.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding); observer.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
long errorCode, ByteBuf debugData) throws Http2Exception { throws Http2Exception {
observer.onGoAwayRead(ctx, lastStreamId, errorCode, copy(debugData)); observer.onGoAwayRead(ctx, lastStreamId, errorCode, copy(debugData));
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
int windowSizeIncrement) throws Http2Exception { throws Http2Exception {
observer.onWindowUpdateRead(ctx, streamId, windowSizeIncrement); observer.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
requestLatch.countDown(); latch.countDown();
} }
@Override @Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
Http2Flags flags, ByteBuf payload) { ByteBuf payload) {
observer.onUnknownFrame(ctx, frameType, streamId, flags, payload); observer.onUnknownFrame(ctx, frameType, streamId, flags, payload);
requestLatch.countDown(); latch.countDown();
} }
}); });
} }

View File

@ -40,7 +40,7 @@ import java.util.HashSet;
import java.util.TimeZone; import java.util.TimeZone;
public class DefaultTextHeaders implements TextHeaders { public class DefaultTextHeaders implements TextHeaders {
private static final int HASH_CODE_PRIME = 31;
private static final int BUCKET_SIZE = 17; private static final int BUCKET_SIZE = 17;
private static int index(int hash) { private static int index(int hash) {
@ -853,7 +853,17 @@ public class DefaultTextHeaders implements TextHeaders {
@Override @Override
public Set<String> names() { public Set<String> names() {
Set<String> names = new LinkedHashSet<String>(size()); return names(false);
}
/**
* Get the set of names for all text headers
* @param caseInsensitive {@code true} if names should be added in a case insensitive
* @return The set of names for all text headers
*/
public Set<String> names(boolean caseInsensitive) {
Set<String> names = caseInsensitive ? new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
: new LinkedHashSet<String>(size());
HeaderEntry e = head.after; HeaderEntry e = head.after;
while (e != head) { while (e != head) {
names.add(e.getKey().toString()); names.add(e.getKey().toString());
@ -880,13 +890,12 @@ public class DefaultTextHeaders implements TextHeaders {
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31;
int result = 1; int result = 1;
for (String name : names()) { for (String name : names(true)) {
result = prime * result + name.hashCode(); result = HASH_CODE_PRIME * result + name.hashCode();
Set<String> values = new TreeSet<String>(getAll(name)); Set<String> values = new TreeSet<String>(getAll(name));
for (String value : values) { for (String value : values) {
result = prime * result + value.hashCode(); result = HASH_CODE_PRIME * result + value.hashCode();
} }
} }
return result; return result;
@ -901,8 +910,8 @@ public class DefaultTextHeaders implements TextHeaders {
DefaultTextHeaders other = (DefaultTextHeaders) o; DefaultTextHeaders other = (DefaultTextHeaders) o;
// First, check that the set of names match. // First, check that the set of names match.
Set<String> names = names(); Set<String> names = names(true);
if (!names.equals(other.names())) { if (!names.equals(other.names(true))) {
return false; return false;
} }

View File

@ -15,23 +15,22 @@
*/ */
package io.netty.handler.codec; package io.netty.handler.codec;
import io.netty.channel.ChannelHandlerContext; import static org.junit.Assert.assertFalse;
import io.netty.channel.embedded.EmbeddedChannel; import static org.junit.Assert.assertTrue;
import org.junit.Test;
import static org.junit.Assert.*; import org.junit.Test;
public class DefaultTextHeadersTest { public class DefaultTextHeadersTest {
@Test @Test
public void testEqualsMultipleHeaders() { public void testEqualsMultipleHeaders() {
DefaultTextHeaders h1 = new DefaultTextHeaders(); DefaultTextHeaders h1 = new DefaultTextHeaders();
h1.set("foo", "goo"); h1.set("Foo", "goo");
h1.set("foo2", "goo2"); h1.set("foo2", "goo2");
DefaultTextHeaders h2 = new DefaultTextHeaders(); DefaultTextHeaders h2 = new DefaultTextHeaders();
h2.set("foo", "goo"); h2.set("FoO", "goo");
h2.set("foo2", "goo2"); h2.set("fOO2", "goo2");
assertTrue(h1.equals(h2)); assertTrue(h1.equals(h2));
assertTrue(h2.equals(h1)); assertTrue(h2.equals(h1));
@ -42,16 +41,16 @@ public class DefaultTextHeadersTest {
@Test @Test
public void testEqualsDuplicateMultipleHeaders() { public void testEqualsDuplicateMultipleHeaders() {
DefaultTextHeaders h1 = new DefaultTextHeaders(); DefaultTextHeaders h1 = new DefaultTextHeaders();
h1.set("foo", "goo"); h1.set("FOO", "goo");
h1.set("foo2", "goo2"); h1.set("Foo2", "goo2");
h1.add("foo2", "goo3"); h1.add("fOo2", "goo3");
h1.add("foo", "goo4"); h1.add("foo", "goo4");
DefaultTextHeaders h2 = new DefaultTextHeaders(); DefaultTextHeaders h2 = new DefaultTextHeaders();
h2.set("foo", "goo"); h2.set("foo", "goo");
h2.set("foo2", "goo2"); h2.set("foo2", "goo2");
h2.add("foo", "goo4"); h2.add("foo", "goo4");
h2.add("foo2", "goo3"); h2.add("foO2", "goo3");
assertTrue(h1.equals(h2)); assertTrue(h1.equals(h2));
assertTrue(h2.equals(h1)); assertTrue(h2.equals(h1));
@ -62,7 +61,7 @@ public class DefaultTextHeadersTest {
@Test @Test
public void testNotEqualsDuplicateMultipleHeaders() { public void testNotEqualsDuplicateMultipleHeaders() {
DefaultTextHeaders h1 = new DefaultTextHeaders(); DefaultTextHeaders h1 = new DefaultTextHeaders();
h1.set("foo", "goo"); h1.set("FOO", "goo");
h1.set("foo2", "goo2"); h1.set("foo2", "goo2");
h1.add("foo2", "goo3"); h1.add("foo2", "goo3");
h1.add("foo", "goo4"); h1.add("foo", "goo4");

View File

@ -54,12 +54,12 @@ public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
new Http2FrameLogger(INFO, InternalLoggerFactory.getInstance(Http2ClientInitializer.class)); new Http2FrameLogger(INFO, InternalLoggerFactory.getInstance(Http2ClientInitializer.class));
private final SslContext sslCtx; private final SslContext sslCtx;
private final long maxContentLength; private final int maxContentLength;
private DelegatingHttp2ConnectionHandler connectionHandler; private DelegatingHttp2ConnectionHandler connectionHandler;
private HttpResponseHandler responseHandler; private HttpResponseHandler responseHandler;
private Http2SettingsHandler settingsHandler; private Http2SettingsHandler settingsHandler;
public Http2ClientInitializer(SslContext sslCtx, long maxContentLength) { public Http2ClientInitializer(SslContext sslCtx, int maxContentLength) {
this.sslCtx = sslCtx; this.sslCtx = sslCtx;
this.maxContentLength = maxContentLength; this.maxContentLength = maxContentLength;
} }
@ -91,8 +91,6 @@ public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
protected void configureEndOfPipeline(ChannelPipeline pipeline) { protected void configureEndOfPipeline(ChannelPipeline pipeline) {
pipeline.addLast("Http2SettingsHandler", settingsHandler); pipeline.addLast("Http2SettingsHandler", settingsHandler);
pipeline.addLast("Decompressor", new HttpContentDecompressor());
pipeline.addLast("Aggregator", new HttpObjectAggregator((int) maxContentLength));
pipeline.addLast("HttpResponseHandler", responseHandler); pipeline.addLast("HttpResponseHandler", responseHandler);
} }

View File

@ -19,7 +19,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.Http2HttpHeaders; import io.netty.handler.codec.http2.Http2ToHttpHeaders;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import java.util.Iterator; import java.util.Iterator;
@ -76,7 +76,7 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
@Override @Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception { protected void messageReceived(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
int streamId = Integer.parseInt(msg.headers().get(Http2HttpHeaders.Names.STREAM_ID)); int streamId = Integer.parseInt(msg.headers().get(Http2ToHttpHeaders.Names.STREAM_ID));
ChannelPromise promise = streamidPromiseMap.get(streamId); ChannelPromise promise = streamidPromiseMap.get(streamId);
if (promise == null) { if (promise == null) {
System.err.println("Message received for unknown stream id " + streamId); System.err.println("Message received for unknown stream id " + streamId);