Skip to content

Commit afad35f

Browse files
committed
always overwrite Transport-Encoding and Content-Length
1 parent 242e392 commit afad35f

7 files changed

+104
-179
lines changed

Sources/AsyncHTTPClient/HTTPClient.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -883,8 +883,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
883883
case remoteConnectionClosed
884884
case cancelled
885885
case identityCodingIncorrectlyPresent
886+
@available(*, deprecated)
886887
case chunkedSpecifiedMultipleTimes
887-
case transferEncodingSpecifiedButChunkedIsNotTheFinalEncoding
888888
case invalidProxyResponse
889889
case contentLengthMissing
890890
case proxyAuthenticationRequired
@@ -895,6 +895,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
895895
case invalidHeaderFieldNames([String])
896896
case bodyLengthMismatch
897897
case writeAfterRequestSent
898+
@available(*, deprecated)
898899
case incompatibleHeaders
899900
case connectTimeout
900901
case socksHandshakeTimeout
@@ -938,9 +939,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
938939
/// Request contains invalid identity encoding.
939940
public static let identityCodingIncorrectlyPresent = HTTPClientError(code: .identityCodingIncorrectlyPresent)
940941
/// Request contains multiple chunks definitions.
942+
@available(*, deprecated)
941943
public static let chunkedSpecifiedMultipleTimes = HTTPClientError(code: .chunkedSpecifiedMultipleTimes)
942-
/// Request specifies `Transfer-Encoding` but `chunked` is not the final encoding
943-
public static let transferEncodingSpecifiedButChunkedIsNotTheFinalEncoding = HTTPClientError(code: .transferEncodingSpecifiedButChunkedIsNotTheFinalEncoding)
944944
/// Proxy response was invalid.
945945
public static let invalidProxyResponse = HTTPClientError(code: .invalidProxyResponse)
946946
/// Request does not contain `Content-Length` header.
@@ -962,6 +962,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
962962
/// Body part was written after request was fully sent.
963963
public static let writeAfterRequestSent = HTTPClientError(code: .writeAfterRequestSent)
964964
/// Incompatible headers specified, for example `Transfer-Encoding` and `Content-Length`.
965+
@available(*, deprecated)
965966
public static let incompatibleHeaders = HTTPClientError(code: .incompatibleHeaders)
966967
/// Creating a new tcp connection timed out
967968
public static let connectTimeout = HTTPClientError(code: .connectTimeout)

Sources/AsyncHTTPClient/HTTPHandler.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ extension HTTPClient {
308308
head.headers.add(name: "host", value: host)
309309
}
310310

311-
let metadata = try head.headers.validateAndFixTransportFraming(method: self.method, bodyLength: .init(self.body))
311+
let metadata = try head.headers.validateAndSetTransportFraming(method: self.method, bodyLength: .init(self.body))
312312

313313
return (head, metadata)
314314
}

Sources/AsyncHTTPClient/RequestValidation.swift

+41-85
Original file line numberDiff line numberDiff line change
@@ -16,79 +16,35 @@ import NIOCore
1616
import NIOHTTP1
1717

1818
extension HTTPHeaders {
19-
mutating func validateAndFixTransportFraming(
19+
mutating func validateAndSetTransportFraming(
2020
method: HTTPMethod,
2121
bodyLength: RequestBodyLength
2222
) 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-
throw HTTPClientError.incompatibleHeaders
29-
}
30-
3123
try self.validateFieldNames()
32-
try Self.validateTransferEncoding(encodings)
33-
34-
if contentLength != nil {
35-
self.remove(name: "Content-Length")
36-
}
37-
if !encodings.isEmpty {
38-
self.remove(name: "Transfer-Encoding")
39-
}
40-
41-
let connectionClose = self[canonicalForm: "connection"].lazy.map { $0.lowercased() }.contains("close")
42-
43-
switch bodyLength {
44-
case .fixed(0):
45-
// if we don't have a body we might not need to send the Content-Length field
46-
// https://tools.ietf.org/html/rfc7230#section-3.3.2
47-
switch method {
48-
case .GET, .HEAD, .DELETE, .CONNECT, .TRACE:
49-
// A user agent SHOULD NOT send a Content-Length header field when the request
50-
// message does not contain a payload body and the method semantics do not
51-
// anticipate such a body.
24+
25+
if case .TRACE = method {
26+
switch bodyLength {
27+
case .fixed(length: 0):
5228
break
53-
default:
54-
// A user agent SHOULD send a Content-Length in a request message when
55-
// no Transfer-Encoding is sent and the request method defines a meaning
56-
// for an enclosed payload body.
57-
self.add(name: "Content-Length", value: "0")
58-
}
59-
return .init(connectionClose: connectionClose, body: .fixedSize(0))
60-
case .fixed(let length):
61-
if case .TRACE = method {
29+
case .dynamic, .fixed(_):
6230
// A client MUST NOT send a message body in a TRACE request.
6331
// https://tools.ietf.org/html/rfc7230#section-4.3.8
6432
throw HTTPClientError.traceRequestWithBody
6533
}
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-
}
34+
}
35+
36+
self.setTransportFraming(method: method, bodyLength: bodyLength)
37+
38+
let connectionClose = self[canonicalForm: "connection"].lazy.map { $0.lowercased() }.contains("close")
39+
switch bodyLength {
7340
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-
}
79-
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: ", "))
85-
}
86-
8741
return .init(connectionClose: connectionClose, body: .stream)
42+
case .fixed(let length):
43+
return .init(connectionClose: connectionClose, body: .fixedSize(length))
8844
}
8945
}
9046

