@@ -16,93 +16,79 @@ import NIOCore
1616import NIOHTTP1
1717
1818extension 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 {
2828 throw HTTPClientError . incompatibleHeaders
2929 }
3030
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)
3433
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 " )
3739 }
3840
39- self . remove ( name: " Transfer-Encoding " )
40-
41- try self . validateFieldNames ( )
41+ let connectionClose = self [ canonicalForm: " connection " ] . lazy. map { $0. lowercased ( ) } . contains ( " close " )
4242
43- guard let body = body else {
44- self . remove ( name : " Content-Length " )
43+ switch bodyLength {
44+ case . fixed ( 0 ) :
4545 // if we don't have a body we might not need to send the Content-Length field
4646 // https://tools.ietf.org/html/rfc7230#section-3.3.2
4747 switch method {
4848 case . GET, . HEAD, . DELETE, . CONNECT, . TRACE:
4949 // A user agent SHOULD NOT send a Content-Length header field when the request
5050 // message does not contain a payload body and the method semantics do not
5151 // anticipate such a body.
52- return metadata
52+ break
5353 default :
5454 // A user agent SHOULD send a Content-Length in a request message when
5555 // no Transfer-Encoding is sent and the request method defines a meaning
5656 // for an enclosed payload body.
5757 self . add ( name: " Content-Length " , value: " 0 " )
58- return metadata
5958 }
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
7878 }
79- } else {
80- self . remove ( name: " Content-Length " )
8179
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: " , " ) )
8885 }
89- }
9086
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)
10088 }
101-
102- return metadata
10389 }
10490
105- private func validateFieldNames( ) throws {
91+ func validateFieldNames( ) throws {
10692 let invalidFieldNames = self . compactMap { ( name, _) -> String ? in
10793 let satisfy = name. utf8. allSatisfy { ( char) -> Bool in
10894 switch char {
@@ -137,4 +123,33 @@ extension HTTPHeaders {
137123 throw HTTPClientError . invalidHeaderFieldNames ( invalidFieldNames)
138124 }
139125 }
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+ }
140155}
0 commit comments