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" - 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 =>