Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Sources/NIOHTTPServer/NIOHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ public struct NIOHTTPServer: HTTPServer {
} catch {
self.logger.debug("Error thrown while handling connection: \(error)")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a change made in this pr, but we should never use string interpolation for log messages. the correct pattern is to put the error message into metadata.

Suggested change
self.logger.debug("Error thrown while handling connection: \(error)")
self.logger.debug("Error thrown while handling connection", metadata: ["error": "\(error)"])

Copy link
Copy Markdown
Collaborator

@gjcairo gjcairo Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was already changed in a different PR - in any case there's an open issue to audit these log messages.

// TODO: We need to send a response head here potentially
throw error
try? await channel.channel.close()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what has happened to the OG error? that is just swallowed here? I think we should at least add a comment, why we swallow it here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also I think, we should log the error, if we can't close the connection.

Copy link
Copy Markdown
Collaborator

@gjcairo gjcairo Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change in this PR was a bit of a workaround to avoid the whole server coming down when an error is thrown. I will work on handling this properly - that's part of what the TODO right above is. I'll make sure we also log the error when closing.

}
}

Expand Down
74 changes: 74 additions & 0 deletions Tests/NIOHTTPServerTests/NIOHTTPServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -509,14 +509,88 @@ struct NIOHTTPServerTests {
)
}
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
@Test("Server can still process other connections despite one failing")
func testServerCanContinueDespiteFailedConnection() async throws {
let server = try self.makePlaintextHTTP1Server()

let elg: EventLoopGroup = .singletonMultiThreadedEventLoopGroup
let firstRequestErrorCaught = elg.any().makePromise(of: Void.self)

try await Self.withServer(
server: server,
serverHandler: HTTPServerClosureRequestHandler { request, context, requestReader, responseSender in
do {
try await Self.echoResponse(
readUpTo: Self.bodyData.readableBytes,
reader: requestReader,
sender: responseSender
)
} catch {
// Complete the promise
firstRequestErrorCaught.succeed()

// Propagate the error upwards
throw error
}
},
body: { serverAddress in
try await confirmation { responseReceived in
let firstClientChannel = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.connectToTestHTTP1Server(at: serverAddress)

try await firstClientChannel.executeThenClose { inbound, outbound in
// Only send a request head; finish the stream immediately afterwards.
try await outbound.write(.head(.init(method: .post, scheme: "http", authority: "", path: "/")))
outbound.finish()
}

try await firstRequestErrorCaught.futureResult.get()

let secondClientChannel = try await ClientBootstrap(group: .singletonMultiThreadedEventLoopGroup)
.connectToTestHTTP1Server(at: serverAddress)

try await secondClientChannel.executeThenClose { inbound, outbound in
try await outbound.write(.head(.init(method: .post, scheme: "http", authority: "", path: "/")))
try await outbound.write(.body(Self.bodyData))
try await outbound.write(.end(nil))

try await Self.validateResponse(
inbound,
expectedHead: [Self.responseHead(status: .ok, for: .http1_1)],
expectedBody: [Self.bodyData]
)

responseReceived()
}
}
}
)
}
}

extension NIOHTTPServerTests {
static let bodyData = ByteBuffer(repeating: 5, count: 100)
static let reqBody = HTTPRequestPart.body(Self.bodyData)

static let trailer: HTTPFields = [.trailer: "test_trailer"]
static let reqEnd = HTTPRequestPart.end(trailer)

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
func makePlaintextHTTP1Server() throws -> NIOHTTPServer {
let server = NIOHTTPServer(
logger: self.serverLogger,
configuration: try .init(
bindTarget: .hostAndPort(host: "127.0.0.1", port: 0),
supportedHTTPVersions: [.http1_1],
transportSecurity: .plaintext
)
)

return server
}

@available(macOS 26.2, iOS 26.2, watchOS 26.2, tvOS 26.2, visionOS 26.2, *)
func makeSecureUpgradeServer() throws -> (NIOHTTPServer, ChainPrivateKeyPair) {
let serverChain = try TestCA.makeSelfSignedChain()
Expand Down
Loading