From a4e8aa1942638e8b076a75b2e9b3df3cac371734 Mon Sep 17 00:00:00 2001 From: danicheg Date: Mon, 2 Dec 2024 18:49:21 +0300 Subject: [PATCH 01/13] Use withMaxRequestLength in the example --- .../src/main/scala/com/example/fs2grpc/armeria/Main.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/armeria-fs2grpc/src/main/scala/com/example/fs2grpc/armeria/Main.scala b/examples/armeria-fs2grpc/src/main/scala/com/example/fs2grpc/armeria/Main.scala index 0275b2c..ddb5764 100644 --- a/examples/armeria-fs2grpc/src/main/scala/com/example/fs2grpc/armeria/Main.scala +++ b/examples/armeria-fs2grpc/src/main/scala/com/example/fs2grpc/armeria/Main.scala @@ -61,6 +61,7 @@ object Main extends IOApp { .bindHttp(httpPort) .withIdleTimeout(Duration.Zero) .withRequestTimeout(Duration.Zero) + .withMaxRequestLength(0L) .withHttpServiceUnder("/grpc", grpcService) .withHttpRoutes("/rest", ExampleService[IO].routes()) .withDecorator(LoggingService.newDecorator()) From ef4eee37c36c0a30bc8661424635f01255953667 Mon Sep 17 00:00:00 2001 From: danicheg Date: Tue, 3 Dec 2024 13:16:30 +0300 Subject: [PATCH 02/13] Prevent hanging in ArmeriaServerBuilderSuite --- .../http4s/armeria/server/ArmeriaServerBuilderSuite.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala b/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala index 2e30ef1..b6f68ae 100644 --- a/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala +++ b/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala @@ -24,7 +24,7 @@ import cats.implicits._ import com.linecorp.armeria.client.logging.LoggingClient import com.linecorp.armeria.client.{ClientFactory, WebClient} import com.linecorp.armeria.common.{HttpData, HttpStatus} -import com.linecorp.armeria.server.logging.{ContentPreviewingService, LoggingService} +import com.linecorp.armeria.server.logging.LoggingService import fs2._ import munit.CatsEffectSuite import org.http4s.dsl.io._ @@ -50,7 +50,9 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture { IO(Thread.currentThread.getName).flatMap(Ok(_)) case req @ POST -> Root / "echo" => - Ok(req.body) + req.decode[String] { r => + Ok(r) + } case GET -> Root / "trailers" => Ok("Hello").map(response => @@ -72,7 +74,6 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture { protected def configureServer(serverBuilder: ArmeriaServerBuilder[IO]): ArmeriaServerBuilder[IO] = serverBuilder - .withDecorator(ContentPreviewingService.newDecorator(Int.MaxValue)) .withDecorator(LoggingService.newDecorator()) .bindAny() .withRequestTimeout(10.seconds) From 5f45807c96b3ffd769f84e2b6ae8f692c572c4eb Mon Sep 17 00:00:00 2001 From: danicheg Date: Tue, 3 Dec 2024 13:40:01 +0300 Subject: [PATCH 03/13] Check entity length limiting in tests --- .../server/ArmeriaServerBuilderSuite.scala | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala b/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala index b6f68ae..10e422c 100644 --- a/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala +++ b/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala @@ -78,6 +78,7 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture { .bindAny() .withRequestTimeout(10.seconds) .withGracefulShutdownTimeout(0.seconds, 0.seconds) + .withMaxRequestLength(1024 * 1024) .withHttpRoutes("/service", service) lazy val client: WebClient = WebClient @@ -152,6 +153,16 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture { assertEquals(postChunkedMultipart("/service/issue2610", "aa", body), "a") } + test("reliably handle entity length limiting") { + val input = List.fill(1024 * 1024 + 1)("F").mkString + + val statusIO = IO( + postLargeBody("/service/echo", input) + ) + + assertIO(statusIO, HttpStatus.REQUEST_ENTITY_TOO_LARGE.code()) + } + test("stream") { val response = client.get("/service/stream") val deferred = Deferred.unsafe[IO, Boolean] @@ -177,6 +188,19 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture { } yield () } + private def postLargeBody(path: String, body: String): Int = { + val url = new URL(s"http://127.0.0.1:${httpPort.get}$path") + val conn = url.openConnection().asInstanceOf[HttpURLConnection] + val bytes = body.getBytes(StandardCharsets.UTF_8) + conn.setRequestMethod("POST") + conn.setRequestProperty("Content-Type", "text/html; charset=utf-8") + conn.setDoOutput(true) + conn.getOutputStream.write(bytes) + val code = conn.getResponseCode + conn.disconnect() + code + } + private def postChunkedMultipart(path: String, boundary: String, body: String): String = { val url = new URL(s"http://127.0.0.1:${httpPort.get}$path") val conn = url.openConnection().asInstanceOf[HttpURLConnection] From 970148c56ab5851ebf170dda09545f772aa8352d Mon Sep 17 00:00:00 2001 From: danicheg Date: Sat, 7 Dec 2024 14:15:02 +0300 Subject: [PATCH 04/13] Tweak the ArmeriaServerBuilder#withHttpApp --- .../org/http4s/armeria/server/ArmeriaServerBuilder.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala index 1de785f..2533c2e 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala @@ -169,7 +169,9 @@ sealed class ArmeriaServerBuilder[F[_]] private ( def withHttpApp(prefix: String, service: HttpApp[F]): Self = copy(addServices = (ab, dispatcher) => addServices(ab, dispatcher).map( - _.serviceUnder(prefix, ArmeriaHttp4sHandler(prefix, service, dispatcher)))) + _.serviceUnder( + prefix, + ArmeriaHttp4sHandler(prefix, service, serviceErrorHandler, dispatcher)))) /** Decorates all HTTP services with the specified [[DecoratingFunction]]. */ def withDecorator(decorator: DecoratingFunction): Self = From 26cbeeb5a47b003d97d5a29518faa360022c68d8 Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 8 Dec 2024 20:54:58 +0300 Subject: [PATCH 05/13] Use defaultServiceErrorHandler as the service error handler --- .../scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala index 2533c2e..b67dfb5 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala @@ -361,7 +361,7 @@ object ArmeriaServerBuilder { new ArmeriaServerBuilder( (armeriaBuilder, _) => armeriaBuilder.pure, socketAddress = defaults.IPv4SocketAddress.toInetSocketAddress, - serviceErrorHandler = DefaultServiceErrorHandler, + serviceErrorHandler = defaultServiceErrorHandler[F], banner = defaults.Banner ) } From 098ea7ab7e27f6d7fc4bcbc1b7313214ac7c3ea0 Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 8 Dec 2024 21:25:34 +0300 Subject: [PATCH 06/13] Fix scaladoc reference --- .../armeria/server/ArmeriaServerBuilder.scala | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala index b67dfb5..8c30f79 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala @@ -364,4 +364,25 @@ object ArmeriaServerBuilder { serviceErrorHandler = defaultServiceErrorHandler[F], banner = defaults.Banner ) + + /** Incorporates the default service error handling from Http4s' + * [[org.http4s.server.DefaultServiceErrorHandler DefaultServiceErrorHandler]] and adds handling + * for some errors propagated from the Armeria side. + */ + def defaultServiceErrorHandler[F[_]](implicit + F: Monad[F]): Request[F] => PartialFunction[Throwable, F[Response[F]]] = { + val contentLengthErrorHandler: Request[F] => PartialFunction[Throwable, F[Response[F]]] = + req => { case _: ContentTooLargeException => + Response[F]( + Status.PayloadTooLarge, + req.httpVersion, + Headers( + Connection.close, + `Content-Length`.zero + ) + ).pure[F] + } + + req => contentLengthErrorHandler(req).orElse(DefaultServiceErrorHandler(F)(req)) + } } From ab78eea86e161378964780ca31f4c0500b91900e Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 8 Dec 2024 20:54:22 +0300 Subject: [PATCH 07/13] Add ArmeriaServerBuilder#defaultServiceErrorHandler --- .../armeria/server/ArmeriaServerBuilder.scala | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala index 8c30f79..b1bf33a 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala @@ -16,12 +16,25 @@ package org.http4s.armeria.server +import java.io.{File, InputStream} +import java.net.InetSocketAddress +import java.security.PrivateKey +import java.security.cert.X509Certificate +import java.util.function.{Function => JFunction} +import javax.net.ssl.KeyManagerFactory + +import cats.Monad import cats.effect.{Async, Resource} -import cats.syntax.applicative._ -import cats.syntax.flatMap._ -import cats.syntax.functor._ +import cats.effect.std.Dispatcher +import cats.syntax.all._ import com.linecorp.armeria.common.util.Version -import com.linecorp.armeria.common.{HttpRequest, HttpResponse, SessionProtocol, TlsKeyPair} +import com.linecorp.armeria.common.{ + ContentTooLargeException, + HttpRequest, + HttpResponse, + SessionProtocol, + TlsKeyPair +} import com.linecorp.armeria.server.{ HttpService, HttpServiceWithRoutes, @@ -33,18 +46,10 @@ import com.linecorp.armeria.server.{ import io.micrometer.core.instrument.MeterRegistry import io.netty.channel.ChannelOption import io.netty.handler.ssl.SslContextBuilder - -import java.io.{File, InputStream} -import java.net.InetSocketAddress -import java.security.PrivateKey -import java.security.cert.X509Certificate -import java.util.function.{Function => JFunction} -import cats.effect.std.Dispatcher import com.comcast.ip4s - -import javax.net.ssl.KeyManagerFactory import org.http4s.armeria.server.ArmeriaServerBuilder.AddServices -import org.http4s.{BuildInfo, HttpApp, HttpRoutes} +import org.http4s.headers.{Connection, `Content-Length`} +import org.http4s.{BuildInfo, Headers, HttpApp, HttpRoutes, Request, Response, Status} import org.http4s.server.{ DefaultServiceErrorHandler, Server, @@ -370,7 +375,7 @@ object ArmeriaServerBuilder { * for some errors propagated from the Armeria side. */ def defaultServiceErrorHandler[F[_]](implicit - F: Monad[F]): Request[F] => PartialFunction[Throwable, F[Response[F]]] = { + F: Monad[F], LF: LoggerFactory[F]): Request[F] => PartialFunction[Throwable, F[Response[F]]] = { val contentLengthErrorHandler: Request[F] => PartialFunction[Throwable, F[Response[F]]] = req => { case _: ContentTooLargeException => Response[F]( @@ -383,6 +388,6 @@ object ArmeriaServerBuilder { ).pure[F] } - req => contentLengthErrorHandler(req).orElse(DefaultServiceErrorHandler(F)(req)) + req => contentLengthErrorHandler(req).orElse(DefaultServiceErrorHandler(LF, F)(req)) } } From 33a26745829732271f919b5f34c84167f848b91b Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 15 Dec 2024 15:04:06 +0300 Subject: [PATCH 08/13] Fix ArmeriaHttp4sHandler --- .../org/http4s/armeria/server/ArmeriaHttp4sHandler.scala | 7 +++---- .../org/http4s/armeria/server/ArmeriaServerBuilder.scala | 3 ++- .../http4s/armeria/server/ArmeriaServerBuilderSuite.scala | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala index f24f302..3d48341 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala @@ -43,13 +43,11 @@ import fs2.interop.reactivestreams._ import ArmeriaHttp4sHandler.{RightUnit, canHasBody, defaultVault, toHttp4sMethod} import com.comcast.ip4s.SocketAddress import org.http4s.server.{ - DefaultServiceErrorHandler, SecureSession, ServerRequestKeys, ServiceErrorHandler } import org.typelevel.ci.CIString -import org.typelevel.log4cats.LoggerFactory import scodec.bits.ByteVector import scala.jdk.CollectionConverters._ @@ -257,11 +255,12 @@ private[armeria] class ArmeriaHttp4sHandler[F[_]]( } private[armeria] object ArmeriaHttp4sHandler { - def apply[F[_]: Async: LoggerFactory]( + def apply[F[_]: Async]( prefix: String, service: HttpApp[F], + serviceErrorHandler: ServiceErrorHandler[F], dispatcher: Dispatcher[F]): ArmeriaHttp4sHandler[F] = - new ArmeriaHttp4sHandler(prefix, service, DefaultServiceErrorHandler, dispatcher) + new ArmeriaHttp4sHandler(prefix, service, serviceErrorHandler, dispatcher) private val serverSoftware: ServerSoftware = ServerSoftware("armeria", Some(Version.get("armeria").artifactVersion())) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala index b1bf33a..ac88e2f 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala @@ -375,7 +375,8 @@ object ArmeriaServerBuilder { * for some errors propagated from the Armeria side. */ def defaultServiceErrorHandler[F[_]](implicit - F: Monad[F], LF: LoggerFactory[F]): Request[F] => PartialFunction[Throwable, F[Response[F]]] = { + F: Monad[F], + LF: LoggerFactory[F]): Request[F] => PartialFunction[Throwable, F[Response[F]]] = { val contentLengthErrorHandler: Request[F] => PartialFunction[Throwable, F[Response[F]]] = req => { case _: ContentTooLargeException => Response[F]( diff --git a/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala b/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala index 10e422c..f2af962 100644 --- a/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala +++ b/server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala @@ -50,7 +50,7 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture { IO(Thread.currentThread.getName).flatMap(Ok(_)) case req @ POST -> Root / "echo" => - req.decode[String] { r => + req.decode[IO, String] { r => Ok(r) } From 6e1ceb56262dc0b12dc386a6508667d521baf054 Mon Sep 17 00:00:00 2001 From: danicheg Date: Mon, 2 Dec 2024 18:47:52 +0300 Subject: [PATCH 09/13] Add ArmeriaServerBuilder#withMaxRequestLength --- .../org/http4s/armeria/server/ArmeriaServerBuilder.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala index ac88e2f..4984078 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala @@ -212,6 +212,14 @@ sealed class ArmeriaServerBuilder[F[_]] private ( def withIdleTimeout(idleTimeout: FiniteDuration): Self = atBuild(_.idleTimeoutMillis(idleTimeout.toMillis)) + /** Sets the maximum allowed length of the content decoded at the session layer. + * + * @param limit + * the maximum allowed length. {@code 0} disables the length limit. + */ + def withMaxRequestLength(limit: Long): Self = + atBuild(_.maxRequestLength(limit)) + /** Sets the timeout of a request. * * @param requestTimeout From bec16eb9df2002072b376928dc179e04ba8cb2fa Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 15 Dec 2024 15:05:35 +0300 Subject: [PATCH 10/13] Scalafmt --- .../org/http4s/armeria/server/ArmeriaHttp4sHandler.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala b/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala index 3d48341..80ce607 100644 --- a/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala +++ b/server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala @@ -42,11 +42,7 @@ import fs2._ import fs2.interop.reactivestreams._ import ArmeriaHttp4sHandler.{RightUnit, canHasBody, defaultVault, toHttp4sMethod} import com.comcast.ip4s.SocketAddress -import org.http4s.server.{ - SecureSession, - ServerRequestKeys, - ServiceErrorHandler -} +import org.http4s.server.{SecureSession, ServerRequestKeys, ServiceErrorHandler} import org.typelevel.ci.CIString import scodec.bits.ByteVector From 3780058cdbd5efb34d8aa17632754232b9e2b3b0 Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 15 Dec 2024 14:31:53 +0300 Subject: [PATCH 11/13] Fix link to the license in contributing guide --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c865407..f3cb5a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,3 +10,4 @@ request signifies your consent to license your contributions under the Apache License 2.0. [contributors' guide]: https://http4s.org/contributing/ +[Apache License 2.0]: ./LICENSE From 9751bcd340199fc70e929c4510e330d7f400f21c Mon Sep 17 00:00:00 2001 From: danicheg Date: Sun, 15 Dec 2024 14:43:44 +0300 Subject: [PATCH 12/13] Add current status section to the readme --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index aa5055e..d3309bd 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ - Distributed tracing - [and](https://armeria.dev/docs/server-docservice) [so](https://armeria.dev/docs/server-thrift) [on](https://armeria.dev/docs/advanced-metrics) +# Current status + +Two series are currently under active development: the `0.x` and `1.0-x` release milestone series. +The first depends on the `http4s-core`'s `0.23` series and belongs to the [main branch]. +The latter is for the cutting-edge `http4s-core`'s `1.0-x` release milestone series and belongs to the [series/1.x branch]. + ## Installation Add the following dependencies to `build.sbt` @@ -162,3 +168,5 @@ Visit [examples](./examples) to find a fully working example. [http4s]: https://http4s.org/ [armeria]: https://armeria.dev/ +[main branch]: https://github.com/http4s/http4s-armeria/tree/main +[series/1.x branch]: https://github.com/http4s/http4s-armeria/tree/series/1.x \ No newline at end of file From 82f2933b4f143b89c2ca943980f02df69c0462be Mon Sep 17 00:00:00 2001 From: Daniel Esik Date: Sun, 15 Dec 2024 14:49:16 +0300 Subject: [PATCH 13/13] Fix the header type in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d3309bd..98abe7b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - Distributed tracing - [and](https://armeria.dev/docs/server-docservice) [so](https://armeria.dev/docs/server-thrift) [on](https://armeria.dev/docs/advanced-metrics) -# Current status +## Current status Two series are currently under active development: the `0.x` and `1.0-x` release milestone series. The first depends on the `http4s-core`'s `0.23` series and belongs to the [main branch]. @@ -169,4 +169,4 @@ Visit [examples](./examples) to find a fully working example. [http4s]: https://http4s.org/ [armeria]: https://armeria.dev/ [main branch]: https://github.com/http4s/http4s-armeria/tree/main -[series/1.x branch]: https://github.com/http4s/http4s-armeria/tree/series/1.x \ No newline at end of file +[series/1.x branch]: https://github.com/http4s/http4s-armeria/tree/series/1.x