[#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:
Trustin Lee 2012-08-19 19:06:47 +09:00
parent c22b559dfa
commit 602f976e41
17 changed files with 173 additions and 161 deletions

View File

@ -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() {
return content; if (getTransferEncoding() == HttpTransferEncoding.SINGLE) {
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);

View File

@ -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());

View File

@ -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());

View File

@ -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.

View File

@ -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();

View File

@ -154,7 +154,11 @@ 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);
m.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, values); if (values.isEmpty()) {
m.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
} else {
m.setHeader(HttpHeaders.Names.TRANSFER_ENCODING, values);
}
} }
static boolean isContentLengthSet(HttpMessage m) { static boolean isContentLengthSet(HttpMessage m) {

View File

@ -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();

View File

@ -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();

View File

@ -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);
} }

View File

@ -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;

View File

@ -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()) { out.writeBytes(content, content.readerIndex(), content.readableBytes());
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());
}
}
} 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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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));

View File

@ -24,7 +24,7 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
public class HttpServerCodecTest { public class HttpServerCodecTest {
/** /**
* Testcase for https://github.com/netty/netty/issues/433 * Testcase for https://github.com/netty/netty/issues/433
*/ */
@ -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;

View File

@ -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 {

View File

@ -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();