diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c5dd3641ba..a69572a7e4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout current branch (full)
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -94,7 +94,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -187,7 +187,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
diff --git a/build.sbt b/build.sbt
index 8117ad4479..8f07e8758e 100644
--- a/build.sbt
+++ b/build.sbt
@@ -198,8 +198,8 @@ lazy val zioHttpBenchmarks = (project in file("zio-http-benchmarks"))
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "1.5.1",
"com.softwaremill.sttp.client3" %% "core" % "3.9.0",
// "dev.zio" %% "zio-interop-cats" % "3.3.0",
- "org.slf4j" % "slf4j-api" % "2.0.7",
- "org.slf4j" % "slf4j-simple" % "2.0.7",
+ "org.slf4j" % "slf4j-api" % "2.0.9",
+ "org.slf4j" % "slf4j-simple" % "2.0.9",
),
)
.dependsOn(zioHttp)
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index bda3f2675c..f065a2a497 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -6,7 +6,7 @@ object Dependencies {
val NettyVersion = "4.1.93.Final"
val NettyIncubatorVersion = "0.0.20.Final"
val ScalaCompactCollectionVersion = "2.11.0"
- val ZioVersion = "2.0.16"
+ val ZioVersion = "2.0.17"
val ZioCliVersion = "0.5.0"
val ZioSchemaVersion = "0.4.13"
val SttpVersion = "3.3.18"
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 2ce0487449..e909d62bf4 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -3,7 +3,7 @@ addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3")
addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0")
-addSbtPlugin("com.github.sbt" % "sbt-github-actions" % "0.15.0")
+addSbtPlugin("com.github.sbt" % "sbt-github-actions" % "0.16.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.5.1")
addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")
addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.3.10")
diff --git a/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala b/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala
index 0764282e8b..5c7ba70f94 100644
--- a/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala
+++ b/zio-http-cli/src/test/scala/zio/http/endpoint/cli/EndpointGen.scala
@@ -106,7 +106,7 @@ object EndpointGen {
List(transformOrFail, withDoc, withExamples).map(_.asInstanceOf[Mapper[CliReprOf[Codec[_]], Any]])
def transformOrFail[A] = Mapper[CliReprOf[Codec[A]], Any](
- (repr, _: Any) => CliRepr(repr.value.transform((x: A) => x, (x: A) => x), repr.repr),
+ (repr, _: Any) => CliRepr(repr.value.transform((x: A) => x)((x: A) => x), repr.repr),
Gen.empty,
)
diff --git a/zio-http/src/main/scala/zio/http/codec/HeaderCodecs.scala b/zio-http/src/main/scala/zio/http/codec/HeaderCodecs.scala
index b758507bc5..2149e73c38 100644
--- a/zio-http/src/main/scala/zio/http/codec/HeaderCodecs.scala
+++ b/zio-http/src/main/scala/zio/http/codec/HeaderCodecs.scala
@@ -29,7 +29,7 @@ private[codec] trait HeaderCodecs {
def header(headerType: HeaderType): HeaderCodec[headerType.HeaderValue] =
headerCodec(headerType.name, TextCodec.string)
- .transformOrFailLeft(headerType.parse(_), headerType.render(_))
+ .transformOrFailLeft(headerType.parse(_))(headerType.render(_))
def name[A](name: String)(implicit codec: TextCodec[A]): HeaderCodec[A] =
headerCodec(name, codec)
@@ -39,20 +39,19 @@ private[codec] trait HeaderCodecs {
parse: B => A,
render: A => B,
)(implicit codec: TextCodec[B]): HeaderCodec[A] =
- headerCodec(name, codec).transformOrFailLeft(
- s => Try(parse(s)).toEither.left.map(e => s"Failed to parse header $name: ${e.getMessage}"),
- render,
- )
+ headerCodec(name, codec).transformOrFailLeft(s =>
+ Try(parse(s)).toEither.left.map(e => s"Failed to parse header $name: ${e.getMessage}"),
+ )(render)
def nameTransformOption[A, B](name: String, parse: B => Option[A], render: A => B)(implicit
codec: TextCodec[B],
): HeaderCodec[A] =
- headerCodec(name, codec).transformOrFailLeft(parse(_).toRight(s"Failed to parse header $name"), render)
+ headerCodec(name, codec).transformOrFailLeft(parse(_).toRight(s"Failed to parse header $name"))(render)
def nameTransformOrFail[A, B](name: String, parse: B => Either[String, A], render: A => B)(implicit
codec: TextCodec[B],
): HeaderCodec[A] =
- headerCodec(name, codec).transformOrFailLeft(parse, render)
+ headerCodec(name, codec).transformOrFailLeft(parse)(render)
final val accept: HeaderCodec[Header.Accept] = header(Header.Accept)
final val acceptEncoding: HeaderCodec[Header.AcceptEncoding] = header(Header.AcceptEncoding)
diff --git a/zio-http/src/main/scala/zio/http/codec/HttpCodec.scala b/zio-http/src/main/scala/zio/http/codec/HttpCodec.scala
index 08560ea1ee..f80665ac30 100644
--- a/zio-http/src/main/scala/zio/http/codec/HttpCodec.scala
+++ b/zio-http/src/main/scala/zio/http/codec/HttpCodec.scala
@@ -93,14 +93,12 @@ sealed trait HttpCodec[-AtomTypes, Value] {
else {
HttpCodec
.Fallback(self, that)
- .transform[alternator.Out](
- either => either.fold(alternator.left(_), alternator.right(_)),
- value =>
- alternator
- .unleft(value)
- .map(Left(_))
- .orElse(alternator.unright(value).map(Right(_)))
- .get, // TODO: Solve with partiality
+ .transform[alternator.Out](either => either.fold(alternator.left(_), alternator.right(_)))(value =>
+ alternator
+ .unleft(value)
+ .map(Left(_))
+ .orElse(alternator.unright(value).map(Right(_)))
+ .get, // TODO: Solve with partiality
)
}
}
@@ -171,10 +169,10 @@ sealed trait HttpCodec[-AtomTypes, Value] {
* semantically distinct values.
*/
final def const(canonical: => Value): HttpCodec[AtomTypes, Unit] =
- self.transform(_ => (), _ => canonical)
+ self.transform(_ => ())(_ => canonical)
final def const[Value2](value2: => Value2)(implicit ev: Unit <:< Value): HttpCodec[AtomTypes, Value2] =
- self.transform(_ => value2, _ => ev(()))
+ self.transform(_ => value2)(_ => ev(()))
/**
* Uses this codec to decode the Scala value from a request.
@@ -253,12 +251,10 @@ sealed trait HttpCodec[-AtomTypes, Value] {
* value.
*/
def expect(expected: Value): HttpCodec[AtomTypes, Unit] =
- transformOrFailLeft(
- actual =>
- if (actual == expected) Right(())
- else Left(s"Expected ${expected} but found ${actual}"),
- _ => expected,
- )
+ transformOrFailLeft(actual =>
+ if (actual == expected) Right(())
+ else Left(s"Expected ${expected} but found ${actual}"),
+ )(_ => expected)
def named(name: String): HttpCodec[AtomTypes, Value] =
HttpCodec.Annotated(self, Metadata.Named(name))
@@ -277,7 +273,7 @@ sealed trait HttpCodec[-AtomTypes, Value] {
Annotated(
self
.orElseEither(HttpCodec.empty)
- .transform[Option[Value]](_.swap.toOption, _.fold[Either[Unit, Value]](Left(()))(Right(_)).swap),
+ .transform(_.swap.toOption)(_.fold[Either[Unit, Value]](Left(()))(Right(_)).swap),
Metadata.Optional(),
)
@@ -287,15 +283,13 @@ sealed trait HttpCodec[-AtomTypes, Value] {
self | that
final def toLeft[R]: HttpCodec[AtomTypes, Either[Value, R]] =
- self.transformOrFail[Either[Value, R]](
- value => Right(Left(value)),
- either => either.swap.left.map(_ => "Error!"),
+ self.transformOrFail[Either[Value, R]](value => Right(Left(value)))(either =>
+ either.swap.left.map(_ => "Error!"),
) // TODO: Solve with partiality
final def toRight[L]: HttpCodec[AtomTypes, Either[L, Value]] =
- self.transformOrFail[Either[L, Value]](
- value => Right(Right(value)),
- either => either.left.map(_ => "Error!"),
+ self.transformOrFail[Either[L, Value]](value => Right(Right(value)))(either =>
+ either.left.map(_ => "Error!"),
) // TODO: Solve with partiality
/**
@@ -309,23 +303,20 @@ sealed trait HttpCodec[-AtomTypes, Value] {
* used in encoding, for example, when a client calls the endpoint on the
* server.
*/
- final def transform[Value2](f: Value => Value2, g: Value2 => Value): HttpCodec[AtomTypes, Value2] =
+ final def transform[Value2](f: Value => Value2)(g: Value2 => Value): HttpCodec[AtomTypes, Value2] =
HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, in => Right(f(in)), output => Right(g(output)))
- final def transformOrFail[Value2](
- f: Value => Either[String, Value2],
+ final def transformOrFail[Value2](f: Value => Either[String, Value2])(
g: Value2 => Either[String, Value],
): HttpCodec[AtomTypes, Value2] =
HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, f, g)
- final def transformOrFailLeft[Value2](
- f: Value => Either[String, Value2],
+ final def transformOrFailLeft[Value2](f: Value => Either[String, Value2])(
g: Value2 => Value,
): HttpCodec[AtomTypes, Value2] =
HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, f, output => Right(g(output)))
- final def transformOrFailRight[Value2](
- f: Value => Value2,
+ final def transformOrFailRight[Value2](f: Value => Value2)(
g: Value2 => Either[String, Value],
): HttpCodec[AtomTypes, Value2] =
HttpCodec.TransformOrFail[AtomTypes, Value, Value2](self, in => Right(f(in)), g)
@@ -360,14 +351,12 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec1: HttpCodec[AtomTypes, Sub1],
codec2: HttpCodec[AtomTypes, Sub2],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2).transformOrFail(
- either => Right(either.merge),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(sub1))
- case sub2: Sub2 => Right(Right(sub2))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2).transformOrFail(either => Right(either.merge))((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(sub1))
+ case sub2: Sub2 => Right(Right(sub2))
+ case _ => Left(s"Unexpected error type")
+ },
)
def apply[AtomTypes, Sub1 <: Value: ClassTag, Sub2 <: Value: ClassTag, Sub3 <: Value: ClassTag](
@@ -375,15 +364,13 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec2: HttpCodec[AtomTypes, Sub2],
codec3: HttpCodec[AtomTypes, Sub3],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2 | codec3).transformOrFail(
- either => Right(either.left.map(_.merge).merge),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(Left(sub1)))
- case sub2: Sub2 => Right(Left(Right(sub2)))
- case sub3: Sub3 => Right(Right(sub3))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2 | codec3).transformOrFail(either => Right(either.left.map(_.merge).merge))((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(Left(sub1)))
+ case sub2: Sub2 => Right(Left(Right(sub2)))
+ case sub3: Sub3 => Right(Right(sub3))
+ case _ => Left(s"Unexpected error type")
+ },
)
def apply[
@@ -398,16 +385,16 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec3: HttpCodec[AtomTypes, Sub3],
codec4: HttpCodec[AtomTypes, Sub4],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2 | codec3 | codec4).transformOrFail(
- either => Right(either.left.map(_.left.map(_.merge).merge).merge),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(Left(Left(sub1))))
- case sub2: Sub2 => Right(Left(Left(Right(sub2))))
- case sub3: Sub3 => Right(Left(Right(sub3)))
- case sub4: Sub4 => Right(Right(sub4))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2 | codec3 | codec4).transformOrFail(either =>
+ Right(either.left.map(_.left.map(_.merge).merge).merge),
+ )((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(Left(Left(sub1))))
+ case sub2: Sub2 => Right(Left(Left(Right(sub2))))
+ case sub3: Sub3 => Right(Left(Right(sub3)))
+ case sub4: Sub4 => Right(Right(sub4))
+ case _ => Left(s"Unexpected error type")
+ },
)
def apply[
@@ -424,17 +411,17 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec4: HttpCodec[AtomTypes, Sub4],
codec5: HttpCodec[AtomTypes, Sub5],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2 | codec3 | codec4 | codec5).transformOrFail(
- either => Right(either.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(Left(Left(Left(sub1)))))
- case sub2: Sub2 => Right(Left(Left(Left(Right(sub2)))))
- case sub3: Sub3 => Right(Left(Left(Right(sub3))))
- case sub4: Sub4 => Right(Left(Right(sub4)))
- case sub5: Sub5 => Right(Right(sub5))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2 | codec3 | codec4 | codec5).transformOrFail(either =>
+ Right(either.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge),
+ )((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(Left(Left(Left(sub1)))))
+ case sub2: Sub2 => Right(Left(Left(Left(Right(sub2)))))
+ case sub3: Sub3 => Right(Left(Left(Right(sub3))))
+ case sub4: Sub4 => Right(Left(Right(sub4)))
+ case sub5: Sub5 => Right(Right(sub5))
+ case _ => Left(s"Unexpected error type")
+ },
)
def apply[
@@ -453,18 +440,18 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec5: HttpCodec[AtomTypes, Sub5],
codec6: HttpCodec[AtomTypes, Sub6],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2 | codec3 | codec4 | codec5 | codec6).transformOrFail(
- either => Right(either.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(Left(Left(Left(Left(sub1))))))
- case sub2: Sub2 => Right(Left(Left(Left(Left(Right(sub2))))))
- case sub3: Sub3 => Right(Left(Left(Left(Right(sub3)))))
- case sub4: Sub4 => Right(Left(Left(Right(sub4))))
- case sub5: Sub5 => Right(Left(Right(sub5)))
- case sub6: Sub6 => Right(Right(sub6))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2 | codec3 | codec4 | codec5 | codec6).transformOrFail(either =>
+ Right(either.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge),
+ )((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(Left(Left(Left(Left(sub1))))))
+ case sub2: Sub2 => Right(Left(Left(Left(Left(Right(sub2))))))
+ case sub3: Sub3 => Right(Left(Left(Left(Right(sub3)))))
+ case sub4: Sub4 => Right(Left(Left(Right(sub4))))
+ case sub5: Sub5 => Right(Left(Right(sub5)))
+ case sub6: Sub6 => Right(Right(sub6))
+ case _ => Left(s"Unexpected error type")
+ },
)
def apply[
@@ -485,20 +472,19 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec6: HttpCodec[AtomTypes, Sub6],
codec7: HttpCodec[AtomTypes, Sub7],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2 | codec3 | codec4 | codec5 | codec6 | codec7).transformOrFail(
- either =>
- Right(either.left.map(_.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge).merge),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(Left(Left(Left(Left(Left(sub1)))))))
- case sub2: Sub2 => Right(Left(Left(Left(Left(Left(Right(sub2)))))))
- case sub3: Sub3 => Right(Left(Left(Left(Left(Right(sub3))))))
- case sub4: Sub4 => Right(Left(Left(Left(Right(sub4)))))
- case sub5: Sub5 => Right(Left(Left(Right(sub5))))
- case sub6: Sub6 => Right(Left(Right(sub6)))
- case sub7: Sub7 => Right(Right(sub7))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2 | codec3 | codec4 | codec5 | codec6 | codec7).transformOrFail(either =>
+ Right(either.left.map(_.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge).merge),
+ )((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(Left(Left(Left(Left(Left(sub1)))))))
+ case sub2: Sub2 => Right(Left(Left(Left(Left(Left(Right(sub2)))))))
+ case sub3: Sub3 => Right(Left(Left(Left(Left(Right(sub3))))))
+ case sub4: Sub4 => Right(Left(Left(Left(Right(sub4)))))
+ case sub5: Sub5 => Right(Left(Left(Right(sub5))))
+ case sub6: Sub6 => Right(Left(Right(sub6)))
+ case sub7: Sub7 => Right(Right(sub7))
+ case _ => Left(s"Unexpected error type")
+ },
)
def apply[
@@ -521,25 +507,24 @@ object HttpCodec extends ContentCodecs with HeaderCodecs with MethodCodecs with
codec7: HttpCodec[AtomTypes, Sub7],
codec8: HttpCodec[AtomTypes, Sub8],
): HttpCodec[AtomTypes, Value] =
- (codec1 | codec2 | codec3 | codec4 | codec5 | codec6 | codec7 | codec8).transformOrFail(
- either =>
- Right(
- either.left
- .map(_.left.map(_.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge).merge)
- .merge,
- ),
- (value: Value) =>
- value match {
- case sub1: Sub1 => Right(Left(Left(Left(Left(Left(Left(Left(sub1))))))))
- case sub2: Sub2 => Right(Left(Left(Left(Left(Left(Left(Right(sub2))))))))
- case sub3: Sub3 => Right(Left(Left(Left(Left(Left(Right(sub3)))))))
- case sub4: Sub4 => Right(Left(Left(Left(Left(Right(sub4))))))
- case sub5: Sub5 => Right(Left(Left(Left(Right(sub5)))))
- case sub6: Sub6 => Right(Left(Left(Right(sub6))))
- case sub7: Sub7 => Right(Left(Right(sub7)))
- case sub8: Sub8 => Right(Right(sub8))
- case _ => Left(s"Unexpected error type")
- },
+ (codec1 | codec2 | codec3 | codec4 | codec5 | codec6 | codec7 | codec8).transformOrFail(either =>
+ Right(
+ either.left
+ .map(_.left.map(_.left.map(_.left.map(_.left.map(_.left.map(_.merge).merge).merge).merge).merge).merge)
+ .merge,
+ ),
+ )((value: Value) =>
+ value match {
+ case sub1: Sub1 => Right(Left(Left(Left(Left(Left(Left(Left(sub1))))))))
+ case sub2: Sub2 => Right(Left(Left(Left(Left(Left(Left(Right(sub2))))))))
+ case sub3: Sub3 => Right(Left(Left(Left(Left(Left(Right(sub3)))))))
+ case sub4: Sub4 => Right(Left(Left(Left(Left(Right(sub4))))))
+ case sub5: Sub5 => Right(Left(Left(Left(Right(sub5)))))
+ case sub6: Sub6 => Right(Left(Left(Right(sub6))))
+ case sub7: Sub7 => Right(Left(Right(sub7)))
+ case sub8: Sub8 => Right(Right(sub8))
+ case _ => Left(s"Unexpected error type")
+ },
)
}
diff --git a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala
index 0e3dd81934..d0dbefb322 100644
--- a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala
+++ b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala
@@ -66,19 +66,16 @@ sealed trait RichTextCodec[A] { self =>
* Tranforms this constant unit codec to a constant codec of another type.
*/
final def as[B](b: => B)(implicit ev: A =:= Unit): RichTextCodec[B] =
- self.asType[Unit].transform(_ => b, _ => ())
+ self.asType[Unit].transform(_ => b)(_ => ())
final def asType[B](implicit ev: A =:= B): RichTextCodec[B] =
self.asInstanceOf[RichTextCodec[B]]
final def collectOrFail(failure: String)(pf: PartialFunction[A, A]): RichTextCodec[A] =
- transformOrFailLeft[A](
- {
- case x if pf.isDefinedAt(x) => Right(pf(x))
- case _ => Left(failure)
- },
- a => a,
- )
+ transformOrFailLeft[A] {
+ case x if pf.isDefinedAt(x) => Right(pf(x))
+ case _ => Left(failure)
+ }(identity)
final def decode(value: CharSequence): Either[String, A] =
RichTextCodec.parse(value, self).map(_._2)
@@ -109,42 +106,38 @@ sealed trait RichTextCodec[A] { self =>
final def encode(value: A): Either[String, String] = RichTextCodec.encode(value, self)
final def optional(default: A): RichTextCodec[Option[A]] =
- self.transform(a => Some(a), { case None => default; case Some(a) => a })
+ self.transform[Option[A]](a => Some(a))(_.fold(default)(identity))
lazy val repeat: RichTextCodec[Chunk[A]] =
- ((self ~ repeat).transform[NonEmptyChunk[A]](
- t => NonEmptyChunk(t._1, t._2: _*),
- c => (c.head, c.tail),
+ ((self ~ repeat).transform[NonEmptyChunk[A]](t => NonEmptyChunk(t._1, t._2: _*))(c =>
+ (c.head, c.tail),
) | RichTextCodec.empty.as(Chunk.empty[A]))
- .transform[Chunk[A]](
- _ match {
- case Left(nonEmpty) => nonEmpty
- case Right(maybeEmpty) => maybeEmpty
- },
- c => c.nonEmptyOrElse[Either[NonEmptyChunk[A], Chunk[A]]](Right(c))(Left(_)),
- )
+ .transform[Chunk[A]](_ match {
+ case Left(nonEmpty) => nonEmpty
+ case Right(maybeEmpty) => maybeEmpty
+ })(c => c.nonEmptyOrElse[Either[NonEmptyChunk[A], Chunk[A]]](Right(c))(Left(_)))
final def singleton: RichTextCodec[NonEmptyChunk[A]] =
- self.transform(a => NonEmptyChunk(a), _.head)
+ self.transform(a => NonEmptyChunk(a))(_.head)
- final def transform[B](f: A => B, g: B => A): RichTextCodec[B] =
- self.transformOrFail[B](a => Right(f(a)), b => Right(g(b)))
+ final def transform[B](f: A => B)(g: B => A): RichTextCodec[B] =
+ self.transformOrFail[B](a => Right(f(a)))(b => Right(g(b)))
- final def transformOrFail[B](f: A => Either[String, B], g: B => Either[String, A]): RichTextCodec[B] =
+ final def transformOrFail[B](f: A => Either[String, B])(g: B => Either[String, A]): RichTextCodec[B] =
RichTextCodec.TransformOrFail(self, f, g)
- final def transformOrFailLeft[B](f: A => Either[String, B], g: B => A): RichTextCodec[B] =
- self.transformOrFail[B](f, b => Right(g(b)))
+ final def transformOrFailLeft[B](f: A => Either[String, B])(g: B => A): RichTextCodec[B] =
+ self.transformOrFail[B](f)(b => Right(g(b)))
- final def transformOrFailRight[B](f: A => B, g: B => Either[String, A]): RichTextCodec[B] =
- self.transformOrFail[B](a => Right(f(a)), g)
+ final def transformOrFailRight[B](f: A => B)(g: B => Either[String, A]): RichTextCodec[B] =
+ self.transformOrFail[B](a => Right(f(a)))(g)
/**
* Converts this codec of `A` into a codec of `Unit` by specifying a canonical
* value to use when an HTTP client needs to generate a value for this codec.
*/
final def const(canonical: A): RichTextCodec[Unit] =
- self.transform[Unit](_ => (), _ => canonical)
+ self.transform[Unit](_ => ())(_ => canonical)
/**
* Attempts to validate a decoded value, or fails using the specified failure
@@ -195,7 +188,7 @@ object RichTextCodec {
* A codec that describes a digit character.
*/
val digit: RichTextCodec[Int] =
- filter(c => c >= '0' && c <= '9').transform[Int](c => parseInt(c.toString), x => x.toString.head)
+ filter(c => c >= '0' && c <= '9').transform(c => parseInt(c.toString))(x => x.toString.head)
/**
* A codec that describes nothing at all. Such codecs successfully decode even
@@ -247,7 +240,7 @@ object RichTextCodec {
* A codec that describes any number of whitespace characters.
*/
lazy val whitespaces: RichTextCodec[Unit] =
- whitespaceChar.repeat.transform(_ => (), _ => Chunk.empty)
+ whitespaceChar.repeat.transform(_ => ())(_ => Chunk.empty)
/**
* A codec that describes a single whitespace character.
diff --git a/zio-http/src/main/scala/zio/http/endpoint/internal/Memoized.scala b/zio-http/src/main/scala/zio/http/endpoint/internal/Memoized.scala
deleted file mode 100644
index 0ae6bc72d2..0000000000
--- a/zio-http/src/main/scala/zio/http/endpoint/internal/Memoized.scala
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package zio.http.endpoint.internal
-
-import zio.stacktracer.TracingImplicits.disableAutoTrace
-
-private[http] class Memoized[K, A] private (compute: K => A) { self =>
- private var map: Map[K, A] = Map()
-
- def get(api: K): A = {
- map.get(api) match {
- case Some(a) => a
- case None =>
- val a = compute(api)
- map = map.updated(api, a)
- a
- }
- }
-}
-private[http] object Memoized {
- def apply[K, A](compute: K => A): Memoized[K, A] = new Memoized(compute)
-}
diff --git a/zio-http/src/main/scala/zio/http/internal/OutputEncoder.scala b/zio-http/src/main/scala/zio/http/internal/OutputEncoder.scala
index 52f8ad3944..572082a5c9 100644
--- a/zio-http/src/main/scala/zio/http/internal/OutputEncoder.scala
+++ b/zio-http/src/main/scala/zio/http/internal/OutputEncoder.scala
@@ -24,7 +24,6 @@ private[http] object OutputEncoder {
private val `>` = ">"
private val `"` = """
private val `'` = "'"
- private val `/` = "/"
/**
* Encode HTML characters that can cause XSS, according to OWASP
@@ -32,7 +31,7 @@ private[http] object OutputEncoder {
* https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-rules-summary
*
* Specification: Convert & to &, Convert < to <, Convert > to >,
- * Convert " to ", Convert ' to ', Convert / to /
+ * Convert " to ", Convert ' to '
*
* Only use this function to encode characters inside HTML context:
* output' => `>`
case '"' => `"`
case '\'' => `'`
- case '/' => `/`
case _ @char => char.toString
}
diff --git a/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala b/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala
index b6e34896b2..3b23123c1a 100644
--- a/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala
+++ b/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala
@@ -66,27 +66,15 @@ object NettyBodyWriter {
None
case StreamBody(stream, _, _) =>
Some(
- stream.chunks
- .runFoldZIO(Option.empty[Chunk[Byte]]) {
- case (Some(previous), current) =>
- NettyFutureExecutor.executed {
- ctx.writeAndFlush(new DefaultHttpContent(Unpooled.wrappedBuffer(previous.toArray)))
- } *>
- ZIO.succeed(Some(current))
- case (_, current) =>
- ZIO.succeed(Some(current))
+ stream.chunks.mapZIO { bytes =>
+ NettyFutureExecutor.executed {
+ ctx.writeAndFlush(new DefaultHttpContent(Unpooled.wrappedBuffer(bytes.toArray)))
}
- .flatMap { maybeLastChunk =>
- // last chunk is handled separately to avoid fiber interrupt before EMPTY_LAST_CONTENT is sent
- ZIO.attempt(
- maybeLastChunk.foreach { lastChunk =>
- ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(lastChunk.toArray)))
- },
- ) *>
- NettyFutureExecutor.executed {
- ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
- }
- },
+ }.runDrain.zipRight {
+ NettyFutureExecutor.executed {
+ ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
+ }
+ },
)
case ChunkBody(data, _, _) =>
ctx.write(Unpooled.wrappedBuffer(data.toArray))
diff --git a/zio-http/src/main/scala/zio/http/netty/NettyResponseEncoder.scala b/zio-http/src/main/scala/zio/http/netty/NettyResponseEncoder.scala
index 0e6fcc828f..f5e1cc14cf 100644
--- a/zio-http/src/main/scala/zio/http/netty/NettyResponseEncoder.scala
+++ b/zio-http/src/main/scala/zio/http/netty/NettyResponseEncoder.scala
@@ -39,6 +39,10 @@ private[zio] object NettyResponseEncoder {
fastEncode(response, bytes)
} else {
val jHeaders = Conversions.headersToNetty(response.headers)
+ // Prevent client from closing connection before server writes EMPTY_LAST_CONTENT.
+ if (response.body.isInstanceOf[Body.StreamBody]) {
+ jHeaders.remove(HttpHeaderNames.CONTENT_LENGTH)
+ }
val jStatus = Conversions.statusToNetty(response.status)
val hasContentLength = jHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)
if (!hasContentLength) jHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
diff --git a/zio-http/src/main/scala/zio/http/template/Dom.scala b/zio-http/src/main/scala/zio/http/template/Dom.scala
index e92faa197f..0ad20889ba 100644
--- a/zio-http/src/main/scala/zio/http/template/Dom.scala
+++ b/zio-http/src/main/scala/zio/http/template/Dom.scala
@@ -16,6 +16,8 @@
package zio.http.template
+import zio.http.internal.OutputEncoder
+
/**
* Light weight DOM implementation that can be rendered as a html string.
*
@@ -40,6 +42,7 @@ sealed trait Dom { self =>
val elements = children.collect {
case self: Dom.Element => self
case self: Dom.Text => self
+ case self: Dom.Raw => self
}
val noElements = elements.isEmpty
@@ -61,9 +64,11 @@ sealed trait Dom { self =>
else
s"<$name ${attributes.mkString(" ")}>$inner$name>"
- case Dom.Text(data) => data
- case Dom.Attribute(name, value) => s"""$name="$value""""
+ case Dom.Text(data) => OutputEncoder.encodeHtml(data.toString)
+ case Dom.Attribute(name, value) =>
+ s"""$name="${OutputEncoder.encodeHtml(value.toString)}""""
case Dom.Empty => ""
+ case Dom.Raw(raw) => raw
}
}
@@ -76,10 +81,14 @@ object Dom {
def text(data: CharSequence): Dom = Dom.Text(data)
+ def raw(raw: CharSequence): Dom = Dom.Raw(raw)
+
private[zio] final case class Element(name: CharSequence, children: Seq[Dom]) extends Dom
private[zio] final case class Text(data: CharSequence) extends Dom
+ private[zio] final case class Raw(raw: CharSequence) extends Dom
+
private[zio] final case class Attribute(name: CharSequence, value: CharSequence) extends Dom
private[zio] object Empty extends Dom
diff --git a/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala b/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala
index 7499ab3c3f..445b680413 100644
--- a/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala
+++ b/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala
@@ -100,10 +100,6 @@ object StaticFileServerSpec extends HttpRunnableSpec {
val res = resourceOk.run().map(_.status)
assertZIO(res)(equalTo(Status.Ok))
},
- test("should have content-length") {
- val res = resourceOk.run().map(_.header(Header.ContentLength))
- assertZIO(res)(isSome(equalTo(Header.ContentLength(7L))))
- },
test("should have content") {
val res = resourceOk.run().flatMap(_.body.asString)
assertZIO(res)(equalTo("foo\nbar"))
diff --git a/zio-http/src/test/scala/zio/http/codec/HttpCodecSpec.scala b/zio-http/src/test/scala/zio/http/codec/HttpCodecSpec.scala
index 058bebb9c1..2f4c5aabb3 100644
--- a/zio-http/src/test/scala/zio/http/codec/HttpCodecSpec.scala
+++ b/zio-http/src/test/scala/zio/http/codec/HttpCodecSpec.scala
@@ -99,7 +99,7 @@ object HttpCodecSpec extends ZIOHttpSpec {
test("no fallback for defects") {
val e = new RuntimeException("boom")
- val codec1 = HttpCodec.empty.transform[Unit](_ => throw e, _ => ()).const("route1")
+ val codec1 = HttpCodec.empty.transform[Unit](_ => throw e)(_ => ()).const("route1")
val codec2 = HttpCodec.empty.const("route2")
val fallback = codec1 | codec2
diff --git a/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala b/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala
index 2221c774c9..8a01282efa 100644
--- a/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala
+++ b/zio-http/src/test/scala/zio/http/codec/RichTextCodecSpec.scala
@@ -246,7 +246,7 @@ object RichTextCodecSpec extends ZIOHttpSpec {
assertTrue(codec.decode("---123---").isLeft)
},
test("transformOrFail decoder") {
- val codec = RichTextCodec.literal("123").transform[Int](_.toInt, _.toString)
+ val codec = RichTextCodec.literal("123").transform(_.toInt)(_.toString)
assertTrue(success(123) == codec.decode("123--")) &&
assertTrue(codec.decode("4123").isLeft)
},
diff --git a/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala b/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala
index fc53412848..be95cb392d 100644
--- a/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala
+++ b/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala
@@ -1,8 +1,9 @@
package zio.http.netty
import zio._
+import zio.test.Assertion._
import zio.test.TestAspect.withLiveClock
-import zio.test.{Spec, TestEnvironment, assertTrue}
+import zio.test.{Spec, TestEnvironment, assert}
import zio.stream.{ZStream, ZStreamAspect}
@@ -19,8 +20,7 @@ object NettyStreamBodySpec extends HttpRunnableSpec {
handler(
http.Response(
status = Status.Ok,
- // content length header is important,
- // in this case the server will not use chunked transfer encoding even if response is a stream
+ // Content-Length header will be removed when the body is a stream
headers = Headers(Header.ContentLength(len)),
body = Body.fromStream(streams.next()),
),
@@ -76,13 +76,9 @@ object NettyStreamBodySpec extends HttpRunnableSpec {
)
client <- ZIO.service[Client]
firstResponse <- makeRequest(client, port)
- firstResponseBodyReceive <- firstResponse.body.asStream.chunks
- .map(chunk => new String(chunk.toArray))
- .mapZIO { chunk =>
- atLeastOneChunkReceived.succeed(()) *> ZIO.succeed(chunk)
- }
- .runCollect
- .fork
+ firstResponseBodyReceive <- firstResponse.body.asStream.chunks.mapZIO { chunk =>
+ atLeastOneChunkReceived.succeed(()) *> ZIO.succeed(chunk.asString)
+ }.runCollect.fork
_ <- firstResponseQueue.offerAll(message.getBytes.toList)
_ <- atLeastOneChunkReceived.await
// saying that there will be no more data in the first response stream
@@ -93,20 +89,25 @@ object NettyStreamBodySpec extends HttpRunnableSpec {
// java.lang.IllegalStateException: unexpected message type: LastHttpContent"
// exception will be thrown
secondResponse <- makeRequest(client, port)
- secondResponseBody <- secondResponse.body.asStream.chunks.map(chunk => new String(chunk.toArray)).runCollect
- firstResponseBody <- firstResponseBodyReceive.join
- value =
- firstResponse.status == Status.Ok &&
- // since response has not chunked transfer encoding header we can't guarantee that
- // received chunks will be the same as it was transferred. So we need to check the whole body
- firstResponseBody.reduce(_ + _) == message &&
- secondResponse.status == Status.Ok &&
- secondResponseBody == Chunk(message)
- } yield {
- assertTrue(
- value,
- )
- }
+ secondResponseBody <- secondResponse.body.asStream.chunks.map(_.asString).runCollect
+ firstResponseBody <- firstResponseBodyReceive.join
+
+ assertFirst =
+ assert(firstResponse.status)(equalTo(Status.Ok)) &&
+ assert(firstResponse.headers.get(Header.ContentLength))(isNone) &&
+ assert(firstResponse.headers.get(Header.TransferEncoding))(
+ isSome(equalTo(Header.TransferEncoding.Chunked)),
+ ) &&
+ assert(firstResponseBody.reduce(_ + _))(equalTo(message))
+
+ assertSecond =
+ assert(secondResponse.status)(equalTo(Status.Ok)) &&
+ assert(secondResponse.headers.get(Header.ContentLength))(isNone) &&
+ assert(secondResponse.headers.get(Header.TransferEncoding))(
+ isSome(equalTo(Header.TransferEncoding.Chunked)),
+ ) &&
+ assert(secondResponseBody)(equalTo(Chunk(message, "")))
+ } yield assertFirst && assertSecond
},
).provide(
singleConnectionClient,
diff --git a/zio-http/src/test/scala/zio/http/template/DomSpec.scala b/zio-http/src/test/scala/zio/http/template/DomSpec.scala
index e0395bd415..39e173480b 100644
--- a/zio-http/src/test/scala/zio/http/template/DomSpec.scala
+++ b/zio-http/src/test/scala/zio/http/template/DomSpec.scala
@@ -86,6 +86,36 @@ object DomSpec extends ZIOHttpSpec {
assertTrue(dom.encode == """zio-http""")
},
+ test("xss protection for text nodes") {
+ val dom = Dom.element(
+ "a",
+ Dom.attr("href", "http://www.zio-http.com"),
+ Dom.text(""""""),
+ )
+ assertTrue(
+ dom.encode == """<script type="text/javascript">alert("xss")</script>""",
+ )
+ },
+ test("xss protection for attributes") {
+ val dom = Dom.element(
+ "a",
+ Dom.attr("href", """"""),
+ Dom.text("my link"),
+ )
+ assertTrue(
+ dom.encode == """my link""",
+ )
+ },
+ test("raw output") {
+ val dom = Dom.element(
+ "a",
+ Dom.attr("href", "http://www.zio-http.com"),
+ Dom.raw(""""""),
+ )
+ assertTrue(
+ dom.encode == """""",
+ )
+ },
suite("Self Closing")(
test("void") {
checkAll(voidTagGen) { name =>