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.ChannelHandlerContext;
21  import io.netty.channel.CombinedChannelDuplexHandler;
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
44          extends CombinedChannelDuplexHandler<HttpResponseDecoder, HttpRequestEncoder> {
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      public void setSingleDecode(boolean singleDecode) {
65          inboundHandler().setSingleDecode(singleDecode);
66      }
67  
68      public boolean isSingleDecode() {
69          return inboundHandler().isSingleDecode();
70      }
71  
72      /**
73       * Creates a new instance with the specified decoder options.
74       */
75      public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
76          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, false);
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          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, true);
85      }
86  
87      /**
88       * Creates a new instance with the specified decoder options.
89       */
90      public HttpClientCodec(
91              int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
92              boolean validateHeaders) {
93          init(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders), new Encoder());
94          this.failOnMissingResponse = failOnMissingResponse;
95      }
96  
97      /**
98       * Creates a new instance with the specified decoder options.
99       */
100     public HttpClientCodec(
101             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
102             boolean validateHeaders, int initialBufferSize) {
103         init(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize),
104              new Encoder());
105         this.failOnMissingResponse = failOnMissingResponse;
106     }
107 
108     private final class Encoder extends HttpRequestEncoder {
109 
110         @Override
111         protected void encode(
112                 ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
113             if (msg instanceof HttpRequest && !done) {
114                 queue.offer(((HttpRequest) msg).getMethod());
115             }
116 
117             super.encode(ctx, msg, out);
118 
119             if (failOnMissingResponse) {
120                 // check if the request is chunked if so do not increment
121                 if (msg instanceof LastHttpContent) {
122                     // increment as its the last chunk
123                     requestResponseCounter.incrementAndGet();
124                 }
125             }
126         }
127     }
128 
129     private final class Decoder extends HttpResponseDecoder {
130         Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders) {
131             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders);
132         }
133 
134         Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders,
135                 int initialBufferSize) {
136             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize);
137         }
138 
139         @Override
140         protected void decode(
141                 ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
142             if (done) {
143                 int readable = actualReadableBytes();
144                 if (readable == 0) {
145                     // if non is readable just return null
146                     // https://github.com/netty/netty/issues/1159
147                     return;
148                 }
149                 out.add(buffer.readBytes(readable));
150             } else {
151                 int oldSize = out.size();
152                 super.decode(ctx, buffer, out);
153                 if (failOnMissingResponse) {
154                     int size = out.size();
155                     for (int i = oldSize; i < size; i++) {
156                         decrement(out.get(i));
157                     }
158                 }
159             }
160         }
161 
162         private void decrement(Object msg) {
163             if (msg == null) {
164                 return;
165             }
166 
167             // check if it's an Header and its transfer encoding is not chunked.
168             if (msg instanceof LastHttpContent) {
169                 requestResponseCounter.decrementAndGet();
170             }
171         }
172 
173         @Override
174         protected boolean isContentAlwaysEmpty(HttpMessage msg) {
175             final int statusCode = ((HttpResponse) msg).getStatus().code();
176             if (statusCode == 100 || statusCode == 101) {
177                 // 100-continue and 101 switching protocols response should be excluded from paired comparison.
178                 // Just delegate to super method which has all the needed handling.
179                 return super.isContentAlwaysEmpty(msg);
180             }
181 
182             // Get the getMethod of the HTTP request that corresponds to the
183             // current response.
184             HttpMethod method = queue.poll();
185 
186             char firstChar = method.name().charAt(0);
187             switch (firstChar) {
188             case 'H':
189                 // According to 4.3, RFC2616:
190                 // All responses to the HEAD request method MUST NOT include a
191                 // message-body, even though the presence of entity-header fields
192                 // might lead one to believe they do.
193                 if (HttpMethod.HEAD.equals(method)) {
194                     return true;
195 
196                     // The following code was inserted to work around the servers
197                     // that behave incorrectly.  It has been commented out
198                     // because it does not work with well behaving servers.
199                     // Please note, even if the 'Transfer-Encoding: chunked'
200                     // header exists in the HEAD response, the response should
201                     // have absolutely no content.
202                     //
203                     //// Interesting edge case:
204                     //// Some poorly implemented servers will send a zero-byte
205                     //// chunk if Transfer-Encoding of the response is 'chunked'.
206                     ////
207                     //// return !msg.isChunked();
208                 }
209                 break;
210             case 'C':
211                 // Successful CONNECT request results in a response with empty body.
212                 if (statusCode == 200) {
213                     if (HttpMethod.CONNECT.equals(method)) {
214                         // Proxy connection established - Not HTTP anymore.
215                         done = true;
216                         queue.clear();
217                         return true;
218                     }
219                 }
220                 break;
221             }
222 
223             return super.isContentAlwaysEmpty(msg);
224         }
225 
226         @Override
227         public void channelInactive(ChannelHandlerContext ctx)
228                 throws Exception {
229             super.channelInactive(ctx);
230 
231             if (failOnMissingResponse) {
232                 long missingResponses = requestResponseCounter.get();
233                 if (missingResponses > 0) {
234                     ctx.fireExceptionCaught(new PrematureChannelClosureException(
235                             "channel gone inactive with " + missingResponses +
236                             " missing response(s)"));
237                 }
238             }
239         }
240     }
241 }