Skip to content

Commit 0cd8cbe

Browse files
authored
Merge pull request #379 from http4s/0.24.0-RC2-to-0.25
2 parents c731f9e + 6174fd4 commit 0cd8cbe

File tree

12 files changed

+373
-277
lines changed

12 files changed

+373
-277
lines changed

.github/workflows/ci.yml

Lines changed: 170 additions & 132 deletions
Large diffs are not rendered by default.

.scalafmt.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version = 3.6.1
1+
version = 3.9.4
22

33
style = default
44

build.sbt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,36 @@ ThisBuild / developers := List(
1010
// publish website from this branch
1111
ThisBuild / tlSitePublishBranch := Some("main")
1212

13-
val Scala213 = "2.13.10"
14-
ThisBuild / crossScalaVersions := Seq("2.12.17", Scala213, "3.2.1")
13+
val Scala213 = "2.13.16"
14+
ThisBuild / crossScalaVersions := Seq("2.12.20", Scala213, "3.3.5")
1515
ThisBuild / scalaVersion := Scala213 // the default Scala
1616

1717
// Jetty 10+, for testing, requires Java 11.
1818
ThisBuild / githubWorkflowJavaVersions -= JavaSpec.temurin("8")
1919
ThisBuild / tlJdkRelease := Some(8)
20+
ThisBuild / startYear := Some(2013)
2021

2122
lazy val root = tlCrossRootProject.aggregate(servlet, examples)
2223

2324
val asyncHttpClientVersion = "2.12.3"
2425
val jettyVersion = "11.0.15"
25-
val http4sVersion = "0.23.17"
26-
val munitCatsEffectVersion = "1.0.7"
26+
val http4sVersion = "0.23.30"
27+
val munitCatsEffectVersion = "2.1.0"
2728
val servletApiVersion = "5.0.0"
2829

2930
lazy val servlet = project
3031
.in(file("servlet"))
3132
.settings(
3233
name := "http4s-servlet",
3334
description := "Portable servlet implementation for http4s servers",
34-
startYear := Some(2013),
3535
libraryDependencies ++= Seq(
3636
"jakarta.servlet" % "jakarta.servlet-api" % servletApiVersion % Provided,
3737
"org.eclipse.jetty" % "jetty-client" % jettyVersion % Test,
3838
"org.eclipse.jetty" % "jetty-server" % jettyVersion % Test,
3939
"org.eclipse.jetty" % "jetty-servlet" % jettyVersion % Test,
4040
"org.http4s" %% "http4s-dsl" % http4sVersion % Test,
4141
"org.http4s" %% "http4s-server" % http4sVersion,
42-
"org.typelevel" %% "munit-cats-effect-3" % munitCatsEffectVersion % Test,
42+
"org.typelevel" %% "munit-cats-effect" % munitCatsEffectVersion % Test,
4343
),
4444
)
4545

@@ -59,4 +59,4 @@ lazy val examples = project
5959
)
6060
.dependsOn(servlet)
6161

62-
lazy val docs = project.in(file("site")).enablePlugins(TypelevelSitePlugin)
62+
lazy val docs = project.in(file("site")).enablePlugins(Http4sOrgSitePlugin)

flake.lock

Lines changed: 35 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

project/build.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sbt.version=1.8.1
1+
sbt.version=1.10.11

project/plugins.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.2.4")
2-
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.14.9")
1+
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.2.5")
2+
addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.18.0")

servlet/src/main/scala/org/http4s/servlet/AsyncHttp4sServlet.scala

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import org.http4s.server._
2929
import scala.annotation.nowarn
3030
import scala.concurrent.duration.Duration
3131

