Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ the public-API contract.

## [Unreleased]

_Nothing yet._
### Fixed

- Keep persistent HTTP/1.1 body framing in the caller's isolation while reading
chunks. Swift 6.4 can otherwise reject the cancellation-handler read with
"sending 'current' risks causing data races"; the chunked decoder remains
single-consumer, without unchecked Sendable conformance or an executor hop.

## [7.40.0] - 2026-10-18

Expand Down
10 changes: 8 additions & 2 deletions Sources/AetherEngine/IO/HLSIngest/PersistentHTTP1Transport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,10 @@ final class PersistentHTTP1Connection: @unchecked Sendable {

/// The next piece of a body under `framing`, nil at its end, plus the framing after it. No
/// cancellation handler: the caller decides whether an abandoned body is drained or closed.
func readChunk(_ framing: Framing, progress: () -> String) async throws -> (Data?, Framing) {
func readChunk(
_ framing: Framing, progress: () -> String,
isolation: isolated (any Actor)? = #isolation
) async throws -> (Data?, Framing) {
guard let task = stateLock.withLock({ retired ? nil : task }) else { throw CancellationError() }
while true {
switch framing {
Expand Down Expand Up @@ -731,7 +734,10 @@ final class PersistentHTTP1Connection: @unchecked Sendable {
}

/// Bytes left over from the head read first, then the socket. nil is EOF.
private func nextBytes(_ task: URLSessionStreamTask, upTo limit: Int, progress: () -> String) async throws -> Data? {
private func nextBytes(
_ task: URLSessionStreamTask, upTo limit: Int, progress: () -> String,
isolation: isolated (any Actor)? = #isolation
) async throws -> Data? {
let buffered = stateLock.withLock { () -> Data in
let slice = pending.prefix(limit)
pending.removeFirst(slice.count)
Expand Down
29 changes: 29 additions & 0 deletions Tests/AetherEngineTests/Issue732PersistentHTTP1Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,35 @@ final class Issue732PersistentHTTP1Tests: XCTestCase {
XCTAssertEqual(server.connectionCount, 1)
}

func testChunkedReadsKeepFramingAcrossCancellationHandlers() async throws {
let server = try XCTUnwrap(KeepAliveOrigin())
defer { server.stop() }
let expected = Data((0..<5000).map { UInt8($0 & 0xFF) })
server.route = { _ in .init(body: expected, chunked: true) }
let url = server.url("/chunked")
let origin = try XCTUnwrap(PersistentHTTP1Transport.Origin(url))
let connection = PersistentHTTP1Connection(origin: origin, label: "isolation")
defer { connection.retire(reason: "test finished") }
let head = try await connection.sendRequest(target: url, headers: [:])
var framing = PersistentHTTP1Connection.framing(for: head)
var received = Data()

while true {
let current = framing
let (chunk, advanced) = try await withTaskCancellationHandler {
try await connection.readChunk(current, progress: { "\(received.count) bytes" })
} onCancel: {
connection.retire(reason: "test cancelled")
}
framing = advanced
guard let chunk else { break }
received.append(chunk)
}

XCTAssertEqual(received, expected)
XCTAssertNil(PersistentHTTP1Connection.reuseRefusal(head, framing))
}

func testRefusalKeepsTheConnectionAndReportsItsStatus() async throws {
let server = try XCTUnwrap(KeepAliveOrigin())
defer { server.stop() }
Expand Down
6 changes: 6 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ at a time, until a 120 s time limit fired at 151 s because its watchdog is a tas
pool. `LIBDISPATCH_COOPERATIVE_POOL_STRICT=1` narrows the pool to one thread and reproduces that
on any Mac; the full suite has to pass under it.

Persistent HTTP/1.1 body reads inherit the caller's isolation through
`PersistentHTTP1Connection.readChunk`'s isolated parameter. Its framing can own a
mutable chunked-body decoder, so a cancellation-handler read must not send that
single-consumer state to a different executor. Cancellation still abandons the
body through its existing drain-or-close path.

## Subtitle recognition scheduling

Bitmap subtitle OCR awaits each image's recognition on a dedicated utility thread.
Expand Down
Loading