Skip to content

Commit 9c72385

Browse files
authored
Merge pull request #140 from swhitty/AsyncBufferedPrefixSequence
Fix HTTPBodySequence for large requests (adds AsyncBufferedPrefixSequence)
2 parents fe9437d + 4231165 commit 9c72385

File tree

4 files changed

+217
-13
lines changed

4 files changed

+217
-13
lines changed

FlyingFox/Sources/HTTPDecoder.swift

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,10 @@ struct HTTPDecoder {
122122
if length <= sharedRequestReplaySize {
123123
return HTTPBodySequence(shared: bytes, count: length, suggestedBufferSize: 4096)
124124
} else {
125-
return HTTPBodySequence(from: bytes, count: length, suggestedBufferSize: 4096)
125+
let prefix = AsyncBufferedPrefixSequence(base: bytes, count: length)
126+
return HTTPBodySequence(from: prefix, count: length, suggestedBufferSize: 4096)
126127
}
127128
}
128-
129-
func makeBodyData(from bytes: some AsyncBufferedSequence<UInt8>, length: Int) async throws -> Data {
130-
var iterator = bytes.makeAsyncIterator()
131-
guard let buffer = try await iterator.nextBuffer(count: length) else {
132-
throw Error("AsyncBufferedSequence prematurely ended")
133-
}
134-
return Data(buffer)
135-
}
136129
}
137130

138131
extension HTTPDecoder {

FlyingFox/Tests/HTTPServerTests.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,29 @@ actor HTTPServerTests {
184184
)
185185
}
186186