32-
class AsyncHttp4sServlet[F[_]] @deprecated("Use AsyncHttp4sServlet.builder", "0.23.17") (
32+
class AsyncHttp4sServlet[F[_]] @deprecated("Use AsyncHttp4sServlet.builder", "0.23.18") (
3333
httpApp: HttpApp[F],
3434
asyncTimeout: Duration = Duration.Inf,
3535
servletIo: ServletIo[F],
@@ -62,20 +62,18 @@ class AsyncHttp4sServlet[F[_]] @deprecated("Use AsyncHttp4sServlet.builder", "0.
6262
ctx.setTimeout(asyncTimeoutMillis)
6363
// Must be done on the container thread for Tomcat's sake when using async I/O.
6464
val bodyWriter = servletIo.bodyWriter(servletResponse, dispatcher) _
65-
val result = F
66-
.attempt(
67-
toRequest(servletRequest).fold(
68-
onParseFailure(_, servletResponse, bodyWriter),
65+
val result =
66+
toRequest(servletRequest)
67+
.fold(
68+
onParseFailure(_, servletResponse),
6969
handleRequest(ctx, _, bodyWriter),
7070
)
71-
)
72-
.flatMap {
73-
case Right(()) => F.delay(ctx.complete)
74-
case Left(t) => errorHandler(servletRequest, servletResponse)(t)
75-
}
71+
.recoverWith(errorHandler(servletRequest, servletResponse))
7672
dispatcher.unsafeRunAndForget(result)
7773
} catch errorHandler(servletRequest, servletResponse).andThen(dispatcher.unsafeRunSync _)
7874

75+
private[this] val noopCancelToken = Some(F.unit)
76+
7977
private def handleRequest(
8078
ctx: AsyncContext,
8179
request: Request[F],
@@ -85,17 +83,23 @@ class AsyncHttp4sServlet[F[_]] @deprecated("Use AsyncHttp4sServlet.builder", "0.
8583
// It is an error to add a listener to an async context that is
8684
// already completed, so we must take care to add the listener
8785
// before the response can complete.
88-
8986
val timeout =
90-
F.async[Response[F]](cb =>
91-
gate.complete(ctx.addListener(new AsyncTimeoutHandler(cb))).as(Option.empty[F[Unit]])
87+
F.async[Unit](cb =>
88+
gate.complete(ctx.addListener(new AsyncTimeoutHandler(cb))).as(noopCancelToken)
9289
)
9390
val response =
9491
gate.get *>
9592
F.defer(serviceFn(request))
9693
.recoverWith(serviceErrorHandler(request))
97-
val servletResponse = ctx.getResponse.asInstanceOf[HttpServletResponse]
98-
F.race(timeout, response).flatMap(r => renderResponse(r.merge, servletResponse, bodyWriter))
94+
F.race(timeout, response).flatMap {
95+
case Left(_) =>
96+
// In Jetty, if onTimeout is called, we need to complete on the
97+
// listener's own thread.
98+
F.unit
99+
case Right(resp) =>
100+
val servletResponse = ctx.getResponse.asInstanceOf[HttpServletResponse]
101+
renderResponse(resp, servletResponse, bodyWriter) *> F.delay(ctx.complete())
102+
}
99103
}
100104

101105
private def errorHandler(
@@ -122,11 +126,19 @@ class AsyncHttp4sServlet[F[_]] @deprecated("Use AsyncHttp4sServlet.builder", "0.
122126
}
123127
}
124128

125-
private class AsyncTimeoutHandler(cb: Callback[Response[F]]) extends AbstractAsyncListener {
129+
private class AsyncTimeoutHandler(cb: Callback[Unit]) extends AbstractAsyncListener {
126130
override def onTimeout(event: AsyncEvent): Unit = {
131+
// In Jetty, we must complete on the same thread as the timeout
132+
// handler. This triggers a cancellation of the service so we
133+
// can take over.
134+
cb(Right(()))
135+
136+
val ctx = event.getAsyncContext
127137
val req = event.getAsyncContext.getRequest.asInstanceOf[HttpServletRequest]
128138
logger.info(s"Request timed out: ${req.getMethod} ${req.getServletPath}${req.getPathInfo}")
129-
cb(Right(Response.timeout[F]))
139+
val resp = event.getAsyncContext.getResponse.asInstanceOf[HttpServletResponse]
140+
resp.sendError(Response.timeout.status.code, "Response timed out")
141+
ctx.complete()
130142
}
131143
}
132144
}

servlet/src/main/scala/org/http4s/servlet/BlockingHttp4sServlet.scala

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import jakarta.servlet.http.HttpServletRequest
2424
import jakarta.servlet.http.HttpServletResponse
2525
import org.http4s.server._
2626

27+
import scala.annotation.nowarn
28+
2729
class BlockingHttp4sServlet[F[_]] private (
2830
service: HttpApp[F],
2931
servletIo: ServletIo[F],
@@ -32,6 +34,7 @@ class BlockingHttp4sServlet[F[_]] private (
3234
)(implicit F: Sync[F])
3335
extends Http4sServlet[F](service, servletIo, dispatcher) {
3436

37+
@deprecated("Use BlockingHttp4sServlet.builder", "0.24.0")
3538
def this(
3639
service: HttpApp[F],
3740
servletIo: BlockingServletIo[F],
@@ -49,7 +52,7 @@ class BlockingHttp4sServlet[F[_]] private (
4952
val bodyWriter = servletIo.bodyWriter(servletResponse, dispatcher) _
5053

5154
val render = toRequest(servletRequest).fold(
52-
onParseFailure(_, servletResponse, bodyWriter),
55+
onParseFailure(_, servletResponse),
5356
handleRequest(_, servletResponse, bodyWriter),
5457
)
5558

@@ -85,12 +88,55 @@ class BlockingHttp4sServlet[F[_]] private (
8588
}
8689

8790
object BlockingHttp4sServlet {
88-
def apply[F[_]: Sync](
91+
92+
class Builder[F[_]] private[BlockingHttp4sServlet] (
93+
httpApp: HttpApp[F],
94+
dispatcher: Dispatcher[F],
95+
chunkSize: Option[Int],
96+
) {
97+
private def copy(
98+
httpApp: HttpApp[F] = httpApp,
99+
dispatcher: Dispatcher[F] = dispatcher,
100+
chunkSize: Option[Int] = chunkSize,
101+
): Builder[F] =
102+
new Builder[F](
103+
httpApp,
104+
dispatcher,
105+
chunkSize,
106+
) {}
107+
108+
@nowarn("cat=deprecation")
109+
def build(implicit F: Sync[F]): BlockingHttp4sServlet[F] =
110+
new BlockingHttp4sServlet(
111+
httpApp,
112+
BlockingServletIo(chunkSize.getOrElse(DefaultChunkSize)),
113+
DefaultServiceErrorHandler,
114+
dispatcher,
115+
)
116+
117+
def withHttpApp(httpApp: HttpApp[F]): Builder[F] =
118+
copy(httpApp = httpApp)
119+
120+
def withDispatcher(dispatcher: Dispatcher[F]): Builder[F] =
121+
copy(dispatcher = dispatcher)
122+
123+
def withChunkSize(chunkSize: Int): Builder[F] =
124+
copy(chunkSize = Some(chunkSize))
125+
}
126+
127+
def builder[F[_]](httpApp: HttpApp[F], dispatcher: Dispatcher[F]): Builder[F] =
128+
new Builder[F](httpApp, dispatcher, None) {}
129+
130+
@deprecated(
131+
"Use `builder` instead",
132+
"0.24.0",
133+
)
134+
def apply[F[_]](
89135
service: HttpApp[F],
90136
servletIo: ServletIo[F],
91137
dispatcher: Dispatcher[F],
92-
): BlockingHttp4sServlet[F] =
93-
new BlockingHttp4sServlet[F](
138+
)(implicit F: Sync[F]): BlockingHttp4sServlet[F] =
139+
new BlockingHttp4sServlet(
94140
service,
95141
servletIo,
96142
DefaultServiceErrorHandler,

servlet/src/main/scala/org/http4s/servlet/Http4sServlet.scala

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,19 @@ abstract class Http4sServlet[F[_]](
6868
serverSoftware = ServerSoftware(servletContext.getServerInfo)
6969
}
7070

71+
@deprecated("Use the overload without bodyWriter.", "0.23.15")
7172
protected def onParseFailure(
7273
parseFailure: ParseFailure,
7374
servletResponse: HttpServletResponse,
7475
bodyWriter: BodyWriter[F],
75-
): F[Unit] = {
76-
val response = Response[F](Status.BadRequest).withEntity(parseFailure.sanitized)
77-
renderResponse(response, servletResponse, bodyWriter)
78-
}
76+
): F[Unit] =
77+
onParseFailure(parseFailure, servletResponse)
78+
79+
protected def onParseFailure(
80+
parseFailure: ParseFailure,
81+
servletResponse: HttpServletResponse,
82+
): F[Unit] =
83+
F.delay(servletResponse.sendError(Status.BadRequest.code, parseFailure.sanitized))
7984

8085
protected def renderResponse(
8186
response: Response[F],

0 commit comments

Comments
 (0)