Skip to content

Commit 9a431d1

Browse files
committed
wip
1 parent 995a98b commit 9a431d1

File tree

5 files changed

+187
-35
lines changed

5 files changed

+187
-35
lines changed

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
960960
case traceRequestWithBody
961961
case invalidHeaderFieldNames([String])
962962
case bodyLengthMismatch
963+
case incompatibleHeaders
963964
}
964965

965966
private var code: Code
@@ -1012,4 +1013,6 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
10121013
public static func invalidHeaderFieldNames(_ names: [String]) -> HTTPClientError { return HTTPClientError(code: .invalidHeaderFieldNames(names)) }
10131014
/// Body length is not equal to `Content-Length`.
10141015
public static let bodyLengthMismatch = HTTPClientError(code: .bodyLengthMismatch)
1016+
/// Incompatible headers specified, for example `Transfer-Encoding` and `Content-Length`.
1017+
public static let incompatibleHeaders = HTTPClientError(code: .incompatibleHeaders)
10151018
}

Sources/AsyncHTTPClient/RequestValidation.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import NIOHTTP1
1818
extension HTTPHeaders {
1919
mutating func validate(method: HTTPMethod, body: HTTPClient.Body?) throws {
2020
// validate transfer encoding and content length (https://tools.ietf.org/html/rfc7230#section-3.3.1)
21+
if self.contains(name: "Transfer-Encoding") && self.contains(name: "Content-Length") {
22+
throw HTTPClientError.incompatibleHeaders
23+
}
24+
2125
var transferEncoding: String?
2226
var contentLength: Int?
2327
let encodings = self[canonicalForm: "Transfer-Encoding"].map { $0.lowercased() }

Tests/AsyncHTTPClientTests/HTTPClientTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2281,7 +2281,7 @@ class HTTPClientTests: XCTestCase {
22812281

22822282
var request = try HTTPClient.Request(url: "http://localhost:\(server.serverPort)/")
22832283
request.body = .stream { writer in
2284-
writer.write(.byteBuffer(ByteBuffer.of(string: "1234")))
2284+
writer.write(.byteBuffer(ByteBuffer(string: "1234")))
22852285
}
22862286

22872287
let future = client.execute(request: request)

Tests/AsyncHTTPClientTests/RequestValidationTests+XCTest.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,10 @@ extension RequestValidationTests {
2828
("testContentLengthHeaderIsRemovedFromGETIfNoBody", testContentLengthHeaderIsRemovedFromGETIfNoBody),
2929
("testContentLengthHeaderIsAddedToPOSTAndPUTWithNoBody", testContentLengthHeaderIsAddedToPOSTAndPUTWithNoBody),
3030
("testContentLengthHeaderIsChangedIfBodyHasDifferentLength", testContentLengthHeaderIsChangedIfBodyHasDifferentLength),
31-
("testChunkedEncodingDoesNotHaveContentLengthHeader", testChunkedEncodingDoesNotHaveContentLengthHeader),
3231
("testTRACERequestMustNotHaveBody", testTRACERequestMustNotHaveBody),
3332
("testGET_HEAD_DELETE_CONNECTRequestCanHaveBody", testGET_HEAD_DELETE_CONNECTRequestCanHaveBody),
3433
("testInvalidHeaderFieldNames", testInvalidHeaderFieldNames),
3534
("testValidHeaderFieldNames", testValidHeaderFieldNames),
36-
("testMultipleContentLengthOnNilStreamLength", testMultipleContentLengthOnNilStreamLength),
3735
]
3836
}
3937
}

Tests/AsyncHTTPClientTests/RequestValidationTests.swift

Lines changed: 179 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,14 @@ class RequestValidationTests: XCTestCase {
4242
XCTAssertEqual(headers.first(name: "Content-Length"), "200")
4343
}
4444

45-
func testChunkedEncodingDoesNotHaveContentLengthHeader() {
46-
var headers = HTTPHeaders([
47-
("Content-Length", "200"),
48-
("Transfer-Encoding", "chunked"),
49-
])
50-
var buffer = ByteBufferAllocator().buffer(capacity: 200)
51-
buffer.writeBytes([UInt8](repeating: 12, count: 200))
52-
XCTAssertNoThrow(try headers.validate(method: .PUT, body: .byteBuffer(buffer)))
53-
54-
// https://tools.ietf.org/html/rfc7230#section-3.3.2
55-
// A sender MUST NOT send a Content-Length header field in any message
56-
// that contains a Transfer-Encoding header field.
57-
58-
XCTAssertNil(headers.first(name: "Content-Length"))
59-
XCTAssertEqual(headers.first(name: "Transfer-Encoding"), "chunked")
60-
}
61-
6245
func testTRACERequestMustNotHaveBody() {
63-
var headers = HTTPHeaders([
64-
("Content-Length", "200"),
65-
("Transfer-Encoding", "chunked"),
66-
])
67-
var buffer = ByteBufferAllocator().buffer(capacity: 200)
68-
buffer.writeBytes([UInt8](repeating: 12, count: 200))
69-
XCTAssertThrowsError(try headers.validate(method: .TRACE, body: .byteBuffer(buffer))) {
70-
XCTAssertEqual($0 as? HTTPClientError, .traceRequestWithBody)
46+
for header in [("Content-Length", "200"), ("Transfer-Encoding", "chunked")] {
47+
var headers = HTTPHeaders([header])
48+
var buffer = ByteBufferAllocator().buffer(capacity: 200)
49+
buffer.writeBytes([UInt8](repeating: 12, count: 200))
50+
XCTAssertThrowsError(try headers.validate(method: .TRACE, body: .byteBuffer(buffer))) {
51+
XCTAssertEqual($0 as? HTTPClientError, .traceRequestWithBody)
52+
}
7153
}
7254
}
7355

@@ -105,13 +87,178 @@ class RequestValidationTests: XCTestCase {
10587
XCTAssertNoThrow(try headers.validate(method: .GET, body: nil))
10688
}
10789

108-
func testMultipleContentLengthOnNilStreamLength() {
109-
var headers = HTTPHeaders([("Content-Length", "1"), ("Content-Length", "2")])
110-
var buffer = ByteBufferAllocator().buffer(capacity: 10)
111-
buffer.writeBytes([UInt8](repeating: 12, count: 10))
112-
let body: HTTPClient.Body = .stream { writer in
113-
writer.write(.byteBuffer(buffer))
90+
// MARK: - Content-Length/Transfer-Encoding Matrix
91+
92+
// Method kind User sets Body Expectation
93+
// ----------------------------------------------------------------------------------
94+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE nothing nil Neither CL nor chunked
95+
// other nothing nil chunked
96+
func testNoHeadersNoBody() throws {
97+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] {
98+
var headers: HTTPHeaders = .init()
99+
XCTAssertNoThrow(try headers.validate(method: method, body: nil))
100+
XCTAssertTrue(headers["content-length"].isEmpty)
101+
XCTAssertTrue(headers["transfer-encoding"].isEmpty)
102+
}
103+
104+
for method: HTTPMethod in [.POST, .PUT] {
105+
var headers: HTTPHeaders = .init()
106+
XCTAssertNoThrow(try headers.validate(method: method, body: nil))
107+
// TODO: This should be CL=0 ??? https://tools.ietf.org/html/rfc7230#section-3.3.2
108+
XCTAssertTrue(headers["content-length"].isEmpty)
109+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
110+
}
111+
}
112+
113+
// Method kind User sets Body Expectation
114+
// --------------------------------------------------------------------------------------
115+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE nothing not nil Neither CL nor chunked
116+
// other nothing not nil chunked
117+
func testNoHeadersHasBody() throws {
118+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT] {
119+
var headers: HTTPHeaders = .init()
120+
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
121+
122+
// TODO: Logically, this should be a Content-Length: 1
123+
124+
XCTAssertTrue(headers["content-length"].isEmpty)
125+
XCTAssertTrue(headers["transfer-encoding"].isEmpty)
126+
}
127+
128+
for method: HTTPMethod in [.POST, .PUT] {
129+
var headers: HTTPHeaders = .init()
130+
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
131+
132+
// TODO: Logically, this should be a Content-Length: 1 since we know size, or chunked if we don't
133+
134+
XCTAssertTrue(headers["content-length"].isEmpty)
135+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
136+
}
137+
138+
for method: HTTPMethod in [.POST, .PUT] {
139+
var headers: HTTPHeaders = .init()
140+
let body: HTTPClient.Body = .stream { writer in
141+
writer.write(.byteBuffer(ByteBuffer(bytes: [0])))
142+
}
143+
XCTAssertNoThrow(try headers.validate(method: method, body: body))
144+
145+
// TODO: Logically, this should be a Content-Length: 1 since we know size, or chunked if we don't
146+
147+
XCTAssertTrue(headers["content-length"].isEmpty)
148+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
149+
}
150+
}
151+
152+
// Method kind User sets Body Expectation
153+
// ------------------------------------------------------------------------------
154+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE content-length nil CL=0
155+
// other content-length nil CL=0
156+
func testContentLengthHeaderNoBody() throws {
157+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] {
158+
var headers: HTTPHeaders = .init([("Content-Length", "1")])
159+
XCTAssertNoThrow(try headers.validate(method: method, body: nil))
160+
// TODO: this should be Neither CL nor chunked https://tools.ietf.org/html/rfc7230#section-3.3.2
161+
XCTAssertEqual(headers["content-length"].first, "0")
162+
XCTAssertTrue(headers["transfer-encoding"].isEmpty)
163+
}
164+
165+
for method: HTTPMethod in [.POST, .PUT] {
166+
var headers: HTTPHeaders = .init([("Content-Length", "1")])
167+
XCTAssertNoThrow(try headers.validate(method: method, body: nil))
168+
XCTAssertEqual(headers["content-length"].first, "0")
169+
XCTAssertTrue(headers["transfer-encoding"].isEmpty)
170+
}
171+
}
172+
173+
// Method kind User sets Body Expectation
174+
// ----------------------------------------------------------------------------------
175+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE content-length not nil CL=1
176+
// other content-length nit nil CL=1
177+
func testContentLengthHeaderHasBody() throws {
178+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT] {
179+
var headers: HTTPHeaders = .init([("Content-Length", "1")])
180+
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
181+
XCTAssertEqual(headers["content-length"].first, "1")
182+
XCTAssertTrue(headers["transfer-encoding"].isEmpty)
183+
}
184+
185+
for method: HTTPMethod in [.POST, .PUT] {
186+
var headers: HTTPHeaders = .init([("Content-Length", "1")])
187+
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
188+
XCTAssertEqual(headers["content-length"].first, "1")
189+
XCTAssertTrue(headers["transfer-encoding"].isEmpty)
190+
}
191+
}
192+
193+
// Method kind User sets Body Expectation
194+
// ------------------------------------------------------------------------------------------
195+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE transfer-encoding: chunked nil chunked
196+
// other transfer-encoding: chunked nil chunked
197+
func testTransferEncodingHeaderNoBody() throws {
198+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] {
199+
var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")])
200+
XCTAssertNoThrow(try headers.validate(method: method, body: nil))
201+
XCTAssertTrue(headers["content-length"].isEmpty)
202+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
203+
}
204+
205+
for method: HTTPMethod in [.POST, .PUT] {
206+
var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")])
207+
XCTAssertNoThrow(try headers.validate(method: method, body: nil))
208+
XCTAssertTrue(headers["content-length"].isEmpty)
209+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
210+
}
211+
}
212+
213+
// Method kind User sets Body Expectation
214+
// --------------------------------------------------------------------------------------
215+
// .GET, .HEAD, .DELETE, .CONNECT transfer-encoding: chunked not nil chunked
216+
// other transfer-encoding: chunked not nil chunked
217+
func testTransferEncodingHeaderHasBody() throws {
218+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT] {
219+
var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")])
220+
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
221+
XCTAssertTrue(headers["content-length"].isEmpty)
222+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
223+
}
224+
225+
for method: HTTPMethod in [.POST, .PUT] {
226+
var headers: HTTPHeaders = .init([("Transfer-Encoding", "chunked")])
227+
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
228+
XCTAssertTrue(headers["content-length"].isEmpty)
229+
XCTAssertTrue(headers["transfer-encoding"].contains("chunked"))
230+
}
231+
}
232+
233+
// Method kind User sets Body Expectation
234+
// ---------------------------------------------------------------------------------------
235+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE CL & chunked (illegal) nil throws error
236+
// other CL & chunked (illegal) nil throws error
237+
func testBothHeadersNoBody() throws {
238+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] {
239+
var headers: HTTPHeaders = .init([("Content-Length", "1"), ("Transfer-Encoding", "chunked")])
240+
XCTAssertThrowsError(try headers.validate(method: method, body: nil))
241+
}
242+
243+
for method: HTTPMethod in [.POST, .PUT] {
244+
var headers: HTTPHeaders = .init([("Content-Length", "1"), ("Transfer-Encoding", "chunked")])
245+
XCTAssertThrowsError(try headers.validate(method: method, body: nil))
246+
}
247+
}
248+
249+
// Method kind User sets Body Expectation
250+
// -------------------------------------------------------------------------------------------
251+
// .GET, .HEAD, .DELETE, .CONNECT, .TRACE CL & chunked (illegal) not nil throws error
252+
// other CL & chunked (illegal) not nil throws error
253+
func testBothHeadersHasBody() throws {
254+
for method: HTTPMethod in [.GET, .HEAD, .DELETE, .CONNECT, .TRACE] {
255+
var headers: HTTPHeaders = .init([("Content-Length", "1"), ("Transfer-Encoding", "chunked")])
256+
XCTAssertThrowsError(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
257+
}
258+
259+
for method: HTTPMethod in [.POST, .PUT] {
260+
var headers: HTTPHeaders = .init([("Content-Length", "1"), ("Transfer-Encoding", "chunked")])
261+
XCTAssertThrowsError(try headers.validate(method: method, body: .byteBuffer(ByteBuffer(bytes: [0]))))
114262
}
115-
XCTAssertThrowsError(try headers.validate(method: .PUT, body: body))
116263
}
117264
}

0 commit comments

Comments
 (0)