View Javadoc
1   /*
2    * Copyright 2016 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    *   https://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.channel.ChannelDuplexHandler;
19  import io.netty.channel.ChannelFutureListener;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.ChannelPipeline;
22  import io.netty.channel.ChannelPromise;
23  
24  import static io.netty.handler.codec.http.HttpUtil.*;
25  
26  /**
27   * HttpServerKeepAliveHandler helps close persistent connections when appropriate.
28   * <p>
29   * The server channel is expected to set the proper 'Connection' header if it can handle persistent connections. {@link
30   * HttpServerKeepAliveHandler} will automatically close the channel for any LastHttpContent that corresponds to a client
31   * request for closing the connection, or if the HttpResponse associated with that LastHttpContent requested closing the
32   * connection or didn't have a self defined message length.
33   * <p>
34   * Since {@link HttpServerKeepAliveHandler} expects {@link HttpObject}s it should be added after {@link HttpServerCodec}
35   * but before any other handlers that might send a {@link HttpResponse}. <blockquote>
36   * <pre>
37   *  {@link ChannelPipeline} p = ...;
38   *  ...
39   *  p.addLast("serverCodec", new {@link HttpServerCodec}());
40   *  p.addLast("httpKeepAlive", <b>new {@link HttpServerKeepAliveHandler}()</b>);
41   *  p.addLast("aggregator", new {@link HttpObjectAggregator}(1048576));
42   *  ...
43   *  p.addLast("handler", new HttpRequestHandler());
44   *  </pre>
45   * </blockquote>
46   */
47  public class HttpServerKeepAliveHandler extends ChannelDuplexHandler {
48      private static final String MULTIPART_PREFIX = "multipart";
49  
50      private boolean persistentConnection = true;
51      // Track pending responses to support client pipelining: https://tools.ietf.org/html/rfc7230#section-6.3.2
52      private int pendingResponses;
53  
54      @Override
55      public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
56          // read message and track if it was keepAlive
57          if (msg instanceof HttpRequest) {
58              final HttpRequest request = (HttpRequest) msg;
59              if (persistentConnection) {
60                  pendingResponses += 1;
61                  persistentConnection = isKeepAlive(request);
62              }
63          }
64          super.channelRead(ctx, msg);
65      }
66  
67      @Override
68      public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
69          // modify message on way out to add headers if needed
70          if (msg instanceof HttpResponse) {
71              final HttpResponse response = (HttpResponse) msg;
72              trackResponse(response);
73              // Assume the response writer knows if they can persist or not and sets isKeepAlive on the response
74              if (!isKeepAlive(response) || !isSelfDefinedMessageLength(response)) {
75                  // No longer keep alive as the client can't tell when the message is done unless we close connection
76                  pendingResponses = 0;
77                  persistentConnection = false;
78              }
79              // Server might think it can keep connection alive, but we should fix response header if we know better
80              if (!shouldKeepAlive()) {
81                  setKeepAlive(response, false);
82              }
83          }
84          if (msg instanceof LastHttpContent && !shouldKeepAlive()) {
85              promise = promise.unvoid().addListener(ChannelFutureListener.CLOSE);
86          }
87          super.write(ctx, msg, promise);
88      }
89  
90      private void trackResponse(HttpResponse response) {
91          if (!isInformational(response)) {
92              pendingResponses -= 1;
93          }
94      }
95  
96      private boolean shouldKeepAlive() {
97          return pendingResponses != 0 || persistentConnection;
98      }
99  
100     /**
101      * Keep-alive only works if the client can detect when the message has ended without relying on the connection being
102      * closed.
103      * <p>
104      * <ul>
105      *     <li>See <a href="https://tools.ietf.org/html/rfc7230#section-6.3"/></li>
106      *     <li>See <a href="https://tools.ietf.org/html/rfc7230#section-3.3.2"/></li>
107      *     <li>See <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3"/></li>
108      * </ul>
109      *
110      * @param response The HttpResponse to check
111      *
112      * @return true if the response has a self defined message length.
113      */
114     private static boolean isSelfDefinedMessageLength(HttpResponse response) {
115         return isContentLengthSet(response) || isTransferEncodingChunked(response) || isMultipart(response) ||
116                isInformational(response) || response.status().code() == HttpResponseStatus.NO_CONTENT.code();
117     }
118 
119     private static boolean isInformational(HttpResponse response) {
120         return response.status().codeClass() == HttpStatusClass.INFORMATIONAL;
121     }
122 
123     private static boolean isMultipart(HttpResponse response) {
124         String contentType = response.headers().get(HttpHeaderNames.CONTENT_TYPE);
125         return contentType != null &&
126                contentType.regionMatches(true, 0, MULTIPART_PREFIX, 0, MULTIPART_PREFIX.length());
127     }
128 }