@@ -16,93 +16,79 @@ import NIOCore
16
16
import NIOHTTP1
17
17
18
18
extension HTTPHeaders {
19
- mutating func validate ( method : HTTPMethod , body : HTTPClient . Body ? ) throws -> RequestFramingMetadata {
20
- var metadata = RequestFramingMetadata ( connectionClose : false , body : . none )
21
-
22
- if self [ canonicalForm : " connection " ] . lazy . map ( { $0 . lowercased ( ) } ) . contains ( " close " ) {
23
- metadata . connectionClose = true
24
- }
25
-
26
- // validate transfer encoding and content length (https://tools.ietf.org/html/rfc7230#section-3.3.1)
27
- if self . contains ( name : " Transfer-Encoding " ) , self . contains ( name : " Content-Length " ) {
19
+ mutating func validateAndFixTransportFraming (
20
+ method : HTTPMethod ,
21
+ bodyLength : RequestBodyLength
22
+ ) throws -> RequestFramingMetadata {
23
+ let contentLength = self . first ( name : " Content-Length " )
24
+ let encodings = self [ canonicalForm : " Transfer-Encoding " ]
25
+
26
+ // "Transfer-Encoding" and "Content-Length" are not allowed to present at the same time (https://tools.ietf.org/html/rfc7230#section-3.3.1)
27
+ guard encodings . isEmpty || contentLength == nil else {
28
28
throw HTTPClientError . incompatibleHeaders
29
29
}
30
30
31
- var transferEncoding : String ?
32
- var contentLength : Int ?
33
- let encodings = self [ canonicalForm: " Transfer-Encoding " ] . map { $0. lowercased ( ) }
31
+ try self . validateFieldNames ( )
32
+ try Self . validateTransferEncoding ( encodings)
34
33
35
- guard !encodings. contains ( " identity " ) else {
36
- throw HTTPClientError . identityCodingIncorrectlyPresent
34
+ if contentLength != nil {
35
+ self . remove ( name: " Content-Length " )
36
+ }
37
+ if !encodings. isEmpty {
38
+ self . remove ( name: " Transfer-Encoding " )
37
39
}
38
40
39
- self . remove ( name: " Transfer-Encoding " )
40
-
41
- try self . validateFieldNames ( )
41
+ let connectionClose = self [ canonicalForm: " connection " ] . lazy. map { $0. lowercased ( ) } . contains ( " close " )
42
42
43
- guard let body = body else {
44
- self . remove ( name : " Content-Length " )
43
+ switch bodyLength {
44
+ case . fixed ( 0 ) :
45
45
// if we don't have a body we might not need to send the Content-Length field
46
46
// https://tools.ietf.org/html/rfc7230#section-3.3.2
47
47
switch method {
48
48
case . GET, . HEAD, . DELETE, . CONNECT, . TRACE:
49
49
// A user agent SHOULD NOT send a Content-Length header field when the request
50
50
// message does not contain a payload body and the method semantics do not
51
51
// anticipate such a body.
52
- return metadata
52
+ break
53
53
default :
54
54
// A user agent SHOULD send a Content-Length in a request message when
55
55
// no Transfer-Encoding is sent and the request method defines a meaning
56
56
// for an enclosed payload body.
57
57
self . add ( name: " Content-Length " , value: " 0 " )
58
- return metadata
59
58
}
60
- }
61
-
62
- if case . TRACE = method {
63
- // A client MUST NOT send a message body in a TRACE request.
64
- // https://tools.ietf.org/html/rfc7230#section-4.3.8
65
- throw HTTPClientError . traceRequestWithBody
66
- }
67
-
68
- guard ( encodings. lazy. filter { $0 == " chunked " } . count <= 1 ) else {
69
- throw HTTPClientError . chunkedSpecifiedMultipleTimes
70
- }
71
-
72
- if encodings. isEmpty {
73
- if let length = body. length {
74
- self . remove ( name: " Content-Length " )
75
- contentLength = length
76
- } else if !self . contains ( name: " Content-Length " ) {
77
- transferEncoding = " chunked "
59
+ return . init( connectionClose: connectionClose, body: . fixedSize( 0 ) )
60
+ case . fixed( let length) :
61
+ if case . TRACE = method {
62
+ // A client MUST NOT send a message body in a TRACE request.
63
+ // https://tools.ietf.org/html/rfc7230#section-4.3.8
64
+ throw HTTPClientError . traceRequestWithBody
65
+ }
66
+ if encodings. isEmpty {
67
+ self . add ( name: " Content-Length " , value: String ( length) )
68
+ return . init( connectionClose: connectionClose, body: . fixedSize( length) )
69
+ } else {
70
+ self . add ( name: " Transfer-Encoding " , value: encodings. joined ( separator: " , " ) )
71
+ return . init( connectionClose: connectionClose, body: . stream)
72
+ }
73
+ case . dynamic:
74
+ if case . TRACE = method {
75
+ // A client MUST NOT send a message body in a TRACE request.
76
+ // https://tools.ietf.org/html/rfc7230#section-4.3.8
77
+ throw HTTPClientError . traceRequestWithBody
78
78
}
79
- } else {
80
- self . remove ( name: " Content-Length " )
81
79
82
- transferEncoding = encodings. joined ( separator: " , " )
83
- if !encodings. contains ( " chunked " ) {
84
- guard let length = body. length else {
85
- throw HTTPClientError . contentLengthMissing
86
- }
87
- contentLength = length
80
+ if encodings. isEmpty && contentLength == nil {
81
+ // if a user forgot to specify a Content-Length and Transfer-Encoding, we will set it for them
82
+ self . add ( name: " Transfer-Encoding " , value: " chunked " )
83
+ } else {
84
+ self . add ( name: " Transfer-Encoding " , value: encodings. joined ( separator: " , " ) )
88
85
}
89
- }
90
86
91
- // add headers if required
92
- if let enc = transferEncoding {
93
- self . add ( name: " Transfer-Encoding " , value: enc)
94
- metadata. body = . stream
95
- } else if let length = contentLength {
96
- // A sender MUST NOT send a Content-Length header field in any message
97
- // that contains a Transfer-Encoding header field.
98
- self . add ( name: " Content-Length " , value: String ( length) )
99
- metadata. body = . fixedSize( length)
87
+ return . init( connectionClose: connectionClose, body: . stream)
100
88
}
101
-
102
- return metadata
103
89
}
104
90
105
- private func validateFieldNames( ) throws {
91
+ func validateFieldNames( ) throws {
106
92
let invalidFieldNames = self . compactMap { ( name, _) -> String ? in
107
93
let satisfy = name. utf8. allSatisfy { ( char) -> Bool in
108
94
switch char {
@@ -137,4 +123,33 @@ extension HTTPHeaders {
137
123
throw HTTPClientError . invalidHeaderFieldNames ( invalidFieldNames)
138
124
}
139
125
}
126
+
127
+ static func validateTransferEncoding< Encodings> (
128
+ _ encodings: Encodings
129
+ ) throws where Encodings: Sequence , Encodings. Element: StringProtocol {
130
+ let encodings = encodings. map { $0. lowercased ( ) }
131
+
132
+ guard !encodings. contains ( " identity " ) else {
133
+ throw HTTPClientError . identityCodingIncorrectlyPresent
134
+ }
135
+
136
+ // If `Transfer-Encoding` is specified, `chunked` needs to be the last encoding and should not be specified multiple times
137
+ // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
138
+ let chunkedEncodingCount = encodings. lazy. filter { $0 == " chunked " } . count
139
+ switch chunkedEncodingCount {
140
+ case 0 :
141
+ if !encodings. isEmpty {
142
+ throw HTTPClientError . transferEncodingSpecifiedButChunkedIsNotTheFinalEncoding
143
+ }
144
+ case 1 :
145
+ guard encodings. last == " chunked " else {
146
+ throw HTTPClientError . transferEncodingSpecifiedButChunkedIsNotTheFinalEncoding
147
+ }
148
+ case 2 ... :
149
+ throw HTTPClientError . chunkedSpecifiedMultipleTimes
150
+ default :
151
+ // unreachable because `chunkedEncodingCount` is guaranteed to be positive
152
+ preconditionFailure ( )
153
+ }
154
+ }
140
155
}
0 commit comments