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
10 changes: 9 additions & 1 deletion FlyingFox/Sources/HTTPRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,15 @@ public extension HTTPRequest {
}

extension HTTPRequest {
// RFC 9112 §9.3 — HTTP/1.1 connections persist unless `Connection: close`;
// HTTP/1.0 connections close unless `Connection: keep-alive` is present.
var shouldKeepAlive: Bool {
headers[.connection]?.caseInsensitiveCompare("keep-alive") == .orderedSame
let tokens = (headers[.connection] ?? "")
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces).lowercased() }
if version == .http11 {
return !tokens.contains("close")
}
return tokens.contains("keep-alive")
}
}
4 changes: 3 additions & 1 deletion FlyingFox/Sources/HTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,9 @@ public final actor HTTPServer {

func handleRequest(_ request: HTTPRequest) async -> HTTPResponse {
var response = await handleRequest(request, timeout: config.timeout)
if request.shouldKeepAlive {
// Echo the request's Connection header on keep-alive responses, but only
// if the handler did not set its own (e.g. WebSocket upgrade → "upgrade").
if request.shouldKeepAlive, response.headers[.connection] == nil {
response.headers[.connection] = request.headers[.connection]
}
return response
Expand Down
1 change: 1 addition & 0 deletions FlyingFox/Tests/HTTPConnectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct HTTPConnectionTests {
Connection: Keep-Alive\r
\r
GET /hello HTTP/1.1\r
Connection: close\r
\r

"""
Expand Down
57 changes: 57 additions & 0 deletions FlyingFox/Tests/HTTPRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,61 @@ struct HTTPRequestTests {
#expect(request.target.query() == "food=fish%20%26%20chips&qty=15")
#expect(request.target.query(percentEncoded: false) == "food=fish & chips&qty=15")
}

// RFC 9112 §9.3 — HTTP/1.1 connections persist by default; only `Connection: close` opts out.
@Test
func http11_keepsAliveByDefault_whenConnectionHeaderAbsent() {
let request = HTTPRequest.make(version: .http11, headers: [:])
#expect(request.shouldKeepAlive)
}

@Test
func http11_closes_whenConnectionHeaderIsClose() {
let request = HTTPRequest.make(version: .http11, headers: [.connection: "close"])
#expect(!request.shouldKeepAlive)
}

@Test
func http11_closes_whenConnectionHeaderIsCloseMixedCase() {
let request = HTTPRequest.make(version: .http11, headers: [.connection: "Close"])
#expect(!request.shouldKeepAlive)
}

@Test
func http11_keepsAlive_whenConnectionHeaderIsKeepAlive() {
let request = HTTPRequest.make(version: .http11, headers: [.connection: "keep-alive"])
#expect(request.shouldKeepAlive)
}

// RFC 9110 §7.6.1 — Connection is a comma-separated list of options.
@Test
func http11_keepsAlive_withMultiTokenConnectionHeader() {
let request = HTTPRequest.make(version: .http11, headers: [.connection: "keep-alive, Upgrade"])
#expect(request.shouldKeepAlive)
}

@Test
func http11_closes_whenCloseTokenAppearsAmongOthers() {
let request = HTTPRequest.make(version: .http11, headers: [.connection: "Upgrade, close"])
#expect(!request.shouldKeepAlive)
}

// RFC 9112 §9.3 — HTTP/1.0 closes by default; only `Connection: keep-alive` opts in.
@Test
func http10_closesByDefault_whenConnectionHeaderAbsent() {
let request = HTTPRequest.make(version: HTTPVersion("HTTP/1.0"), headers: [:])
#expect(!request.shouldKeepAlive)
}

@Test
func http10_keepsAlive_whenConnectionHeaderIsKeepAlive() {
let request = HTTPRequest.make(version: HTTPVersion("HTTP/1.0"), headers: [.connection: "keep-alive"])
#expect(request.shouldKeepAlive)
}

@Test
func http10_closes_whenConnectionHeaderIsClose() {
let request = HTTPRequest.make(version: HTTPVersion("HTTP/1.0"), headers: [.connection: "close"])
#expect(!request.shouldKeepAlive)
}
}
Loading