View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.Channel;
20  import io.netty.channel.ChannelHandlerAppender;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.handler.codec.PrematureChannelClosureException;
23  
24  import java.util.ArrayDeque;
25  import java.util.List;
26  import java.util.Queue;
27  import java.util.concurrent.atomic.AtomicLong;
28  
29  /**
30   * A combination of {@link HttpRequestEncoder} and {@link HttpResponseDecoder}
31   * which enables easier client side HTTP implementation. {@link HttpClientCodec}
32   * provides additional state management for <tt>HEAD</tt> and <tt>CONNECT</tt>
33   * requests, which {@link HttpResponseDecoder} lacks.  Please refer to
34   * {@link HttpResponseDecoder} to learn what additional state management needs
35   * to be done for <tt>HEAD</tt> and <tt>CONNECT</tt> and why
36   * {@link HttpResponseDecoder} can not handle it by itself.
37   *
38   * If the {@link Channel} is closed and there are missing responses,
39   * a {@link PrematureChannelClosureException} is thrown.
40   *
41   * @see HttpServerCodec
42   */
43  public final class HttpClientCodec extends ChannelHandlerAppender implements
44          HttpClientUpgradeHandler.SourceCodec {
45  
46      /** A queue that is used for correlating a request and a response. */
47      private final Queue<HttpMethod> queue = new ArrayDeque<HttpMethod>();
48  
49      /** If true, decoding stops (i.e. pass-through) */
50      private boolean done;
51  
52      private final AtomicLong requestResponseCounter = new AtomicLong();
53      private final boolean failOnMissingResponse;
54  
55      /**
56       * Creates a new instance with the default decoder options
57       * ({@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and
58       * {@code maxChunkSize (8192)}).
59       */
60      public HttpClientCodec() {
61          this(4096, 8192, 8192, false);
62      }
63  
64      /**
65       * Creates a new instance with the specified decoder options.
66       */
67      public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
68          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, false);
69      }
70  
71      /**
72       * Creates a new instance with the specified decoder options.
73       */
74      public HttpClientCodec(
75              int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse) {
76          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, true);
77      }
78  
79      /**
80       * Creates a new instance with the specified decoder options.
81       */
82      public HttpClientCodec(
83              int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
84              boolean validateHeaders) {
85          add(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders));
86          add(new Encoder());
87          this.failOnMissingResponse = failOnMissingResponse;
88      }
89  
90      /**
91       * Upgrades to another protocol from HTTP. Removes the {@link Decoder} and {@link Encoder} from
92       * the pipeline.
93       */
94      @Override
95      public void upgradeFrom(ChannelHandlerContext ctx) {
96          ctx.pipeline().remove(Decoder.class);
97          ctx.pipeline().remove(Encoder.class);
98      }
99  
100     /**
101      * Returns the encoder of this codec.
102      */
103     public HttpRequestEncoder encoder() {
104         return handlerAt(1);
105     }
106 
107     /**
108      * Returns the decoder of this codec.
109      */
110     public HttpResponseDecoder decoder() {
111         return handlerAt(0);
112     }
113 
114     public void setSingleDecode(boolean singleDecode) {
115         decoder().setSingleDecode(singleDecode);
116     }
117 
118     public boolean isSingleDecode() {
119         return decoder().isSingleDecode();
120     }
121 
122     private final class Encoder extends HttpRequestEncoder {
123 
124         @Override
125         protected void encode(
126                 ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
127             if (msg instanceof HttpRequest && !done) {
128                 queue.offer(((HttpRequest) msg).method());
129             }
130 
131             super.encode(ctx, msg, out);
132 
133             if (failOnMissingResponse) {
134                 // check if the request is chunked if so do not increment
135                 if (msg instanceof LastHttpContent) {
136                     // increment as its the last chunk
137                     requestResponseCounter.incrementAndGet();
138                 }
139             }
140         }
141     }
142 
143     private final class Decoder extends HttpResponseDecoder {
144         Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders) {
145             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders);
146         }
147 
148         @Override
149         protected void decode(
150                 ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
151             if (done) {
152                 int readable = actualReadableBytes();
153                 if (readable == 0) {
154                     // if non is readable just return null
155                     // https://github.com/netty/netty/issues/1159
156                     return;
157                 }
158                 out.add(buffer.readBytes(readable));
159             } else {
160                 int oldSize = out.size();
161                 super.decode(ctx, buffer, out);
162                 if (failOnMissingResponse) {
163                     int size = out.size();
164                     for (int i = oldSize; i < size; i++) {
165                         decrement(out.get(i));
166                     }
167                 }
168             }
169         }
170 
171         private void decrement(Object msg) {
172             if (msg == null) {
173                 return;
174             }
175 
176             // check if it's an Header and its transfer encoding is not chunked.
177             if (msg instanceof LastHttpContent) {
178                 requestResponseCounter.decrementAndGet();
179             }
180         }
181 
182         @Override
183         protected boolean isContentAlwaysEmpty(HttpMessage msg) {
184             final int statusCode = ((HttpResponse) msg).status().code();
185             if (statusCode == 100) {
186                 // 100-continue response should be excluded from paired comparison.
187                 return true;
188             }
189 
190             // Get the getMethod of the HTTP request that corresponds to the
191             // current response.
192             HttpMethod method = queue.poll();
193 
194             char firstChar = method.name().charAt(0);
195             switch (firstChar) {
196             case 'H':
197                 // According to 4.3, RFC2616:
198                 // All responses to the HEAD request getMethod MUST NOT include a
199                 // message-body, even though the presence of entity-header fields
200                 // might lead one to believe they do.
201                 if (HttpMethod.HEAD.equals(method)) {
202                     return true;
203 
204                     // The following code was inserted to work around the servers
205                     // that behave incorrectly.  It has been commented out
206                     // because it does not work with well behaving servers.
207                     // Please note, even if the 'Transfer-Encoding: chunked'
208                     // header exists in the HEAD response, the response should
209                     // have absolutely no content.
210                     //
211                     //// Interesting edge case:
212                     //// Some poorly implemented servers will send a zero-byte
213                     //// chunk if Transfer-Encoding of the response is 'chunked'.
214                     ////
215                     //// return !msg.isChunked();
216                 }
217                 break;
218             case 'C':
219                 // Successful CONNECT request results in a response with empty body.
220                 if (statusCode == 200) {
221                     if (HttpMethod.CONNECT.equals(method)) {
222                         // Proxy connection established - Not HTTP anymore.
223                         done = true;
224                         queue.clear();
225                         return true;
226                     }
227                 }
228                 break;
229             }
230 
231             return super.isContentAlwaysEmpty(msg);
232         }
233 
234         @Override
235         public void channelInactive(ChannelHandlerContext ctx)
236                 throws Exception {
237             super.channelInactive(ctx);
238 
239             if (failOnMissingResponse) {
240                 long missingResponses = requestResponseCounter.get();
241                 if (missingResponses > 0) {
242                     ctx.fireExceptionCaught(new PrematureChannelClosureException(
243                             "channel gone inactive with " + missingResponses +
244                             " missing response(s)"));
245                 }
246             }
247         }
248     }
249 }