91-
func validateFieldNames() throws {
47+
private func validateFieldNames() throws {
9248
let invalidFieldNames = self.compactMap { (name, _) -> String? in
9349
let satisfy = name.utf8.allSatisfy { (char) -> Bool in
9450
switch char {
@@ -123,33 +79,33 @@ extension HTTPHeaders {
12379
throw HTTPClientError.invalidHeaderFieldNames(invalidFieldNames)
12480
}
12581
}
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
82+
private mutating func setTransportFraming(
83+
method: HTTPMethod,
84+
bodyLength: RequestBodyLength
85+
) {
86+
self.remove(name: "Content-Length")
87+
self.remove(name: "Transfer-Encoding")
88+
89+
switch bodyLength {
90+
case .fixed(0):
91+
// if we don't have a body we might not need to send the Content-Length field
92+
// https://tools.ietf.org/html/rfc7230#section-3.3.2
93+
switch method {
94+
case .GET, .HEAD, .DELETE, .CONNECT, .TRACE:
95+
// A user agent SHOULD NOT send a Content-Length header field when the request
96+
// message does not contain a payload body and the method semantics do not
97+
// anticipate such a body.
98+
break
99+
default:
100+
// A user agent SHOULD send a Content-Length in a request message when
101+
// no Transfer-Encoding is sent and the request method defines a meaning
102+
// for an enclosed payload body.
103+
self.add(name: "Content-Length", value: "0")
147104
}
148-
case 2...:
149-
throw HTTPClientError.chunkedSpecifiedMultipleTimes
150-
default:
151-
// unreachable because `chunkedEncodingCount` is guaranteed to be positive
152-
preconditionFailure()
105+
case .fixed(let length):
106+
self.add(name: "Content-Length", value: String(length))
107+
case .dynamic:
108+
self.add(name: "Transfer-Encoding", value: "chunked")
153109
}
154110
}
155111
}

Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ extension HTTPClientTests {
128128
("testErrorAfterCloseWhileBackpressureExerted", testErrorAfterCloseWhileBackpressureExerted),
129129
("testRequestSpecificTLS", testRequestSpecificTLS),
130130
("testConnectionPoolSizeConfigValueIsRespected", testConnectionPoolSizeConfigValueIsRespected),
131-
("testRequestWithHeaderTransferEncodingIdentityFails", testRequestWithHeaderTransferEncodingIdentityFails),
131+
("testRequestWithHeaderTransferEncodingIdentityDoesNotFail", testRequestWithHeaderTransferEncodingIdentityDoesNotFail),
132132
]
133133
}
134134
}

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

+6-5
Original file line numberDiff line numberDiff line change
@@ -2941,22 +2941,23 @@ class HTTPClientTests: XCTestCase {
29412941
XCTAssertEqual(httpBin.createdConnections, poolSize)
29422942
}
29432943

2944-
func testRequestWithHeaderTransferEncodingIdentityFails() {
2944+
func testRequestWithHeaderTransferEncodingIdentityDoesNotFail() {
29452945
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
29462946
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
29472947

29482948
let client = HTTPClient(eventLoopGroupProvider: .shared(group))
29492949
defer { XCTAssertNoThrow(try client.syncShutdown()) }
2950+
2951+
let httpBin = HTTPBin()
2952+
defer { XCTAssertNoThrow(try httpBin.shutdown()) }
29502953

2951-
guard var request = try? Request(url: "http://localhost/get") else {
2954+
guard var request = try? Request(url: "http://127.0.0.1:\(httpBin.port)/get") else {
29522955
return XCTFail("Expected to have a request here.")
29532956
}
29542957
request.headers.add(name: "X-Test-Header", value: "X-Test-Value")
29552958
request.headers.add(name: "Transfer-Encoding", value: "identity")
29562959
request.body = .string("1234")
29572960

2958-
XCTAssertThrowsError(try client.execute(request: request).wait()) {
2959-
XCTAssertEqual($0 as? HTTPClientError, .identityCodingIncorrectlyPresent)
2960-
}
2961+
XCTAssertNoThrow(try client.execute(request: request).wait())
29612962
}
29622963
}

Tests/AsyncHTTPClientTests/RequestValidationTests+XCTest.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ extension RequestValidationTests {
4545
("testHostHeaderIsSetCorrectlyInCreateRequestHead", testHostHeaderIsSetCorrectlyInCreateRequestHead),
4646
("testTraceMethodIsNotAllowedToHaveAFixedLengthBody", testTraceMethodIsNotAllowedToHaveAFixedLengthBody),
4747
("testTraceMethodIsNotAllowedToHaveADynamicLengthBody", testTraceMethodIsNotAllowedToHaveADynamicLengthBody),
48-
("testTransferEncodingsArePreservedIfBodyLengthIsFixed", testTransferEncodingsArePreservedIfBodyLengthIsFixed),
49-
("testTransferEncodingsArePreservedIfBodyLengthIsDynamic", testTransferEncodingsArePreservedIfBodyLengthIsDynamic),
50-
("testTransferEncodingValidation", testTransferEncodingValidation),
48+
("testTransferEncodingsAreOverwritteIfBodyLengthIsFixed", testTransferEncodingsAreOverwritteIfBodyLengthIsFixed),
49+
("testTransferEncodingsAreOverwrittenIfBodyLengthIsDynamic", testTransferEncodingsAreOverwrittenIfBodyLengthIsDynamic),
5150
]
5251
}
5352
}

0 commit comments

Comments
 (0)