From 911b5ff7f3977b2eb81ef64f87cc7a675a065fce Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:47:45 +0100 Subject: [PATCH 1/2] Add brotli compression (#2646) --- project/Dependencies.scala | 1 + .../zio/http/netty/model/Conversions.scala | 29 ++-- .../src/main/scala/zio/http/Server.scala | 144 ++++++++++++------ 3 files changed, 115 insertions(+), 59 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 63937065b4..3b933d7b9f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -27,6 +27,7 @@ object Dependencies { "io.netty" % "netty-transport-native-kqueue" % NettyVersion, "io.netty" % "netty-transport-native-kqueue" % NettyVersion % Runtime classifier "osx-x86_64", "io.netty" % "netty-transport-native-kqueue" % NettyVersion % Runtime classifier "osx-aarch_64", + "com.aayushatharva.brotli4j" % "brotli4j" % "1.16.0" % "provided", ) val `netty-incubator` = diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala index 9ceb2d1a1f..9658e437ef 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala @@ -18,12 +18,11 @@ package zio.http.netty.model import scala.collection.AbstractIterator -import zio.stacktracer.TracingImplicits.disableAutoTrace - import zio.http.Server.Config.CompressionOptions import zio.http._ -import io.netty.handler.codec.compression.{DeflateOptions, StandardCompressionOptions} +import com.aayushatharva.brotli4j.encoder.Encoder +import io.netty.handler.codec.compression.StandardCompressionOptions import io.netty.handler.codec.http._ import io.netty.handler.codec.http.websocketx.WebSocketScheme @@ -132,14 +131,26 @@ private[netty] object Conversions { case _ => None } - def compressionOptionsToNetty(compressionOptions: CompressionOptions): DeflateOptions = - compressionOptions.kind match { - case CompressionOptions.CompressionType.GZip => - StandardCompressionOptions.gzip(compressionOptions.level, compressionOptions.bits, compressionOptions.mem) - case CompressionOptions.CompressionType.Deflate => - StandardCompressionOptions.deflate(compressionOptions.level, compressionOptions.bits, compressionOptions.mem) + def compressionOptionsToNetty( + compressionOptions: CompressionOptions, + ): io.netty.handler.codec.compression.CompressionOptions = + compressionOptions match { + case CompressionOptions.GZip(cfg) => + StandardCompressionOptions.gzip(cfg.level, cfg.bits, cfg.mem) + case CompressionOptions.Deflate(cfg) => + StandardCompressionOptions.deflate(cfg.level, cfg.bits, cfg.mem) + case CompressionOptions.Brotli(cfg) => + StandardCompressionOptions.brotli( + new Encoder.Parameters().setQuality(cfg.quality).setWindow(cfg.lgwin).setMode(brotliModeToJava(cfg.mode)), + ) } + def brotliModeToJava(brotli: CompressionOptions.Mode): Encoder.Mode = brotli match { + case CompressionOptions.Mode.Font => Encoder.Mode.FONT + case CompressionOptions.Mode.Text => Encoder.Mode.TEXT + case CompressionOptions.Mode.Generic => Encoder.Mode.GENERIC + } + def versionToNetty(version: Version): HttpVersion = version match { case Version.Http_1_0 => HttpVersion.HTTP_1_0 case Version.Http_1_1 => HttpVersion.HTTP_1_1 diff --git a/zio-http/shared/src/main/scala/zio/http/Server.scala b/zio-http/shared/src/main/scala/zio/http/Server.scala index 00ed14acd0..2cc349d3c5 100644 --- a/zio-http/shared/src/main/scala/zio/http/Server.scala +++ b/zio-http/shared/src/main/scala/zio/http/Server.scala @@ -20,7 +20,6 @@ import java.net.{InetAddress, InetSocketAddress} import java.util.concurrent.atomic._ import zio._ -import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.http.Server.Config.ResponseCompressionConfig @@ -248,71 +247,116 @@ object Server extends ServerPlatformSpecific { ResponseCompressionConfig(0, IndexedSeq(CompressionOptions.gzip(), CompressionOptions.deflate())) } - /** - * @param level - * defines compression level, {@code 1} yields the fastest compression and - * {@code 9} yields the best compression. {@code 0} means no compression. - * @param bits - * defines windowBits, The base two logarithm of the size of the history - * buffer. The value should be in the range {@code 9} to {@code 15} - * inclusive. Larger values result in better compression at the expense of - * memory usage - * @param mem - * defines memlevel, How much memory should be allocated for the internal - * compression state. {@code 1} uses minimum memory and {@code 9} uses - * maximum memory. Larger values result in better and faster compression - * at the expense of memory usage - */ - final case class CompressionOptions( - level: Int, - bits: Int, - mem: Int, - kind: CompressionOptions.CompressionType, - ) + sealed trait CompressionOptions object CompressionOptions { - val DefaultLevel = 6 - val DefaultBits = 15 - val DefaultMem = 8 + + final case class GZip(cfg: DeflateConfig) extends CompressionOptions + final case class Deflate(cfg: DeflateConfig) extends CompressionOptions + final case class Brotli(cfg: BrotliConfig) extends CompressionOptions + + /** + * @param level + * defines compression level, {@code 1} yields the fastest compression + * and {@code 9} yields the best compression. {@code 0} means no + * compression. + * @param bits + * defines windowBits, The base two logarithm of the size of the history + * buffer. The value should be in the range {@code 9} to {@code 15} + * inclusive. Larger values result in better compression at the expense + * of memory usage + * @param mem + * defines memlevel, How much memory should be allocated for the + * internal compression state. {@code 1} uses minimum memory and + * {@code 9} uses maximum memory. Larger values result in better and + * faster compression at the expense of memory usage + */ + final case class DeflateConfig( + level: Int, + bits: Int, + mem: Int, + ) + + object DeflateConfig { + val DefaultLevel = 6 + val DefaultBits = 15 + val DefaultMem = 8 + } + + final case class BrotliConfig( + quality: Int, + lgwin: Int, + mode: Mode, + ) + + object BrotliConfig { + val DefaultQuality = 4 + val DefaultLgwin = -1 + val DefaultMode = Mode.Text + } + + sealed trait Mode + object Mode { + case object Generic extends Mode + case object Text extends Mode + case object Font extends Mode + + def fromString(s: String): Mode = s.toLowerCase match { + case "generic" => Generic + case "text" => Text + case "font" => Font + case _ => Text + } + } /** * Creates GZip CompressionOptions. Defines defaults as per * io.netty.handler.codec.compression.GzipOptions#DEFAULT */ - def gzip(level: Int = DefaultLevel, bits: Int = DefaultBits, mem: Int = DefaultMem): CompressionOptions = - CompressionOptions(level, bits, mem, CompressionType.GZip) + def gzip( + level: Int = DeflateConfig.DefaultLevel, + bits: Int = DeflateConfig.DefaultBits, + mem: Int = DeflateConfig.DefaultMem, + ): CompressionOptions = + CompressionOptions.GZip(DeflateConfig(level, bits, mem)) /** * Creates Deflate CompressionOptions. Defines defaults as per * io.netty.handler.codec.compression.DeflateOptions#DEFAULT */ - def deflate(level: Int = DefaultLevel, bits: Int = DefaultBits, mem: Int = DefaultMem): CompressionOptions = - CompressionOptions(level, bits, mem, CompressionType.Deflate) - - sealed trait CompressionType { - val name: String - } - - private[http] object CompressionType { - case object GZip extends CompressionType { val name = "gzip" } - case object Deflate extends CompressionType { val name = "deflate" } + def deflate( + level: Int = DeflateConfig.DefaultLevel, + bits: Int = DeflateConfig.DefaultBits, + mem: Int = DeflateConfig.DefaultMem, + ): CompressionOptions = + CompressionOptions.Deflate(DeflateConfig(level, bits, mem)) - lazy val config: zio.Config[CompressionType] = - zio.Config.string.mapOrFail { - case "gzip" => Right(GZip) - case "deflate" => Right(Deflate) - case other => Left(zio.Config.Error.InvalidData(message = s"Invalid compression type: $other")) - } - } + /** + * Creates Brotli CompressionOptions. Defines defaults as per + * io.netty.handler.codec.compression.BrotliOptions#DEFAULT + */ + def brotli( + quality: Int = BrotliConfig.DefaultQuality, + lgwin: Int = BrotliConfig.DefaultLgwin, + mode: Mode = BrotliConfig.DefaultMode, + ): CompressionOptions = + CompressionOptions.Brotli(BrotliConfig(quality, lgwin, mode)) lazy val config: zio.Config[CompressionOptions] = ( - zio.Config.int("level").withDefault(DefaultLevel) ++ - zio.Config.int("bits").withDefault(DefaultBits) ++ - zio.Config.int("mem").withDefault(DefaultMem) ++ - CompressionOptions.CompressionType.config.nested("type") - ).map { case (level, bits, mem, kind) => - CompressionOptions(level, bits, mem, kind) + (zio.Config.int("level").withDefault(DeflateConfig.DefaultLevel) ++ + zio.Config.int("bits").withDefault(DeflateConfig.DefaultBits) ++ + zio.Config.int("mem").withDefault(DeflateConfig.DefaultMem)) ++ + zio.Config.int("quantity").withDefault(BrotliConfig.DefaultQuality) ++ + zio.Config.int("lgwin").withDefault(BrotliConfig.DefaultLgwin) ++ + zio.Config.string("mode").map(Mode.fromString).withDefault(BrotliConfig.DefaultMode) ++ + zio.Config.string("type") + ).map { case (level, bits, mem, quantity, lgwin, mode, typ) => + typ.toLowerCase match { + case "gzip" => gzip(level, bits, mem) + case "deflate" => deflate(level, bits, mem) + case "brotli" => brotli(quantity, lgwin, mode) + } } } } From c997a3f035be4565cc216c10c3cd1c3e6d1cf339 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 23 May 2024 12:45:42 +0200 Subject: [PATCH 2/2] Migrate main --- .../zio/http/netty/server/ServerInboundHandler.scala | 2 +- zio-http/shared/src/main/scala/zio/http/Server.scala | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala b/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala index 8d43c1f88e..5298e14c6f 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala @@ -213,7 +213,7 @@ private[zio] final case class ServerInboundHandler( case Some(cfg) => val headers = req.headers() val headerName = Header.AcceptEncoding.name - cfg.options.exists(opt => headers.containsValue(headerName, opt.kind.name, true)) + cfg.options.exists(opt => headers.containsValue(headerName, opt.name, true)) } } diff --git a/zio-http/shared/src/main/scala/zio/http/Server.scala b/zio-http/shared/src/main/scala/zio/http/Server.scala index 2cc349d3c5..c5e0913b8b 100644 --- a/zio-http/shared/src/main/scala/zio/http/Server.scala +++ b/zio-http/shared/src/main/scala/zio/http/Server.scala @@ -247,13 +247,15 @@ object Server extends ServerPlatformSpecific { ResponseCompressionConfig(0, IndexedSeq(CompressionOptions.gzip(), CompressionOptions.deflate())) } - sealed trait CompressionOptions + sealed trait CompressionOptions { + val name: String + } object CompressionOptions { - final case class GZip(cfg: DeflateConfig) extends CompressionOptions - final case class Deflate(cfg: DeflateConfig) extends CompressionOptions - final case class Brotli(cfg: BrotliConfig) extends CompressionOptions + final case class GZip(cfg: DeflateConfig) extends CompressionOptions { val name = "gzip" } + final case class Deflate(cfg: DeflateConfig) extends CompressionOptions { val name = "deflate" } + final case class Brotli(cfg: BrotliConfig) extends CompressionOptions { val name = "brotli" } /** * @param level