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:
@@ -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"
|
||||
),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user