Skip to content

Commit ead4559

Browse files
authored
Merge pull request #583 from danicheg/propagate-features-from-main
Propagate the recent features from the `0.5` series
2 parents e5d54a8 + 82f2933 commit ead4559

File tree

6 files changed

+95
-28
lines changed

6 files changed

+95
-28
lines changed

CONTRIBUTING.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ request signifies your consent to license your contributions under the
1010
Apache License 2.0.
1111

1212
[contributors' guide]: https://http4s.org/contributing/
13+
[Apache License 2.0]: ./LICENSE

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
- Distributed tracing
1919
- [and](https://armeria.dev/docs/server-docservice) [so](https://armeria.dev/docs/server-thrift) [on](https://armeria.dev/docs/advanced-metrics)
2020

21+
## Current status
22+
23+
Two series are currently under active development: the `0.x` and `1.0-x` release milestone series.
24+
The first depends on the `http4s-core`'s `0.23` series and belongs to the [main branch].
25+
The latter is for the cutting-edge `http4s-core`'s `1.0-x` release milestone series and belongs to the [series/1.x branch].
26+
2127
## Installation
2228

2329
Add the following dependencies to `build.sbt`
@@ -162,3 +168,5 @@ Visit [examples](./examples) to find a fully working example.
162168

163169
[http4s]: https://http4s.org/
164170
[armeria]: https://armeria.dev/
171+
[main branch]: https://github.com/http4s/http4s-armeria/tree/main
172+
[series/1.x branch]: https://github.com/http4s/http4s-armeria/tree/series/1.x

examples/armeria-fs2grpc/src/main/scala/com/example/fs2grpc/armeria/Main.scala

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ object Main extends IOApp {
6161
.bindHttp(httpPort)
6262
.withIdleTimeout(Duration.Zero)
6363
.withRequestTimeout(Duration.Zero)
64+
.withMaxRequestLength(0L)
6465
.withHttpServiceUnder("/grpc", grpcService)
6566
.withHttpRoutes("/rest", ExampleService[IO].routes())
6667
.withDecorator(LoggingService.newDecorator())

server/src/main/scala/org/http4s/armeria/server/ArmeriaHttp4sHandler.scala

+4-9
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,8 @@ import fs2._
4242
import fs2.interop.reactivestreams._
4343
import ArmeriaHttp4sHandler.{RightUnit, canHasBody, defaultVault, toHttp4sMethod}
4444
import com.comcast.ip4s.SocketAddress
45-
import org.http4s.server.{
46-
DefaultServiceErrorHandler,
47-
SecureSession,
48-
ServerRequestKeys,
49-
ServiceErrorHandler
50-
}
45+
import org.http4s.server.{SecureSession, ServerRequestKeys, ServiceErrorHandler}
5146
import org.typelevel.ci.CIString
52-
import org.typelevel.log4cats.LoggerFactory
5347
import scodec.bits.ByteVector
5448

5549
import scala.jdk.CollectionConverters._
@@ -257,11 +251,12 @@ private[armeria] class ArmeriaHttp4sHandler[F[_]](
257251
}
258252

259253
private[armeria] object ArmeriaHttp4sHandler {
260-
def apply[F[_]: Async: LoggerFactory](
254+
def apply[F[_]: Async](
261255
prefix: String,
262256
service: HttpApp[F],
257+
serviceErrorHandler: ServiceErrorHandler[F],
263258
dispatcher: Dispatcher[F]): ArmeriaHttp4sHandler[F] =
264-
new ArmeriaHttp4sHandler(prefix, service, DefaultServiceErrorHandler, dispatcher)
259+
new ArmeriaHttp4sHandler(prefix, service, serviceErrorHandler, dispatcher)
265260

266261
private val serverSoftware: ServerSoftware =
267262
ServerSoftware("armeria", Some(Version.get("armeria").artifactVersion()))

server/src/main/scala/org/http4s/armeria/server/ArmeriaServerBuilder.scala

+53-16
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,25 @@
1616

1717
package org.http4s.armeria.server
1818

19+
import java.io.{File, InputStream}
20+
import java.net.InetSocketAddress
21+
import java.security.PrivateKey
22+
import java.security.cert.X509Certificate
23+
import java.util.function.{Function => JFunction}
24+
import javax.net.ssl.KeyManagerFactory
25+
26+
import cats.Monad
1927
import cats.effect.{Async, Resource}
20-
import cats.syntax.applicative._
21-
import cats.syntax.flatMap._
22-
import cats.syntax.functor._
28+
import cats.effect.std.Dispatcher
29+
import cats.syntax.all._
2330
import com.linecorp.armeria.common.util.Version
24-
import com.linecorp.armeria.common.{HttpRequest, HttpResponse, SessionProtocol, TlsKeyPair}
31+
import com.linecorp.armeria.common.{
32+
ContentTooLargeException,
33+
HttpRequest,
34+
HttpResponse,
35+
SessionProtocol,
36+
TlsKeyPair
37+
}
2538
import com.linecorp.armeria.server.{
2639
HttpService,
2740
HttpServiceWithRoutes,
@@ -33,18 +46,10 @@ import com.linecorp.armeria.server.{
3346
import io.micrometer.core.instrument.MeterRegistry
3447
import io.netty.channel.ChannelOption
3548
import io.netty.handler.ssl.SslContextBuilder
36-
37-
import java.io.{File, InputStream}
38-
import java.net.InetSocketAddress
39-
import java.security.PrivateKey
40-
import java.security.cert.X509Certificate
41-
import java.util.function.{Function => JFunction}
42-
import cats.effect.std.Dispatcher
4349
import com.comcast.ip4s
44-
45-
import javax.net.ssl.KeyManagerFactory
4650
import org.http4s.armeria.server.ArmeriaServerBuilder.AddServices
47-
import org.http4s.{BuildInfo, HttpApp, HttpRoutes}
51+
import org.http4s.headers.{Connection, `Content-Length`}
52+
import org.http4s.{BuildInfo, Headers, HttpApp, HttpRoutes, Request, Response, Status}
4853
import org.http4s.server.{
4954
DefaultServiceErrorHandler,
5055
Server,
@@ -169,7 +174,9 @@ sealed class ArmeriaServerBuilder[F[_]] private (
169174
def withHttpApp(prefix: String, service: HttpApp[F]): Self =
170175
copy(addServices = (ab, dispatcher) =>
171176
addServices(ab, dispatcher).map(
172-
_.serviceUnder(prefix, ArmeriaHttp4sHandler(prefix, service, dispatcher))))
177+
_.serviceUnder(
178+
prefix,
179+
ArmeriaHttp4sHandler(prefix, service, serviceErrorHandler, dispatcher))))
173180

174181
/** Decorates all HTTP services with the specified [[DecoratingFunction]]. */
175182
def withDecorator(decorator: DecoratingFunction): Self =
@@ -205,6 +212,14 @@ sealed class ArmeriaServerBuilder[F[_]] private (
205212
def withIdleTimeout(idleTimeout: FiniteDuration): Self =
206213
atBuild(_.idleTimeoutMillis(idleTimeout.toMillis))
207214

215+
/** Sets the maximum allowed length of the content decoded at the session layer.
216+
*
217+
* @param limit
218+
* the maximum allowed length. {@code 0} disables the length limit.
219+
*/
220+
def withMaxRequestLength(limit: Long): Self =
221+
atBuild(_.maxRequestLength(limit))
222+
208223
/** Sets the timeout of a request.
209224
*
210225
* @param requestTimeout
@@ -359,7 +374,29 @@ object ArmeriaServerBuilder {
359374
new ArmeriaServerBuilder(
360375
(armeriaBuilder, _) => armeriaBuilder.pure,
361376
socketAddress = defaults.IPv4SocketAddress.toInetSocketAddress,
362-
serviceErrorHandler = DefaultServiceErrorHandler,
377+
serviceErrorHandler = defaultServiceErrorHandler[F],
363378
banner = defaults.Banner
364379
)
380+
381+
/** Incorporates the default service error handling from Http4s'
382+
* [[org.http4s.server.DefaultServiceErrorHandler DefaultServiceErrorHandler]] and adds handling
383+
* for some errors propagated from the Armeria side.
384+
*/
385+
def defaultServiceErrorHandler[F[_]](implicit
386+
F: Monad[F],
387+
LF: LoggerFactory[F]): Request[F] => PartialFunction[Throwable, F[Response[F]]] = {
388+
val contentLengthErrorHandler: Request[F] => PartialFunction[Throwable, F[Response[F]]] =
389+
req => { case _: ContentTooLargeException =>
390+
Response[F](
391+
Status.PayloadTooLarge,
392+
req.httpVersion,
393+
Headers(
394+
Connection.close,
395+
`Content-Length`.zero
396+
)
397+
).pure[F]
398+
}
399+
400+
req => contentLengthErrorHandler(req).orElse(DefaultServiceErrorHandler(LF, F)(req))
401+
}
365402
}

server/src/test/scala/org/http4s/armeria/server/ArmeriaServerBuilderSuite.scala

+28-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import cats.implicits._
2424
import com.linecorp.armeria.client.logging.LoggingClient
2525
import com.linecorp.armeria.client.{ClientFactory, WebClient}
2626
import com.linecorp.armeria.common.{HttpData, HttpStatus}
27-
import com.linecorp.armeria.server.logging.{ContentPreviewingService, LoggingService}
27+
import com.linecorp.armeria.server.logging.LoggingService
2828
import fs2._
2929
import munit.CatsEffectSuite
3030
import org.http4s.dsl.io._
@@ -50,7 +50,9 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture {
5050
IO(Thread.currentThread.getName).flatMap(Ok(_))
5151

5252
case req @ POST -> Root / "echo" =>
53-
Ok(req.body)
53+
req.decode[IO, String] { r =>
54+
Ok(r)
55+
}
5456

5557
case GET -> Root / "trailers" =>
5658
Ok("Hello").map(response =>
@@ -72,11 +74,11 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture {
7274

7375
protected def configureServer(serverBuilder: ArmeriaServerBuilder[IO]): ArmeriaServerBuilder[IO] =
7476
serverBuilder
75-
.withDecorator(ContentPreviewingService.newDecorator(Int.MaxValue))
7677
.withDecorator(LoggingService.newDecorator())
7778
.bindAny()
7879
.withRequestTimeout(10.seconds)
7980
.withGracefulShutdownTimeout(0.seconds, 0.seconds)
81+
.withMaxRequestLength(1024 * 1024)
8082
.withHttpRoutes("/service", service)
8183

8284
lazy val client: WebClient = WebClient
@@ -151,6 +153,16 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture {
151153
assertEquals(postChunkedMultipart("/service/issue2610", "aa", body), "a")
152154
}
153155

156+
test("reliably handle entity length limiting") {
157+
val input = List.fill(1024 * 1024 + 1)("F").mkString
158+
159+
val statusIO = IO(
160+
postLargeBody("/service/echo", input)
161+
)
162+
163+
assertIO(statusIO, HttpStatus.REQUEST_ENTITY_TOO_LARGE.code())
164+
}
165+
154166
test("stream") {
155167
val response = client.get("/service/stream")
156168
val deferred = Deferred.unsafe[IO, Boolean]
@@ -176,6 +188,19 @@ class ArmeriaServerBuilderSuite extends CatsEffectSuite with ServerFixture {
176188
} yield ()
177189
}
178190

191+
private def postLargeBody(path: String, body: String): Int = {
192+
val url = new URL(s"http://127.0.0.1:${httpPort.get}$path")
193+
val conn = url.openConnection().asInstanceOf[HttpURLConnection]
194+
val bytes = body.getBytes(StandardCharsets.UTF_8)
195+
conn.setRequestMethod("POST")
196+
conn.setRequestProperty("Content-Type", "text/html; charset=utf-8")
197+
conn.setDoOutput(true)
198+
conn.getOutputStream.write(bytes)
199+
val code = conn.getResponseCode
200+
conn.disconnect()
201+
code
202+
}
203+
179204
private def postChunkedMultipart(path: String, boundary: String, body: String): String = {
180205
val url = new URL(s"http://127.0.0.1:${httpPort.get}$path")
181206
val conn = url.openConnection().asInstanceOf[HttpURLConnection]

0 commit comments

Comments
 (0)