[#527] Add a new property to HttpMessage to help clarify its transfer encoding
- Add an enum: HttpTransferEncoding - consists of SINGLE, STREAMED, and CHUNKED - Add HttpMessage.transferEncoding - replaces is/setChunked()
This commit is contained in:
parent
c22b559dfa
commit
602f976e41
@ -31,7 +31,7 @@ public class DefaultHttpMessage implements HttpMessage {
|
|||||||
private final HttpHeaders headers = new HttpHeaders();
|
private final HttpHeaders headers = new HttpHeaders();
|
||||||
private HttpVersion version;
|
private HttpVersion version;
|
||||||
private ByteBuf content = Unpooled.EMPTY_BUFFER;
|
private ByteBuf content = Unpooled.EMPTY_BUFFER;
|
||||||
private boolean chunked;
|
private HttpTransferEncoding te = HttpTransferEncoding.SINGLE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a new instance.
|
||||||
@ -61,19 +61,31 @@ public class DefaultHttpMessage implements HttpMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isChunked() {
|
public HttpTransferEncoding getTransferEncoding() {
|
||||||
if (chunked) {
|
return te;
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return HttpCodecUtil.isTransferEncodingChunked(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setChunked(boolean chunked) {
|
public void setTransferEncoding(HttpTransferEncoding te) {
|
||||||
this.chunked = chunked;
|
if (te == null) {
|
||||||
if (chunked) {
|
throw new NullPointerException("te (transferEncoding)");
|
||||||
|
}
|
||||||
|
this.te = te;
|
||||||
|
switch (te) {
|
||||||
|
case SINGLE:
|
||||||
|
HttpCodecUtil.removeTransferEncodingChunked(this);
|
||||||
|
break;
|
||||||
|
case STREAMED:
|
||||||
|
HttpCodecUtil.removeTransferEncodingChunked(this);
|
||||||
setContent(Unpooled.EMPTY_BUFFER);
|
setContent(Unpooled.EMPTY_BUFFER);
|
||||||
|
break;
|
||||||
|
case CHUNKED:
|
||||||
|
if (!HttpCodecUtil.isTransferEncodingChunked(this)) {
|
||||||
|
addHeader(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
|
||||||
|
}
|
||||||
|
removeHeader(HttpHeaders.Names.CONTENT_LENGTH);
|
||||||
|
setContent(Unpooled.EMPTY_BUFFER);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,10 +99,12 @@ public class DefaultHttpMessage implements HttpMessage {
|
|||||||
if (content == null) {
|
if (content == null) {
|
||||||
content = Unpooled.EMPTY_BUFFER;
|
content = Unpooled.EMPTY_BUFFER;
|
||||||
}
|
}
|
||||||
if (content.readable() && isChunked()) {
|
|
||||||
|
if (!getTransferEncoding().isSingle() && content.readable()) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"non-empty content disallowed if this.chunked == true");
|
"non-empty content disallowed if this.transferEncoding != SINGLE");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.content = content;
|
this.content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +148,11 @@ public class DefaultHttpMessage implements HttpMessage {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ByteBuf getContent() {
|
public ByteBuf getContent() {
|
||||||
|
if (getTransferEncoding() == HttpTransferEncoding.SINGLE) {
|
||||||
return content;
|
return content;
|
||||||
|
} else {
|
||||||
|
return Unpooled.EMPTY_BUFFER;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -145,8 +163,8 @@ public class DefaultHttpMessage implements HttpMessage {
|
|||||||
buf.append(getProtocolVersion().getText());
|
buf.append(getProtocolVersion().getText());
|
||||||
buf.append(", keepAlive: ");
|
buf.append(", keepAlive: ");
|
||||||
buf.append(HttpHeaders.isKeepAlive(this));
|
buf.append(HttpHeaders.isKeepAlive(this));
|
||||||
buf.append(", chunked: ");
|
buf.append(", transferEncoding: ");
|
||||||
buf.append(isChunked());
|
buf.append(getTransferEncoding());
|
||||||
buf.append(')');
|
buf.append(')');
|
||||||
buf.append(StringUtil.NEWLINE);
|
buf.append(StringUtil.NEWLINE);
|
||||||
appendHeaders(buf);
|
appendHeaders(buf);
|
||||||
|
@ -68,8 +68,8 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder buf = new StringBuilder();
|
StringBuilder buf = new StringBuilder();
|
||||||
buf.append(getClass().getSimpleName());
|
buf.append(getClass().getSimpleName());
|
||||||
buf.append("(chunked: ");
|
buf.append("(transferEncoding: ");
|
||||||
buf.append(isChunked());
|
buf.append(getTransferEncoding());
|
||||||
buf.append(')');
|
buf.append(')');
|
||||||
buf.append(StringUtil.NEWLINE);
|
buf.append(StringUtil.NEWLINE);
|
||||||
buf.append(getMethod().toString());
|
buf.append(getMethod().toString());
|
||||||
|
@ -52,8 +52,8 @@ public class DefaultHttpResponse extends DefaultHttpMessage implements HttpRespo
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder buf = new StringBuilder();
|
StringBuilder buf = new StringBuilder();
|
||||||
buf.append(getClass().getSimpleName());
|
buf.append(getClass().getSimpleName());
|
||||||
buf.append("(chunked: ");
|
buf.append("(transferEncoding: ");
|
||||||
buf.append(isChunked());
|
buf.append(getTransferEncoding());
|
||||||
buf.append(')');
|
buf.append(')');
|
||||||
buf.append(StringUtil.NEWLINE);
|
buf.append(StringUtil.NEWLINE);
|
||||||
buf.append(getProtocolVersion().getText());
|
buf.append(getProtocolVersion().getText());
|
||||||
|
@ -26,7 +26,6 @@ import io.netty.handler.codec.MessageToMessageDecoder;
|
|||||||
import io.netty.handler.codec.TooLongFrameException;
|
import io.netty.handler.codec.TooLongFrameException;
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,29 +127,27 @@ public class HttpChunkAggregator extends MessageToMessageDecoder<Object, HttpMes
|
|||||||
ctx.write(CONTINUE.duplicate());
|
ctx.write(CONTINUE.duplicate());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m.isChunked()) {
|
switch (m.getTransferEncoding()) {
|
||||||
// A chunked message - remove 'Transfer-Encoding' header,
|
case SINGLE:
|
||||||
// initialize the cumulative buffer, and wait for incoming chunks.
|
this.currentMessage = null;
|
||||||
List<String> encodings = m.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING);
|
return m;
|
||||||
encodings.remove(HttpHeaders.Values.CHUNKED);
|
case STREAMED:
|
||||||
if (encodings.isEmpty()) {
|
case CHUNKED:
|
||||||
m.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
|
// A streamed message - initialize the cumulative buffer, and wait for incoming chunks.
|
||||||
}
|
m.setTransferEncoding(HttpTransferEncoding.SINGLE);
|
||||||
m.setChunked(false);
|
|
||||||
m.setContent(Unpooled.compositeBuffer(maxCumulationBufferComponents));
|
m.setContent(Unpooled.compositeBuffer(maxCumulationBufferComponents));
|
||||||
this.currentMessage = m;
|
this.currentMessage = m;
|
||||||
return null;
|
return null;
|
||||||
} else {
|
default:
|
||||||
// Not a chunked message - pass through.
|
throw new Error();
|
||||||
this.currentMessage = null;
|
|
||||||
return m;
|
|
||||||
}
|
}
|
||||||
} else if (msg instanceof HttpChunk) {
|
} else if (msg instanceof HttpChunk) {
|
||||||
// Sanity check
|
// Sanity check
|
||||||
if (currentMessage == null) {
|
if (currentMessage == null) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"received " + HttpChunk.class.getSimpleName() +
|
"received " + HttpChunk.class.getSimpleName() +
|
||||||
" without " + HttpMessage.class.getSimpleName());
|
" without " + HttpMessage.class.getSimpleName() +
|
||||||
|
" or last message's transfer encoding was 'SINGLE'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge the received chunk into the content of the current message.
|
// Merge the received chunk into the content of the current message.
|
||||||
|
@ -92,7 +92,7 @@ public class HttpClientCodec extends CombinedChannelHandler {
|
|||||||
|
|
||||||
if (failOnMissingResponse) {
|
if (failOnMissingResponse) {
|
||||||
// check if the request is chunked if so do not increment
|
// check if the request is chunked if so do not increment
|
||||||
if (msg instanceof HttpRequest && !((HttpRequest) msg).isChunked()) {
|
if (msg instanceof HttpRequest && ((HttpMessage) msg).getTransferEncoding().isSingle()) {
|
||||||
requestResponseCounter.incrementAndGet();
|
requestResponseCounter.incrementAndGet();
|
||||||
} else if (msg instanceof HttpChunk && ((HttpChunk) msg).isLast()) {
|
} else if (msg instanceof HttpChunk && ((HttpChunk) msg).isLast()) {
|
||||||
// increment as its the last chunk
|
// increment as its the last chunk
|
||||||
@ -127,8 +127,8 @@ public class HttpClientCodec extends CombinedChannelHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if its a HttpMessage and its not chunked
|
// check if it's an HttpMessage and its transfer encoding is SINGLE.
|
||||||
if (msg instanceof HttpMessage && !((HttpMessage) msg).isChunked()) {
|
if (msg instanceof HttpMessage && ((HttpMessage) msg).getTransferEncoding().isSingle()) {
|
||||||
requestResponseCounter.decrementAndGet();
|
requestResponseCounter.decrementAndGet();
|
||||||
} else if (msg instanceof HttpChunk && ((HttpChunk) msg).isLast()) {
|
} else if (msg instanceof HttpChunk && ((HttpChunk) msg).isLast()) {
|
||||||
requestResponseCounter.decrementAndGet();
|
requestResponseCounter.decrementAndGet();
|
||||||
|
@ -154,8 +154,12 @@ final class HttpCodecUtil {
|
|||||||
static void removeTransferEncodingChunked(HttpMessage m) {
|
static void removeTransferEncodingChunked(HttpMessage m) {
|
||||||
List<String> values = m.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING);
|
List<String> values = m.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING);
|
||||||
values.remove(HttpHeaders.Values.CHUNKED);
|
values.remove(HttpHeaders.Values.CHUNKED);
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
m.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
|
||||||
|
} else {
|
||||||
m.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, values);
|
m.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, values);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static boolean isContentLengthSet(HttpMessage m) {
|
static boolean isContentLengthSet(HttpMessage m) {
|
||||||
List<String> contentLength = m.getHeaders(HttpHeaders.Names.CONTENT_LENGTH);
|
List<String> contentLength = m.getHeaders(HttpHeaders.Names.CONTENT_LENGTH);
|
||||||
|
@ -73,7 +73,8 @@ public abstract class HttpContentDecoder extends MessageToMessageDecoder<Object,
|
|||||||
contentEncoding = HttpHeaders.Values.IDENTITY;
|
contentEncoding = HttpHeaders.Values.IDENTITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasContent = m.isChunked() || m.getContent().readable();
|
boolean hasContent =
|
||||||
|
m.getTransferEncoding().isMultiple() || m.getContent().readable();
|
||||||
if (hasContent && (decoder = newContentDecoder(contentEncoding)) != null) {
|
if (hasContent && (decoder = newContentDecoder(contentEncoding)) != null) {
|
||||||
// Decode the content and remove or replace the existing headers
|
// Decode the content and remove or replace the existing headers
|
||||||
// so that the message looks like a decoded message.
|
// so that the message looks like a decoded message.
|
||||||
@ -81,7 +82,7 @@ public abstract class HttpContentDecoder extends MessageToMessageDecoder<Object,
|
|||||||
HttpHeaders.Names.CONTENT_ENCODING,
|
HttpHeaders.Names.CONTENT_ENCODING,
|
||||||
getTargetContentEncoding(contentEncoding));
|
getTargetContentEncoding(contentEncoding));
|
||||||
|
|
||||||
if (!m.isChunked()) {
|
if (m.getTransferEncoding().isSingle()) {
|
||||||
ByteBuf content = m.getContent();
|
ByteBuf content = m.getContent();
|
||||||
// Decode the content
|
// Decode the content
|
||||||
ByteBuf newContent = Unpooled.buffer();
|
ByteBuf newContent = Unpooled.buffer();
|
||||||
|
@ -96,7 +96,7 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpMessa
|
|||||||
throw new IllegalStateException("cannot send more responses than requests");
|
throw new IllegalStateException("cannot send more responses than requests");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasContent = m.isChunked() || m.getContent().readable();
|
boolean hasContent = m.getTransferEncoding().isMultiple() || m.getContent().readable();
|
||||||
if (!hasContent) {
|
if (!hasContent) {
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@ public abstract class HttpContentEncoder extends MessageToMessageCodec<HttpMessa
|
|||||||
HttpHeaders.Names.CONTENT_ENCODING,
|
HttpHeaders.Names.CONTENT_ENCODING,
|
||||||
result.getTargetContentEncoding());
|
result.getTargetContentEncoding());
|
||||||
|
|
||||||
if (!m.isChunked()) {
|
if (m.getTransferEncoding().isSingle()) {
|
||||||
ByteBuf content = m.getContent();
|
ByteBuf content = m.getContent();
|
||||||
// Encode the content.
|
// Encode the content.
|
||||||
ByteBuf newContent = Unpooled.buffer();
|
ByteBuf newContent = Unpooled.buffer();
|
||||||
|
@ -94,7 +94,8 @@ public interface HttpMessage {
|
|||||||
/**
|
/**
|
||||||
* Returns the content of this {@link HttpMessage}.
|
* Returns the content of this {@link HttpMessage}.
|
||||||
*
|
*
|
||||||
* If there is no content or {@link #isChunked()} returns {@code true},
|
* If there is no content or {@link #getTransferEncoding()} returns
|
||||||
|
* {@link HttpTransferEncoding#STREAMED} or {@link HttpTransferEncoding#CHUNKED},
|
||||||
* an {@link Unpooled#EMPTY_BUFFER} is returned.
|
* an {@link Unpooled#EMPTY_BUFFER} is returned.
|
||||||
*
|
*
|
||||||
* @return A {@link ByteBuf} containing this {@link HttpMessage}'s content
|
* @return A {@link ByteBuf} containing this {@link HttpMessage}'s content
|
||||||
@ -171,43 +172,31 @@ public interface HttpMessage {
|
|||||||
void clearHeaders();
|
void clearHeaders();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks to see if this {@link HttpMessage} is broken into multiple "chunks"
|
* Returns the transfer encoding of this {@link HttpMessage}.
|
||||||
*
|
* <ul>
|
||||||
* If this returns true, it means that this {@link HttpMessage}
|
* <li>{@link HttpTransferEncoding#CHUNKED} - an HTTP message whose {@code "Transfer-Encoding"}
|
||||||
* actually has no content - The {@link HttpChunk}s (which are generated
|
* is {@code "chunked"}.</li>
|
||||||
* by the {@link HttpMessageDecoder} consecutively) contain the actual content.
|
* <li>{@link HttpTransferEncoding#STREAMED} - an HTTP message which is not chunked, but
|
||||||
* <p>
|
* is followed by {@link HttpChunk}s that represent its content. {@link #getContent()}
|
||||||
* Please note that this method will keep returning {@code true} if the
|
* returns an empty buffer.</li>
|
||||||
* {@code "Transfer-Encoding"} of this message is {@code "chunked"}, even if
|
* <li>{@link HttpTransferEncoding#SINGLE} - a self-contained HTTP message which is not chunked
|
||||||
* you attempt to override this property by calling {@link #setChunked(boolean)}
|
* and {@link #getContent()} returns the full content.</li>
|
||||||
* with {@code false}.
|
* </ul>
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return True if this message is chunked, otherwise false
|
|
||||||
*/
|
*/
|
||||||
boolean isChunked();
|
HttpTransferEncoding getTransferEncoding();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the boolean defining if this {@link HttpMessage} is chunked.
|
* Sets the transfer encoding of this {@link HttpMessage}.
|
||||||
*
|
* <ul>
|
||||||
* <p>
|
* <li>If set to {@link HttpTransferEncoding#CHUNKED}, the {@code "Transfer-Encoding: chunked"}
|
||||||
* If this is set to true, it means that this initial {@link HttpMessage}
|
* header is set and the {@code "Content-Length"} header and the content of this message are
|
||||||
* does not contain any content - The content is contained by multiple
|
* removed automatically.</li>
|
||||||
* {@link HttpChunk}s, which are generated by the {@link HttpMessageDecoder}
|
* <li>If set to {@link HttpTransferEncoding#STREAMED}, the {@code "Transfer-Encoding: chunked"}
|
||||||
* consecutively.
|
* header and the content of this message are removed automatically.</li>
|
||||||
*
|
* <li>If set to {@link HttpTransferEncoding#SINGLE}, the {@code "Transfer-Encoding: chunked"}
|
||||||
* Because of this, the content of this {@link HttpMessage} becomes
|
* header is removed automatically.</li>
|
||||||
* {@link Unpooled#EMPTY_BUFFER}
|
* </ul>
|
||||||
* </p>
|
* For more information about what {@link HttpTransferEncoding} means, see {@link #getTransferEncoding()}.
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Even if this method is called with {@code false}, {@link #isChunked()}
|
|
||||||
* will keep returning {@code true} if the {@code "Transfer-Encoding"} of
|
|
||||||
* this message is {@code "chunked"}.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param chunked True if this message is to be delivered in chunks,
|
|
||||||
* otherwise false.
|
|
||||||
*/
|
*/
|
||||||
void setChunked(boolean chunked);
|
void setTransferEncoding(HttpTransferEncoding te);
|
||||||
}
|
}
|
||||||
|
@ -191,15 +191,10 @@ public abstract class HttpMessageDecoder extends ReplayingDecoder<Object, HttpMe
|
|||||||
State nextState = readHeaders(buffer);
|
State nextState = readHeaders(buffer);
|
||||||
checkpoint(nextState);
|
checkpoint(nextState);
|
||||||
if (nextState == State.READ_CHUNK_SIZE) {
|
if (nextState == State.READ_CHUNK_SIZE) {
|
||||||
// Chunked encoding
|
// Chunked encoding - generate HttpMessage first. HttpChunks will follow.
|
||||||
message.setChunked(true);
|
|
||||||
// Generate HttpMessage first. HttpChunks will follow.
|
|
||||||
return message;
|
return message;
|
||||||
} else if (nextState == State.SKIP_CONTROL_CHARS) {
|
} else if (nextState == State.SKIP_CONTROL_CHARS) {
|
||||||
// No content is expected.
|
// No content is expected.
|
||||||
// Remove the headers which are not supposed to be present not
|
|
||||||
// to confuse subsequent handlers.
|
|
||||||
message.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
|
|
||||||
return message;
|
return message;
|
||||||
} else {
|
} else {
|
||||||
long contentLength = HttpHeaders.getContentLength(message, -1);
|
long contentLength = HttpHeaders.getContentLength(message, -1);
|
||||||
@ -213,7 +208,7 @@ public abstract class HttpMessageDecoder extends ReplayingDecoder<Object, HttpMe
|
|||||||
if (contentLength > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
|
if (contentLength > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
|
||||||
// Generate HttpMessage first. HttpChunks will follow.
|
// Generate HttpMessage first. HttpChunks will follow.
|
||||||
checkpoint(State.READ_FIXED_LENGTH_CONTENT_AS_CHUNKS);
|
checkpoint(State.READ_FIXED_LENGTH_CONTENT_AS_CHUNKS);
|
||||||
message.setChunked(true);
|
message.setTransferEncoding(HttpTransferEncoding.STREAMED);
|
||||||
// chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT_AS_CHUNKS
|
// chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT_AS_CHUNKS
|
||||||
// state reads data chunk by chunk.
|
// state reads data chunk by chunk.
|
||||||
chunkSize = HttpHeaders.getContentLength(message, -1);
|
chunkSize = HttpHeaders.getContentLength(message, -1);
|
||||||
@ -224,7 +219,7 @@ public abstract class HttpMessageDecoder extends ReplayingDecoder<Object, HttpMe
|
|||||||
if (buffer.readableBytes() > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
|
if (buffer.readableBytes() > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
|
||||||
// Generate HttpMessage first. HttpChunks will follow.
|
// Generate HttpMessage first. HttpChunks will follow.
|
||||||
checkpoint(State.READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS);
|
checkpoint(State.READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS);
|
||||||
message.setChunked(true);
|
message.setTransferEncoding(HttpTransferEncoding.STREAMED);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -240,8 +235,9 @@ public abstract class HttpMessageDecoder extends ReplayingDecoder<Object, HttpMe
|
|||||||
if (toRead > maxChunkSize) {
|
if (toRead > maxChunkSize) {
|
||||||
toRead = maxChunkSize;
|
toRead = maxChunkSize;
|
||||||
}
|
}
|
||||||
if (!message.isChunked()) {
|
|
||||||
message.setChunked(true);
|
if (message.getTransferEncoding() != HttpTransferEncoding.STREAMED) {
|
||||||
|
message.setTransferEncoding(HttpTransferEncoding.STREAMED);
|
||||||
return new Object[] {message, new DefaultHttpChunk(buffer.readBytes(toRead))};
|
return new Object[] {message, new DefaultHttpChunk(buffer.readBytes(toRead))};
|
||||||
} else {
|
} else {
|
||||||
return new DefaultHttpChunk(buffer.readBytes(toRead));
|
return new DefaultHttpChunk(buffer.readBytes(toRead));
|
||||||
@ -464,8 +460,8 @@ public abstract class HttpMessageDecoder extends ReplayingDecoder<Object, HttpMe
|
|||||||
}
|
}
|
||||||
contentRead += toRead;
|
contentRead += toRead;
|
||||||
if (length < contentRead) {
|
if (length < contentRead) {
|
||||||
if (!message.isChunked()) {
|
if (message.getTransferEncoding() != HttpTransferEncoding.STREAMED) {
|
||||||
message.setChunked(true);
|
message.setTransferEncoding(HttpTransferEncoding.STREAMED);
|
||||||
return new Object[] {message, new DefaultHttpChunk(read(buffer, toRead))};
|
return new Object[] {message, new DefaultHttpChunk(read(buffer, toRead))};
|
||||||
} else {
|
} else {
|
||||||
return new DefaultHttpChunk(read(buffer, toRead));
|
return new DefaultHttpChunk(read(buffer, toRead));
|
||||||
@ -533,14 +529,10 @@ public abstract class HttpMessageDecoder extends ReplayingDecoder<Object, HttpMe
|
|||||||
State nextState;
|
State nextState;
|
||||||
|
|
||||||
if (isContentAlwaysEmpty(message)) {
|
if (isContentAlwaysEmpty(message)) {
|
||||||
|
message.setTransferEncoding(HttpTransferEncoding.SINGLE);
|
||||||
nextState = State.SKIP_CONTROL_CHARS;
|
nextState = State.SKIP_CONTROL_CHARS;
|
||||||
} else if (message.isChunked()) {
|
} else if (HttpCodecUtil.isTransferEncodingChunked(message)) {
|
||||||
// HttpMessage.isChunked() returns true when either:
|
message.setTransferEncoding(HttpTransferEncoding.CHUNKED);
|
||||||
// 1) HttpMessage.setChunked(true) was called or
|
|
||||||
// 2) 'Transfer-Encoding' is 'chunked'.
|
|
||||||
// Because this decoder did not call HttpMessage.setChunked(true)
|
|
||||||
// yet, HttpMessage.isChunked() should return true only when
|
|
||||||
// 'Transfer-Encoding' is 'chunked'.
|
|
||||||
nextState = State.READ_CHUNK_SIZE;
|
nextState = State.READ_CHUNK_SIZE;
|
||||||
} else if (HttpHeaders.getContentLength(message, -1) >= 0) {
|
} else if (HttpHeaders.getContentLength(message, -1) >= 0) {
|
||||||
nextState = State.READ_FIXED_LENGTH_CONTENT;
|
nextState = State.READ_FIXED_LENGTH_CONTENT;
|
||||||
|
@ -21,11 +21,8 @@ import io.netty.buffer.ByteBuf;
|
|||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.MessageToByteEncoder;
|
import io.netty.handler.codec.MessageToByteEncoder;
|
||||||
import io.netty.handler.codec.UnsupportedMessageTypeException;
|
import io.netty.handler.codec.UnsupportedMessageTypeException;
|
||||||
import io.netty.handler.codec.http.HttpHeaders.Names;
|
|
||||||
import io.netty.handler.codec.http.HttpHeaders.Values;
|
|
||||||
import io.netty.util.CharsetUtil;
|
import io.netty.util.CharsetUtil;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,7 +44,7 @@ public abstract class HttpMessageEncoder extends MessageToByteEncoder<Object> {
|
|||||||
private static final ByteBuf LAST_CHUNK =
|
private static final ByteBuf LAST_CHUNK =
|
||||||
copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII);
|
copiedBuffer("0\r\n\r\n", CharsetUtil.US_ASCII);
|
||||||
|
|
||||||
private boolean transferEncodingChunked;
|
private HttpTransferEncoding lastTE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance.
|
* Creates a new instance.
|
||||||
@ -64,26 +61,14 @@ public abstract class HttpMessageEncoder extends MessageToByteEncoder<Object> {
|
|||||||
public void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
|
public void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
|
||||||
if (msg instanceof HttpMessage) {
|
if (msg instanceof HttpMessage) {
|
||||||
HttpMessage m = (HttpMessage) msg;
|
HttpMessage m = (HttpMessage) msg;
|
||||||
boolean contentMustBeEmpty;
|
HttpTransferEncoding te = m.getTransferEncoding();
|
||||||
if (m.isChunked()) {
|
lastTE = te;
|
||||||
// if Content-Length is set then the message can't be HTTP chunked
|
// Calling setTransferEncoding() will sanitize the headers and the content.
|
||||||
if (HttpCodecUtil.isContentLengthSet(m)) {
|
// For example, it will remove the cases such as 'Transfer-Encoding' and 'Content-Length'
|
||||||
contentMustBeEmpty = false;
|
// coexist. It also removes the content if the transferEncoding is not SINGLE.
|
||||||
transferEncodingChunked = false;
|
m.setTransferEncoding(te);
|
||||||
HttpCodecUtil.removeTransferEncodingChunked(m);
|
|
||||||
} else {
|
|
||||||
// check if the Transfer-Encoding is set to chunked already.
|
|
||||||
// if not add the header to the message
|
|
||||||
if (!HttpCodecUtil.isTransferEncodingChunked(m)) {
|
|
||||||
m.addHeader(Names.TRANSFER_ENCODING, Values.CHUNKED);
|
|
||||||
}
|
|
||||||
contentMustBeEmpty = true;
|
|
||||||
transferEncodingChunked = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
transferEncodingChunked = contentMustBeEmpty = HttpCodecUtil.isTransferEncodingChunked(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Encode the message.
|
||||||
out.markWriterIndex();
|
out.markWriterIndex();
|
||||||
encodeInitialLine(out, m);
|
encodeInitialLine(out, m);
|
||||||
encodeHeaders(out, m);
|
encodeHeaders(out, m);
|
||||||
@ -91,20 +76,25 @@ public abstract class HttpMessageEncoder extends MessageToByteEncoder<Object> {
|
|||||||
out.writeByte(LF);
|
out.writeByte(LF);
|
||||||
|
|
||||||
ByteBuf content = m.getContent();
|
ByteBuf content = m.getContent();
|
||||||
if (content.readable()) {
|
|
||||||
if (contentMustBeEmpty) {
|
|
||||||
out.resetWriterIndex();
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"HttpMessage.content must be empty if Transfer-Encoding is chunked.");
|
|
||||||
} else {
|
|
||||||
out.writeBytes(content, content.readerIndex(), content.readableBytes());
|
out.writeBytes(content, content.readerIndex(), content.readableBytes());
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg instanceof HttpChunk) {
|
} else if (msg instanceof HttpChunk) {
|
||||||
HttpChunk chunk = (HttpChunk) msg;
|
HttpChunk chunk = (HttpChunk) msg;
|
||||||
if (transferEncodingChunked) {
|
HttpTransferEncoding te = lastTE;
|
||||||
|
if (te == null) {
|
||||||
|
throw new IllegalArgumentException("HttpChunk must follow an HttpMessage.");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (te) {
|
||||||
|
case SINGLE:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The transfer encoding of the last encoded HttpMessage is SINGLE.");
|
||||||
|
case STREAMED: {
|
||||||
|
ByteBuf content = chunk.getContent();
|
||||||
|
out.writeBytes(content, content.readerIndex(), content.readableBytes());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CHUNKED:
|
||||||
if (chunk.isLast()) {
|
if (chunk.isLast()) {
|
||||||
transferEncodingChunked = false;
|
|
||||||
if (chunk instanceof HttpChunkTrailer) {
|
if (chunk instanceof HttpChunkTrailer) {
|
||||||
out.writeByte((byte) '0');
|
out.writeByte((byte) '0');
|
||||||
out.writeByte(CR);
|
out.writeByte(CR);
|
||||||
@ -125,11 +115,6 @@ public abstract class HttpMessageEncoder extends MessageToByteEncoder<Object> {
|
|||||||
out.writeByte(CR);
|
out.writeByte(CR);
|
||||||
out.writeByte(LF);
|
out.writeByte(LF);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (!chunk.isLast()) {
|
|
||||||
ByteBuf chunkContent = chunk.getContent();
|
|
||||||
out.writeBytes(chunkContent, chunkContent.readerIndex(), chunkContent.readableBytes());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedMessageTypeException(msg, HttpMessage.class, HttpChunk.class);
|
throw new UnsupportedMessageTypeException(msg, HttpMessage.class, HttpChunk.class);
|
||||||
@ -137,27 +122,18 @@ public abstract class HttpMessageEncoder extends MessageToByteEncoder<Object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void encodeHeaders(ByteBuf buf, HttpMessage message) {
|
private static void encodeHeaders(ByteBuf buf, HttpMessage message) {
|
||||||
try {
|
|
||||||
for (Map.Entry<String, String> h: message.getHeaders()) {
|
for (Map.Entry<String, String> h: message.getHeaders()) {
|
||||||
encodeHeader(buf, h.getKey(), h.getValue());
|
encodeHeader(buf, h.getKey(), h.getValue());
|
||||||
}
|
}
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw (Error) new Error().initCause(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void encodeTrailingHeaders(ByteBuf buf, HttpChunkTrailer trailer) {
|
private static void encodeTrailingHeaders(ByteBuf buf, HttpChunkTrailer trailer) {
|
||||||
try {
|
|
||||||
for (Map.Entry<String, String> h: trailer.getHeaders()) {
|
for (Map.Entry<String, String> h: trailer.getHeaders()) {
|
||||||
encodeHeader(buf, h.getKey(), h.getValue());
|
encodeHeader(buf, h.getKey(), h.getValue());
|
||||||
}
|
}
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw (Error) new Error().initCause(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void encodeHeader(ByteBuf buf, String header, String value)
|
private static void encodeHeader(ByteBuf buf, String header, String value) {
|
||||||
throws UnsupportedEncodingException {
|
|
||||||
buf.writeBytes(header.getBytes(CharsetUtil.US_ASCII));
|
buf.writeBytes(header.getBytes(CharsetUtil.US_ASCII));
|
||||||
buf.writeByte(COLON);
|
buf.writeByte(COLON);
|
||||||
buf.writeByte(SP);
|
buf.writeByte(SP);
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License,
|
||||||
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package io.netty.handler.codec.http;
|
||||||
|
|
||||||
|
public enum HttpTransferEncoding {
|
||||||
|
CHUNKED(false),
|
||||||
|
STREAMED(false),
|
||||||
|
SINGLE(true);
|
||||||
|
|
||||||
|
private final boolean single;
|
||||||
|
|
||||||
|
private HttpTransferEncoding(boolean single) {
|
||||||
|
this.single = single;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMultiple() {
|
||||||
|
return !single;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSingle() {
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
}
|
@ -218,7 +218,7 @@ public class SpdyHttpEncoder extends MessageToMessageEncoder<Object, Object> {
|
|||||||
|
|
||||||
private SpdySynStreamFrame createSynStreamFrame(HttpMessage httpMessage)
|
private SpdySynStreamFrame createSynStreamFrame(HttpMessage httpMessage)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
boolean chunked = httpMessage.isChunked();
|
boolean chunked = httpMessage.getTransferEncoding().isMultiple();
|
||||||
|
|
||||||
// Get the Stream-ID, Associated-To-Stream-ID, Priority, URL, and scheme from the headers
|
// Get the Stream-ID, Associated-To-Stream-ID, Priority, URL, and scheme from the headers
|
||||||
int streamID = SpdyHttpHeaders.getStreamId(httpMessage);
|
int streamID = SpdyHttpHeaders.getStreamId(httpMessage);
|
||||||
@ -286,7 +286,7 @@ public class SpdyHttpEncoder extends MessageToMessageEncoder<Object, Object> {
|
|||||||
|
|
||||||
private SpdySynReplyFrame createSynReplyFrame(HttpResponse httpResponse)
|
private SpdySynReplyFrame createSynReplyFrame(HttpResponse httpResponse)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
boolean chunked = httpResponse.isChunked();
|
boolean chunked = httpResponse.getTransferEncoding().isMultiple();
|
||||||
|
|
||||||
// Get the Stream-ID from the headers
|
// Get the Stream-ID from the headers
|
||||||
int streamID = SpdyHttpHeaders.getStreamId(httpResponse);
|
int streamID = SpdyHttpHeaders.getStreamId(httpResponse);
|
||||||
|
@ -38,7 +38,7 @@ public class HttpChunkAggregatorTest {
|
|||||||
|
|
||||||
HttpMessage message = new DefaultHttpMessage(HttpVersion.HTTP_1_1);
|
HttpMessage message = new DefaultHttpMessage(HttpVersion.HTTP_1_1);
|
||||||
HttpHeaders.setHeader(message, "X-Test", true);
|
HttpHeaders.setHeader(message, "X-Test", true);
|
||||||
message.setChunked(true);
|
message.setTransferEncoding(HttpTransferEncoding.STREAMED);
|
||||||
HttpChunk chunk1 = new DefaultHttpChunk(Unpooled.copiedBuffer("test", CharsetUtil.US_ASCII));
|
HttpChunk chunk1 = new DefaultHttpChunk(Unpooled.copiedBuffer("test", CharsetUtil.US_ASCII));
|
||||||
HttpChunk chunk2 = new DefaultHttpChunk(Unpooled.copiedBuffer("test2", CharsetUtil.US_ASCII));
|
HttpChunk chunk2 = new DefaultHttpChunk(Unpooled.copiedBuffer("test2", CharsetUtil.US_ASCII));
|
||||||
HttpChunk chunk3 = new DefaultHttpChunk(Unpooled.EMPTY_BUFFER);
|
HttpChunk chunk3 = new DefaultHttpChunk(Unpooled.EMPTY_BUFFER);
|
||||||
@ -59,7 +59,7 @@ public class HttpChunkAggregatorTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkContentBuffer(HttpMessage aggregatedMessage) {
|
private static void checkContentBuffer(HttpMessage aggregatedMessage) {
|
||||||
CompositeByteBuf buffer = (CompositeByteBuf) aggregatedMessage.getContent();
|
CompositeByteBuf buffer = (CompositeByteBuf) aggregatedMessage.getContent();
|
||||||
assertEquals(2, buffer.numComponents());
|
assertEquals(2, buffer.numComponents());
|
||||||
List<ByteBuf> buffers = buffer.decompose(0, buffer.capacity());
|
List<ByteBuf> buffers = buffer.decompose(0, buffer.capacity());
|
||||||
@ -76,7 +76,7 @@ public class HttpChunkAggregatorTest {
|
|||||||
EmbeddedMessageChannel embedder = new EmbeddedMessageChannel(aggr);
|
EmbeddedMessageChannel embedder = new EmbeddedMessageChannel(aggr);
|
||||||
HttpMessage message = new DefaultHttpMessage(HttpVersion.HTTP_1_1);
|
HttpMessage message = new DefaultHttpMessage(HttpVersion.HTTP_1_1);
|
||||||
HttpHeaders.setHeader(message, "X-Test", true);
|
HttpHeaders.setHeader(message, "X-Test", true);
|
||||||
message.setChunked(true);
|
message.setTransferEncoding(HttpTransferEncoding.CHUNKED);
|
||||||
HttpChunk chunk1 = new DefaultHttpChunk(Unpooled.copiedBuffer("test", CharsetUtil.US_ASCII));
|
HttpChunk chunk1 = new DefaultHttpChunk(Unpooled.copiedBuffer("test", CharsetUtil.US_ASCII));
|
||||||
HttpChunk chunk2 = new DefaultHttpChunk(Unpooled.copiedBuffer("test2", CharsetUtil.US_ASCII));
|
HttpChunk chunk2 = new DefaultHttpChunk(Unpooled.copiedBuffer("test2", CharsetUtil.US_ASCII));
|
||||||
HttpChunkTrailer trailer = new DefaultHttpChunkTrailer();
|
HttpChunkTrailer trailer = new DefaultHttpChunkTrailer();
|
||||||
@ -107,7 +107,7 @@ public class HttpChunkAggregatorTest {
|
|||||||
HttpChunkAggregator aggr = new HttpChunkAggregator(4);
|
HttpChunkAggregator aggr = new HttpChunkAggregator(4);
|
||||||
EmbeddedMessageChannel embedder = new EmbeddedMessageChannel(aggr);
|
EmbeddedMessageChannel embedder = new EmbeddedMessageChannel(aggr);
|
||||||
HttpMessage message = new DefaultHttpMessage(HttpVersion.HTTP_1_1);
|
HttpMessage message = new DefaultHttpMessage(HttpVersion.HTTP_1_1);
|
||||||
message.setChunked(true);
|
message.setTransferEncoding(HttpTransferEncoding.STREAMED);
|
||||||
HttpChunk chunk1 = new DefaultHttpChunk(Unpooled.copiedBuffer("test", CharsetUtil.US_ASCII));
|
HttpChunk chunk1 = new DefaultHttpChunk(Unpooled.copiedBuffer("test", CharsetUtil.US_ASCII));
|
||||||
HttpChunk chunk2 = new DefaultHttpChunk(Unpooled.copiedBuffer("test2", CharsetUtil.US_ASCII));
|
HttpChunk chunk2 = new DefaultHttpChunk(Unpooled.copiedBuffer("test2", CharsetUtil.US_ASCII));
|
||||||
assertFalse(embedder.writeInbound(message));
|
assertFalse(embedder.writeInbound(message));
|
||||||
|
@ -45,8 +45,7 @@ public class HttpServerCodecTest {
|
|||||||
decoderEmbedder.finish();
|
decoderEmbedder.finish();
|
||||||
|
|
||||||
HttpMessage httpMessage = (HttpMessage) decoderEmbedder.readInbound();
|
HttpMessage httpMessage = (HttpMessage) decoderEmbedder.readInbound();
|
||||||
Assert.assertTrue(httpMessage.isChunked());
|
Assert.assertSame(HttpTransferEncoding.STREAMED, httpMessage.getTransferEncoding());
|
||||||
|
|
||||||
|
|
||||||
boolean empty = true;
|
boolean empty = true;
|
||||||
int totalBytesPolled = 0;
|
int totalBytesPolled = 0;
|
||||||
|
@ -44,7 +44,7 @@ public class HttpSnoopClientHandler extends ChannelInboundMessageHandlerAdapter<
|
|||||||
System.out.println();
|
System.out.println();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.isChunked()) {
|
if (response.getTransferEncoding().isMultiple()) {
|
||||||
readingChunks = true;
|
readingChunks = true;
|
||||||
System.out.println("CHUNKED CONTENT {");
|
System.out.println("CHUNKED CONTENT {");
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,7 +84,7 @@ public class HttpSnoopServerHandler extends ChannelInboundMessageHandlerAdapter<
|
|||||||
buf.append("\r\n");
|
buf.append("\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.isChunked()) {
|
if (request.getTransferEncoding().isMultiple()) {
|
||||||
readingChunks = true;
|
readingChunks = true;
|
||||||
} else {
|
} else {
|
||||||
ByteBuf content = request.getContent();
|
ByteBuf content = request.getContent();
|
||||||
|
Loading…
Reference in New Issue
Block a user