From affc9e945b2d6be258ad34d435e095fe4a192429 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 19 May 2024 23:04:11 +0200 Subject: [PATCH 1/2] Provide content-length in the HTTPFields' literal when creating a response This allows the HTTPFields to allocate sufficient room for `Content-Length`, `Server` and `Date`, preventing reallocs --- .../Hummingbird/Codable/JSON/JSONCoding.swift | 6 +++- .../URLEncodedForm+Request.swift | 6 +++- .../Router/EndpointResponder.swift | 4 +-- .../Router/ResponseGenerator.swift | 29 ++++++++++++++++--- .../Utils/HTTPFields+Payload.swift | 14 +++++++++ .../HummingbirdCore/Response/Response.swift | 2 +- 6 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 Sources/Hummingbird/Utils/HTTPFields+Payload.swift diff --git a/Sources/Hummingbird/Codable/JSON/JSONCoding.swift b/Sources/Hummingbird/Codable/JSON/JSONCoding.swift index 4608062d8..b7bb4440e 100644 --- a/Sources/Hummingbird/Codable/JSON/JSONCoding.swift +++ b/Sources/Hummingbird/Codable/JSON/JSONCoding.swift @@ -26,9 +26,13 @@ extension JSONEncoder: ResponseEncoder { var buffer = context.allocator.buffer(capacity: 0) let data = try self.encode(value) buffer.writeBytes(data) + return Response( status: .ok, - headers: [.contentType: "application/json; charset=utf-8"], + headers: HTTPFields( + contentType: "application/json; charset=utf-8", + contentLength: buffer.readableBytes + ), body: .init(byteBuffer: buffer) ) } diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm+Request.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm+Request.swift index ae9d48427..f9a037a80 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm+Request.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm+Request.swift @@ -21,9 +21,13 @@ extension URLEncodedFormEncoder: ResponseEncoder { var buffer = context.allocator.buffer(capacity: 0) let string = try self.encode(value) buffer.writeString(string) + return Response( status: .ok, - headers: [.contentType: "application/x-www-form-urlencoded"], + headers: HTTPFields( + contentType: "application/x-www-form-urlencoded", + contentLength: buffer.readableBytes + ), body: .init(byteBuffer: buffer) ) } diff --git a/Sources/Hummingbird/Router/EndpointResponder.swift b/Sources/Hummingbird/Router/EndpointResponder.swift index b7495e069..93d566408 100644 --- a/Sources/Hummingbird/Router/EndpointResponder.swift +++ b/Sources/Hummingbird/Router/EndpointResponder.swift @@ -26,14 +26,14 @@ struct EndpointResponders: Sendable { } mutating func addResponder(for method: HTTPRequest.Method, responder: any HTTPResponder) { - guard self.methods[method] == nil else { + guard !self.methods.keys.contains(method) else { preconditionFailure("\(method.rawValue) already has a handler") } self.methods[method] = responder } mutating func autoGenerateHeadEndpoint() { - if self.methods[.head] == nil, let get = methods[.get] { + if !self.methods.keys.contains(.head), let get = methods[.get] { self.methods[.head] = CallbackResponder { request, context in let response = try await get.respond(to: request, context: context) return response.createHeadResponse() diff --git a/Sources/Hummingbird/Router/ResponseGenerator.swift b/Sources/Hummingbird/Router/ResponseGenerator.swift index 1d3268709..05b538605 100644 --- a/Sources/Hummingbird/Router/ResponseGenerator.swift +++ b/Sources/Hummingbird/Router/ResponseGenerator.swift @@ -33,7 +33,14 @@ extension String: ResponseGenerator { /// Generate response holding string public func response(from request: Request, context: some BaseRequestContext) -> Response { let buffer = context.allocator.buffer(string: self) - return Response(status: .ok, headers: [.contentType: "text/plain; charset=utf-8"], body: .init(byteBuffer: buffer)) + return Response( + status: .ok, + headers: HTTPFields( + contentType: "text/plain; charset=utf-8", + contentLength: buffer.readableBytes + ), + body: .init(byteBuffer: buffer) + ) } } @@ -42,7 +49,14 @@ extension Substring: ResponseGenerator { /// Generate response holding string public func response(from request: Request, context: some BaseRequestContext) -> Response { let buffer = context.allocator.buffer(substring: self) - return Response(status: .ok, headers: [.contentType: "text/plain; charset=utf-8"], body: .init(byteBuffer: buffer)) + return Response( + status: .ok, + headers: HTTPFields( + contentType: "text/plain; charset=utf-8", + contentLength: buffer.readableBytes + ), + body: .init(byteBuffer: buffer) + ) } } @@ -50,7 +64,14 @@ extension Substring: ResponseGenerator { extension ByteBuffer: ResponseGenerator { /// Generate response holding bytebuffer public func response(from request: Request, context: some BaseRequestContext) -> Response { - Response(status: .ok, headers: [.contentType: "application/octet-stream"], body: .init(byteBuffer: self)) + Response( + status: .ok, + headers: HTTPFields( + contentType: "application/octet-stream", + contentLength: self.readableBytes + ), + body: .init(byteBuffer: self) + ) } } @@ -69,7 +90,7 @@ extension Optional: ResponseGenerator where Wrapped: ResponseGenerator { case .some(let wrapped): return try wrapped.response(from: request, context: context) case .none: - return Response(status: .noContent, headers: [:], body: .init()) + return Response(status: .noContent) } } } diff --git a/Sources/Hummingbird/Utils/HTTPFields+Payload.swift b/Sources/Hummingbird/Utils/HTTPFields+Payload.swift new file mode 100644 index 000000000..ab4d89d8f --- /dev/null +++ b/Sources/Hummingbird/Utils/HTTPFields+Payload.swift @@ -0,0 +1,14 @@ +import HTTPTypes + +extension HTTPFields { + init(contentType: String, contentLength: Int) { + self.init() + + // Content-Type, Content-Length, Server, Date + 2 extra headers + // This should cover our expected amount of headers + self.reserveCapacity(6) + + self[.contentType] = contentType + self[.contentLength] = String(describing: contentLength) + } +} diff --git a/Sources/HummingbirdCore/Response/Response.swift b/Sources/HummingbirdCore/Response/Response.swift index 71eda27a8..d01f571e9 100644 --- a/Sources/HummingbirdCore/Response/Response.swift +++ b/Sources/HummingbirdCore/Response/Response.swift @@ -28,7 +28,7 @@ public struct Response: Sendable { public init(status: HTTPResponse.Status, headers: HTTPFields = .init(), body: ResponseBody = .init()) { self.head = .init(status: status, headerFields: headers) self.body = body - if let contentLength = body.contentLength, headers[.contentLength] == nil { + if let contentLength = body.contentLength, !headers.contains(.contentLength) { self.head.headerFields[.contentLength] = String(describing: contentLength) } } From 90751b11521322dcdb1b0052ad4002780464065d Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sun, 19 May 2024 23:39:23 +0200 Subject: [PATCH 2/2] Remove separate init() + reserveCapacity on HTTPFields, as it's much slower than expected --- Sources/Hummingbird/Utils/HTTPFields+Payload.swift | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/Hummingbird/Utils/HTTPFields+Payload.swift b/Sources/Hummingbird/Utils/HTTPFields+Payload.swift index ab4d89d8f..6299ea6b0 100644 --- a/Sources/Hummingbird/Utils/HTTPFields+Payload.swift +++ b/Sources/Hummingbird/Utils/HTTPFields+Payload.swift @@ -2,13 +2,9 @@ import HTTPTypes extension HTTPFields { init(contentType: String, contentLength: Int) { - self.init() - - // Content-Type, Content-Length, Server, Date + 2 extra headers - // This should cover our expected amount of headers - self.reserveCapacity(6) - - self[.contentType] = contentType - self[.contentLength] = String(describing: contentLength) + self = [ + .contentType: contentType, + .contentLength: String(describing: contentLength) + ] } }