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 }