forked from twitter/finagle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ResponseConformanceFilter.scala
167 lines (149 loc) · 6.88 KB
/
ResponseConformanceFilter.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package com.twitter.finagle.http.codec
import com.twitter.finagle.{Service, SimpleFilter}
import com.twitter.finagle.http.{Fields, Method, Request, Response, Status}
import com.twitter.finagle.http.Status._
import com.twitter.logging.Logger
import com.twitter.util.Future
/**
* Ensure that the `Response` is a legal response for the request that generated it
*
* This attempts to ensure that the HTTP/1.x protocol doesn't get corrupted and
* result in interesting errors at the peers end which can be difficult to diagnose.
*
* TODO: 1xx, 204, and 304 responses are not allowed to have bodies which is
* not currently enforced https://tools.ietf.org/html/rfc7230#section-3.3.3
*/
private[codec] object ResponseConformanceFilter extends SimpleFilter[Request, Response] {
private[this] val logger = Logger.get(this.getClass.getName)
override def apply(
request: Request,
service: Service[Request, Response]
): Future[Response] = {
service(request).map { rep =>
// Because our Response is mutable we can perform all the actions we
// need as side effects. This is not necessarily a good thing.
validate(request, rep)
rep
}
}
/**
* Performs common cleanup tasks to ensure compliance with the HTTP specification
*/
private[this] def validate(req: Request, rep: Response): Unit = {
if (req.method == Method.Head) {
handleHeadResponse(req, rep)
} else if (mustNotIncludeMessageBody(rep.status)) {
handleNoMessageResponse(rep)
} else if (rep.isChunked) {
handleChunkedResponse(rep)
} else {
handleFullyBufferedResponse(rep)
}
}
/**
* 1. To conform to the RFC, a message body is removed if a status code is
* either 1xx, 204 or 304.
*
* RFC7230 section-3.3: (https://tools.ietf.org/html/rfc7230#section-3.3)
* "All 1xx (Informational), 204 (No Content), and 304 (Not Modified) responses
* do not include a message body."
*
* 2. Additionally, a Content-Length header field is dropped for 1xx and 204 responses
* as described in RFC7230 section-3.3.2. It, however, is allowed to send a Content-Length
* header field in a 304 response. To follow the section, we don't remove the header field
* from a 304 response but its value is not checked nor corrected.
*
* RFC7230 section-3.3.2: (https://tools.ietf.org/html/rfc7230#section-3.3.2)
* "A server MUST NOT send a Content-Length header field in any response with a status code
* of 1xx (Informational) or 204 (No Content)."
*
* "A server MAY send a Content-Length header field in a 304 (Not Modified) response to
* a conditional GET request (Section 4.1 of [RFC7232]); a server MUST NOT send Content-Length
* in such a response unless its field-value equals the decimal number of octets that would have
* been sent in the payload body of a 200 (OK) response to the same request."
*/
private[this] def handleNoMessageResponse(rep: Response): Unit = {
val contentLength = rep.length
if (contentLength > 0) {
rep.clearContent()
logger.error(
"Response with a status code of %d must not have a body-message but it has " +
"a %d-byte payload, thus the content has been removed.",
rep.statusCode, contentLength)
}
if (rep.status != NotModified && mustNotIncludeMessageBody(rep.status)) {
if (rep.contentLength.isDefined) {
rep.headerMap.remove(Fields.ContentLength)
logger.error(
"Response with a status code of %d must not have a Content-Length header field " +
"thus the field has been removed.",
rep.statusCode)
}
}
}
private def mustNotIncludeMessageBody(status: Status): Boolean = status match {
case NoContent | NotModified => true
case _ if 100 <= status.code && status.code < 200 => true
case _ => false
}
private[this] def handleFullyBufferedResponse(rep: Response): Unit = {
// Set the Content-Length header to the length of the body
// if it is not already defined. Examples of reasons that a service might
// define a Content-Length header to something other than the actual message
// length include responding to a HEAD request with what the length that the
// body would have been had it been a GET request.
if (rep.contentLength.isEmpty) {
rep.contentLength = rep.content.length
}
}
private[this] def handleChunkedResponse(rep: Response): Unit = {
rep.headerMap.set(Fields.TransferEncoding, "chunked")
// We remove any content-length headers because "A sender MUST NOT
// send a Content-Length header field in any message that contains
// a Transfer-Encoding header field."
// https://tools.ietf.org/html/rfc7230#section-3.3.2
rep.headerMap.remove(Fields.ContentLength)
}
/**
* Ensure consistency of the [[Response]] for HEAD requests
*
* RFC-7231: (https://tools.ietf.org/html/rfc7231#section-4.3.2)
* "The HEAD method is identical to GET except that the server MUST NOT
* send a message body in the response (i.e., the response terminates at
* the end of the header section)."
*
* Because we don't control encoding ourselves, the `Response` is prepared
* such that it will be well formed if the downstream encoder behaves as follows:
* - The outbound transport MUST NOT add content-length or content-encoding
* headers as part of the message encoding process.
* - Non-chunked messages MUST be interpreted as the final element of a HTTP
* response.
*/
private[this] def handleHeadResponse(request: Request, response: Response): Unit = {
// Netty4 cant encode a HEAD response with a 'transfer-encoding: chunked' header, so we omit
// payload headers content-encoding from chunked responses as allowed by RFC-7231 section 4.3.2
response.headerMap.remove(Fields.TransferEncoding)
if (response.isChunked) {
// This was intended to be a chunked response, so we "MUST NOT" include a content-length
// header: https://tools.ietf.org/html/rfc7230#section-3.3.2
response.headerMap.remove(Fields.ContentLength)
// Make sure we don't leave any writers hanging in case they simply called `close`.
response.reader.discard()
// By setting the response to non-chunked, it will be written as a complete
// response by the HTTP pipeline
response.setChunked(false)
}
if (!response.content.isEmpty) {
logger.error(
"Received response to HEAD request (%s) that contained a static body of length %d. " +
"Discarding body. If this is desired behavior, consider adding HeadFilter to your service",
request.toString, response.content.length)
// Might as well salvage a content length header
if (response.contentLength.isEmpty) {
response.contentLength = response.content.length
}
// clear the content from the body: otherwise it's a protocol error
response.clearContent()
}
}
}