Response compression for the Website service (#7)

This PR contains the work done to add response compression to the Website service by registering the `ResponseCompressionMiddleware` middleware so responses are compressed when the client advertises support and the body exceeds a minimum size.

To provide further details about the work done:

* Added the **HummingbirdCompression** package dependency.
* Integrated the `ResponseCompressionMiddleware` middleware into the router, ahead of the not-found and static file middleware.
* Made the `minimum-response-size-to-compress` threshold configurable, with a default.

Reviewed-on: rock-n-code/loud-amsterdam#7
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit is contained in:
2026-06-28 06:15:05 +00:00
committed by javier
parent 6c1a0f713b
commit 7c18cd9ec0
6 changed files with 68 additions and 6 deletions
+8
View File
@@ -31,6 +31,10 @@ let package = Package(
url: "https://github.com/hummingbird-project/hummingbird.git",
from: "2.25.0"
),
.package(
url: "https://github.com/hummingbird-project/hummingbird-compression.git",
from: "2.0.0"
),
.package(
url: "https://github.com/apple/swift-configuration.git",
from: "1.0.0",
@@ -53,6 +57,10 @@ let package = Package(
name: "Hummingbird",
package: "hummingbird"
),
.product(
name: "HummingbirdCompression",
package: "hummingbird-compression"
),
],
path: "Sources/App"
),
+18 -6
View File
@@ -1,12 +1,13 @@
import Configuration
import Hummingbird
import HummingbirdCompression
import Logging
import WebsiteCore
/// Builds the website application.
///
/// Reads the log level, server name, and static files location from the configuration,
/// then assembles the router, server configuration, and logger.
/// Reads the log level, server name, static files location, and minimum response size to
/// compress from the configuration, then assembles the router, server configuration, and logger.
/// - Parameter reader: the configuration reader the values are read from.
/// - Returns: the configured application, ready to run as a service.
func application(
@@ -26,6 +27,10 @@ func application(
default: .Cache.maxAgeDefault
)
)
let compressionMinResponseSize = reader.int(
forKey: .Compression.minResponseSize,
default: .Compression.minResponseSize
)
let logLevel = reader.string(
forKey: .Log.level,
as: Logger.Level.self,
@@ -44,6 +49,7 @@ func application(
router: router(
staticFilesPath: staticFilesPath,
cacheControl: cacheControl,
compressionMinResponseSize: compressionMinResponseSize,
logLevel: logLevel
),
configuration: ApplicationConfiguration(
@@ -102,24 +108,30 @@ private func logger(
/// Builds the application's router.
///
/// Registers the request-logging middleware, the not-found middleware that serves the error
/// page, and the static file middleware that serves the contents of `staticFilesPath` (tagging
/// responses with the given `cacheControl` directives), then adds the `RootController` routes
/// that render the landing page.
/// Registers the request-logging middleware, the response-compression middleware that compresses
/// responses larger than `minimumResponseSizeToCompress` when the client advertises support, the
/// not-found middleware that serves the error page, and the static file middleware that serves the
/// contents of `staticFilesPath` (tagging responses with the given `cacheControl` directives), then
/// adds the `RootController` routes that render the landing page.
/// - Parameters:
/// - staticFilesPath: the folder, relative to the working directory, the static files are served from.
/// - cacheControl: the cache-control directives applied to the served static files.
/// - compressionMinResponseSize: the minimum response body size, in bytes, before compression is applied.
/// - logLevel: the level the request-logging middleware logs at.
/// - Returns: the configured router.
private func router(
staticFilesPath: String,
cacheControl: CacheControl,
compressionMinResponseSize: Int,
logLevel: Logger.Level
) -> Router<AppRequestContext> {
let router = Router(context: AppRequestContext.self)
router.addMiddleware {
LogRequestsMiddleware(logLevel)
ResponseCompressionMiddleware(
minimumResponseSizeToCompress: compressionMinResponseSize
)
NotFoundMiddleware()
FileMiddleware(
staticFilesPath,
@@ -10,6 +10,11 @@ extension AbsoluteConfigKey {
/// The absolute configuration key for the max-age, in seconds, applied to all other static files.
public static let maxAgeDefault: AbsoluteConfigKey = .init(.Cache.maxAgeDefault)
}
/// A namespace for the response compression configuration keys, as absolute keys.
public enum Compression {
/// The absolute configuration key for the minimum response body size, in bytes, before compression is applied.
public static let minResponseSize: AbsoluteConfigKey = .init(.Compression.minResponseSize)
}
/// A namespace for the HTTP server configuration keys, as absolute keys.
public enum HTTP {
/// The absolute configuration key for the host the server binds to.
@@ -10,6 +10,11 @@ extension ConfigKey {
/// The configuration key for the max-age, in seconds, applied to all other static files (e.g. the web manifest).
public static let maxAgeDefault: ConfigKey = "cache.maxAge.default"
}
/// A namespace for the response compression configuration keys.
public enum Compression {
/// The configuration key for the minimum response body size, in bytes, before compression is applied.
public static let minResponseSize: ConfigKey = "compression.minimumResponseSize"
}
/// A namespace for the HTTP server configuration keys.
public enum HTTP {
/// The configuration key for the host the server binds to.
@@ -8,4 +8,9 @@ extension Int {
/// The default max-age, in seconds, applied to all other static files (1 day).
public static let maxAgeDefault = 86_400
}
/// A namespace for the response compression's default configuration values.
public enum Compression {
/// The default minimum response body size, in bytes, before compression is applied (1 KB).
public static let minResponseSize = 1_024
}
}
+27
View File
@@ -71,6 +71,33 @@ struct AppTests {
}
}
@Test
func `response to be compressed when the client supports it`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/",
method: .get,
headers: [.acceptEncoding: "gzip"]
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentEncoding] == "gzip")
}
}
}
@Test
func `response to not be compressed when the client does not support it`() async throws {
try await app.test(.router) { client in
try await client.execute(
uri: "/",
method: .get
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentEncoding] == nil)
}
}
}
@Test
func `error page to be served when not found`() async throws {
try await app.test(.router) { client in