187+
@Test
188+
func requests_larger_than_shared_buffer() async throws {
189+
// given
190+
let server = HTTPServer.make(sharedRequestReplaySize: 100)
191+
let port = try await startServerWithPort(server, preferConnectionsDiscarding: true)
192+
193+
await server.appendRoute("/fish") { req in
194+
let count = try await req.bodyData.count
195+
return HTTPResponse(statusCode: .ok, body: "\(count) bytes".data(using: .utf8)!)
196+
}
197+
198+
// when
199+
var request = URLRequest(url: URL(string: "http://localhost:\(port)/fish")!)
200+
request.httpMethod = "POST"
201+
request.httpBody = Data(repeating: 0x0, count: 200)
202+
let (body, _) = try await URLSession.shared.data(for: request)
203+
204+
// then
205+
#expect(
206+
String(data: body, encoding: .utf8) == "200 bytes"
207+
)
208+
}
209+
187210
@Test
188211
func connections_AreHandled_FallbackTaskGroup() async throws {
189212
let server = HTTPServer.make()
@@ -539,12 +562,18 @@ extension HTTPServer {
539562

540563
static func make(port: UInt16 = 0,
541564
timeout: TimeInterval = 15,
565+
sharedRequestReplaySize: Int? = nil,
542566
logger: some Logging = .disabled,
543567
handler: (any HTTPHandler)? = nil) -> HTTPServer {
544-
HTTPServer(port: port,
545-
timeout: timeout,
546-
logger: logger,
547-
handler: handler)
568+
var config = Configuration(
569+
port: port,
570+
timeout: timeout,
571+
logger: logger
572+
)
573+
if let sharedRequestReplaySize {
574+
config.sharedRequestReplaySize = sharedRequestReplaySize
575+
}
576+
return HTTPServer(config: config, handler: handler)
548577
}
549578

550579
static func make(port: UInt16 = 0,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// AsyncBufferedPrefixSequence.swift
3+
// FlyingFox
4+
//
5+
// Created by Simon Whitty on 04/02/2025.
6+
// Copyright © 2025 Simon Whitty. All rights reserved.
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/FlyingFox
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
package struct AsyncBufferedPrefixSequence<Base: AsyncBufferedSequence>: AsyncBufferedSequence {
33+
package typealias Element = Base.Element
34+
35+
private let base: Base
36+
private let count: Int
37+
38+
package init(base: Base, count: Int) {
39+
self.base = base
40+
self.count = count
41+
}
42+
43+
package func makeAsyncIterator() -> Iterator {
44+
Iterator(iterator: base.makeAsyncIterator(), remaining: count)
45+
}
46+
47+
package struct Iterator: AsyncBufferedIteratorProtocol {
48+
private var iterator: Base.AsyncIterator
49+
private var remaining: Int
50+
51+
init (iterator: Base.AsyncIterator, remaining: Int) {
52+
self.iterator = iterator
53+
self.remaining = remaining
54+
}
55+
56+
package mutating func next() async throws -> Base.Element? {
57+
guard remaining > 0 else { return nil }
58+
59+
if let element = try await iterator.next() {
60+
remaining -= 1
61+
return element
62+
} else {
63+
remaining = 0
64+
return nil
65+
}
66+
}
67+
68+
package mutating func nextBuffer(suggested count: Int) async throws -> Base.AsyncIterator.Buffer? {
69+
guard remaining > 0 else { return nil }
70+
71+
let count = Swift.min(remaining, count)
72+
if let buffer = try await iterator.nextBuffer(suggested: count) {
73+
remaining -= buffer.count
74+
return buffer
75+
} else {
76+
remaining = 0
77+
return nil
78+
}
79+
}
80+
}
81+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// AsyncBufferedPrefixSequenceTests.swift
3+
// FlyingFox
4+
//
5+
// Created by Simon Whitty on 04/02/2025.
6+
// Copyright © 2025 Simon Whitty. All rights reserved.
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/FlyingFox
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
@testable import FlyingSocks
33+
import Foundation
34+
import Testing
35+
36+
struct AsyncBufferedPrefixSequenceTests {
37+
38+
@Test
39+
func next_terminates_after_count() async throws {
40+
let buffer = AsyncBufferedCollection(["a", "b", "c", "d", "e", "f"])
41+
var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 4).makeAsyncIterator()
42+
43+
#expect(
44+
try await prefix.next() == "a"
45+
)
46+
#expect(
47+
try await prefix.next() == "b"
48+
)
49+
#expect(
50+
try await prefix.next() == "c"
51+
)
52+
#expect(
53+
try await prefix.next() == "d"
54+
)
55+
#expect(
56+
try await prefix.next() == nil
57+
)
58+
}
59+
60+
@Test
61+
func nextBuffer_terminates_after_count() async throws {
62+
let buffer = AsyncBufferedCollection(["a", "b", "c", "d", "e", "f"])
63+
var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 4).makeAsyncIterator()
64+
65+
#expect(
66+
try await prefix.nextBuffer(suggested: 3) == ["a", "b", "c"]
67+
)
68+
#expect(
69+
try await prefix.nextBuffer(suggested: 3) == ["d"]
70+
)
71+
#expect(
72+
try await prefix.nextBuffer(suggested: 3) == nil
73+
)
74+
}
75+
76+
@Test
77+
func next_terminates_when_base_terminates() async throws {
78+
let buffer = AsyncBufferedCollection(["a"])
79+
var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 2).makeAsyncIterator()
80+
81+
#expect(
82+
try await prefix.next() == "a"
83+
)
84+
#expect(
85+
try await prefix.next() == nil
86+
)
87+
}
88+
89+
@Test
90+
func nextBuffer_terminates_when_base_terminates() async throws {
91+
let buffer = AsyncBufferedCollection(["a"])
92+
var prefix = AsyncBufferedPrefixSequence(base: buffer, count: 10).makeAsyncIterator()
93+
94+
#expect(
95+
try await prefix.nextBuffer(suggested: 3) == ["a"]
96+
)
97+
#expect(
98+
try await prefix.nextBuffer(suggested: 3) == nil
99+
)
100+
}
101+
}

0 commit comments

Comments
 (0)