Open
Description
Expected behavior
If I create a NIOAsyncChannel
with isOutboundHalfClosureEnabled
set to true. When I call outbound.finish()
I expect that all writes will have been flushed before the channel is closed.
This was meant to be a workaround for the fact that NIOAsyncChannel.executeThenClose
didn't flush writes, but it has the same issue.
Steps to reproduce
- Write small HTTP server that outputs a large response
- Use curl to get response.
- curl will report with something like
curl: (18) transfer closed with 672832 bytes remaining to read
If possible, minimal yet complete reproducer code (or URL to code)
Here is a small HTTP server that returns 1000000 bytes in its response
import NIOCore
import NIOHTTP1
import NIOPosix
/// Sendable server response that doesn't use ``IOData``
public typealias SendableHTTPServerResponsePart = HTTPPart<HTTPResponseHead, ByteBuffer>
/// Channel to convert HTTPServerResponsePart to the Sendable type HBHTTPServerResponsePart
final class HTTPSendableResponseChannelHandler: ChannelOutboundHandler, RemovableChannelHandler {
typealias OutboundIn = SendableHTTPServerResponsePart
typealias OutboundOut = HTTPServerResponsePart
func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
let part = unwrapOutboundIn(data)
switch part {
case .head(let head):
context.write(self.wrapOutboundOut(.head(head)), promise: promise)
case .body(let buffer):
context.write(self.wrapOutboundOut(.body(.byteBuffer(buffer))), promise: promise)
case .end:
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: promise)
}
}
}
@available(macOS 14, *)
func server() async throws {
let asyncChannel = try await ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
// Specify backlog and enable SO_REUSEADDR for the server itself
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: true)
.bind(host: "127.0.0.1", port: 8888, serverBackPressureStrategy: nil) { channel in
return channel.eventLoop.makeCompletedFuture {
try channel.pipeline.syncOperations.configureHTTPServerPipeline()
try channel.pipeline.syncOperations.addHandler(HTTPSendableResponseChannelHandler())
return try NIOAsyncChannel<HTTPServerRequestPart, SendableHTTPServerResponsePart>(
wrappingChannelSynchronously: channel,
configuration: .init(isOutboundHalfClosureEnabled: true)
)
}
}
print("Listening on 127.0.0.1:8888")
await withDiscardingTaskGroup { group in
do {
try await asyncChannel.executeThenClose { inbound in
for try await childChannel in inbound {
group.addTask {
try? await childChannel.executeThenClose { inbound, outbound in
for try await part in inbound {
if case .end = part {
let buffer = ByteBuffer(repeating: 0, count: 1_000_000)
try await outbound.write(
.head(
.init(
version: .http1_1,
status: .ok,
headers: ["Content-Length": "\(buffer.readableBytes)"]
)
)
)
try await outbound.write(.body(buffer))
try await outbound.write(.end(nil))
outbound.finish()
try await childChannel.channel.closeFuture.get()
break
}
}
}
print("Closed")
}
}
}
} catch {
print("ERROR: Waiting on child channel: \(error)")
}
}
}
if #available(macOS 14, *) {
try await server()
}
SwiftNIO version/commit hash
be266a9 (latest of this morning)
System & version information
swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0
Darwin Adams-MBP-M1-Max.local 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:16 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6000 arm64