From 0f7b6a74629118fd45c87a0df2bafa570540bf82 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:24:27 +0200 Subject: [PATCH 01/22] Update zio, zio-streams, zio-test, ... to 2.0.17 (#2447) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 8b7768f16a4479b4c9c380d665e24fa95ac8198f Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 15:41:13 +0200 Subject: [PATCH 02/22] memoized is not used (#2429) memoized not used --- .../zio/http/endpoint/internal/Memoized.scala | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 zio-http/src/main/scala/zio/http/endpoint/internal/Memoized.scala 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) -} From c624db925900d6b30dd6ce90243f615d87c88ec2 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 15:44:32 +0200 Subject: [PATCH 03/22] Update slf4j-api, slf4j-simple to 2.0.9 (#2427) --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 7e2f4550ab9b2330f8aa0e0b6b7a55a635b41840 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 15:44:54 +0200 Subject: [PATCH 04/22] Update sbt-github-actions to 0.16.0 (#2425) * Update sbt-github-actions to 0.16.0 * Regenerate GitHub Actions workflow Executed command: sbt githubWorkflowGenerate --- .github/workflows/ci.yml | 6 +++--- project/plugins.sbt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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/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") From 6adb9722ee0ee082ae4a8e32d2ede787d8ea0728 Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 15:45:16 +0200 Subject: [PATCH 05/22] change signature to allow for type inference (#2422) --- .../zio/http/endpoint/cli/EndpointGen.scala | 2 +- .../scala/zio/http/codec/HeaderCodecs.scala | 13 +- .../main/scala/zio/http/codec/HttpCodec.scala | 211 ++++++++---------- .../scala/zio/http/codec/RichTextCodec.scala | 53 ++--- .../scala/zio/http/codec/HttpCodecSpec.scala | 2 +- .../zio/http/codec/RichTextCodecSpec.scala | 2 +- 6 files changed, 130 insertions(+), 153 deletions(-) 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/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) }, From 7e37bb083bc609d5eb3d5f572ced5842c6acfa14 Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 15:46:00 +0200 Subject: [PATCH 06/22] add xss protection as default (#2421) * * escape output by default for xss protection * escaping '/' is not needed as of owasp-guide * format --- .../zio/http/internal/OutputEncoder.scala | 4 +-- .../main/scala/zio/http/template/Dom.scala | 13 ++++++-- .../scala/zio/http/template/DomSpec.scala | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) 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/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/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 => From 70aabfc0b0c7352ac5af13c0833a151c44529d97 Mon Sep 17 00:00:00 2001 From: Jisoo Park Date: Sun, 24 Sep 2023 22:52:47 +0900 Subject: [PATCH 07/22] Fix stream body delay 2 (#2420) * Fix stream body delay * Remove Content-Length header for stream response * Restore spec * File response with 'jar' protocol doesn't have content-type header * Convert to classic assertions due to a macro error * fmt * fix lint --- .../zio/http/netty/NettyBodyWriter.scala | 28 +++-------- .../zio/http/netty/NettyResponseEncoder.scala | 4 ++ .../scala/zio/http/StaticFileServerSpec.scala | 4 -- .../zio/http/netty/NettyStreamBodySpec.scala | 49 ++++++++++--------- 4 files changed, 37 insertions(+), 48 deletions(-) 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/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/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, From 8c8187b44fdff0fa6bb8faee7d1bf1024fcc5ea8 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 16:07:28 +0200 Subject: [PATCH 08/22] Update scalafmt-core to 3.7.14 (#2426) * Update scalafmt-core to 3.7.14 * Reformat with scalafmt 3.7.14 Executed command: scalafmt --non-interactive * Add 'Reformat with scalafmt 3.7.14' to .git-blame-ignore-revs --- .git-blame-ignore-revs | 3 +++ .scalafmt.conf | 2 +- profiling/project/plugins.sbt | 4 ++-- project/BuildHelper.scala | 2 +- project/Shading.scala | 7 ++++--- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 06da4da483..f2c8c0de46 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -6,3 +6,6 @@ d338861916380701fb1a3cf557bb2b0f18792075 # Scala Steward: Reformat with scalafmt 3.7.3 9685d201c17c737cf4617ec648d76aff461bd831 + +# Scala Steward: Reformat with scalafmt 3.7.14 +85133289bbd902a9fa5c13ff515eda5c0b58fd1d diff --git a/.scalafmt.conf b/.scalafmt.conf index 0aaaa04410..792e8620f8 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.3 +version = 3.7.14 maxColumn = 120 align.preset = more diff --git a/profiling/project/plugins.sbt b/profiling/project/plugins.sbt index 103c04f07b..f8c617df58 100644 --- a/profiling/project/plugins.sbt +++ b/profiling/project/plugins.sbt @@ -1,3 +1,3 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 324ffd2e90..039dcaf6ed 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -74,7 +74,7 @@ object BuildHelper extends ScalaSettings { scalacOptions := stdOptions ++ extraOptions(scalaVersion.value), ThisBuild / scalafixDependencies ++= List( - "com.github.vovapolu" %% "scaluzzi" % "0.1.23", + "com.github.vovapolu" %% "scaluzzi" % "0.1.23", ), Test / parallelExecution := true, incOptions ~= (_.withLogRecompileOnMacro(false)), diff --git a/project/Shading.scala b/project/Shading.scala index c76c6a61dc..ac86dca6c5 100644 --- a/project/Shading.scala +++ b/project/Shading.scala @@ -21,11 +21,12 @@ object Shading { } else Nil lazy val shadingEnabled = { - val enabled = sys.props.get(sysprops.`publish.shaded`).fold(false)(_.toBoolean) || + val enabled = sys.props.get(sysprops.`publish.shaded`).fold(false)(_.toBoolean) || sys.env.get(env.PUBLISH_SHADED).fold(false)(_.toBoolean) - println(s"*** shading enabled: $enabled (env.PUBLISH_SHADED=${sys.env.get(env.PUBLISH_SHADED)}, sysprops.`publish.shaded`=${sys.props.get(sysprops.`publish.shaded`)})") + println(s"*** shading enabled: $enabled (env.PUBLISH_SHADED=${sys.env + .get(env.PUBLISH_SHADED)}, sysprops.`publish.shaded`=${sys.props.get(sysprops.`publish.shaded`)})") enabled } - def plugins(): Seq[Plugins] = if(shadingEnabled) Seq(ShadingPlugin) else Nil + def plugins(): Seq[Plugins] = if (shadingEnabled) Seq(ShadingPlugin) else Nil } From 53f0263d12477d3317c78f930c9347ecbc6cf2be Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 16:07:52 +0200 Subject: [PATCH 09/22] Update sbt-jmh to 0.4.6 (#2428) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e909d62bf4..0940fb6eef 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.4") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") 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.16.0") From 07df316d84228dc8ba1b73af257aa47b7e02fd2d Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 18:25:31 +0200 Subject: [PATCH 10/22] align default values from HandlerAspect.redirect with Response.redirect (#2432) --- zio-http/src/main/scala/zio/http/HandlerAspect.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/HandlerAspect.scala b/zio-http/src/main/scala/zio/http/HandlerAspect.scala index 81ffbfbb14..787b764431 100644 --- a/zio-http/src/main/scala/zio/http/HandlerAspect.scala +++ b/zio-http/src/main/scala/zio/http/HandlerAspect.scala @@ -596,7 +596,7 @@ private[http] trait HandlerAspects extends zio.http.internal.HeaderModifier[Hand /** * Creates a middleware that will redirect requests to the specified URL. */ - def redirect(url: URL, isPermanent: Boolean): HandlerAspect[Any, Unit] = + def redirect(url: URL, isPermanent: Boolean = false): HandlerAspect[Any, Unit] = fail(Response.redirect(url, isPermanent)) /** @@ -604,7 +604,7 @@ private[http] trait HandlerAspects extends zio.http.internal.HeaderModifier[Hand * same path without trailing slash. */ def redirectTrailingSlash( - isPermanent: Boolean, + isPermanent: Boolean = false, ): HandlerAspect[Any, Unit] = ifRequestThenElse(request => request.url.path.hasTrailingSlash && request.url.queryParams.isEmpty)( ifTrue = updatePath(_.dropTrailingSlash) ++ failWith(request => Response.redirect(request.url, isPermanent)), From 04e702cefe39e7f1f034de52db8aa1ff94dec0bc Mon Sep 17 00:00:00 2001 From: Jisoo Park Date: Mon, 25 Sep 2023 01:27:18 +0900 Subject: [PATCH 11/22] Fix stream delay 3 (#2458) * Revert "Fix stream body delay 2 (#2420)" This reverts commit 70aabfc0b0c7352ac5af13c0833a151c44529d97. * Proper fix for #2284 --- .../zio/http/netty/NettyBodyWriter.scala | 50 +++++++++++++++---- .../zio/http/netty/NettyResponseEncoder.scala | 4 -- .../netty/client/ClientInboundHandler.scala | 2 +- .../netty/server/ServerInboundHandler.scala | 10 ++-- .../scala/zio/http/StaticFileServerSpec.scala | 4 ++ .../zio/http/netty/NettyStreamBodySpec.scala | 37 ++++++-------- 6 files changed, 69 insertions(+), 38 deletions(-) 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 3b23123c1a..1fd1b23501 100644 --- a/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala +++ b/zio-http/src/main/scala/zio/http/netty/NettyBodyWriter.scala @@ -29,7 +29,9 @@ import io.netty.channel._ import io.netty.handler.codec.http.{DefaultHttpContent, LastHttpContent} object NettyBodyWriter { - def writeAndFlush(body: Body, ctx: ChannelHandlerContext)(implicit trace: Trace): Option[Task[Unit]] = + def writeAndFlush(body: Body, contentLength: Option[Long], ctx: ChannelHandlerContext)(implicit + trace: Trace, + ): Option[Task[Unit]] = body match { case body: ByteBufBody => ctx.write(body.byteBuf) @@ -66,14 +68,44 @@ object NettyBodyWriter { None case StreamBody(stream, _, _) => Some( - stream.chunks.mapZIO { bytes => - NettyFutureExecutor.executed { - ctx.writeAndFlush(new DefaultHttpContent(Unpooled.wrappedBuffer(bytes.toArray))) - } - }.runDrain.zipRight { - NettyFutureExecutor.executed { - ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) - } + contentLength match { + case Some(length) => + stream.chunks + .runFoldZIO(length) { (remaining, bytes) => + remaining - bytes.size match { + case 0L => + NettyFutureExecutor.executed { + // Flushes the last body content and LastHttpContent together to avoid race conditions. + ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(bytes.toArray))) + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + }.as(0L) + + case n => + NettyFutureExecutor.executed { + ctx.writeAndFlush(new DefaultHttpContent(Unpooled.wrappedBuffer(bytes.toArray))) + }.as(n) + } + } + .flatMap { + case 0L => ZIO.unit + case remaining => + val actualLength = length - remaining + ZIO.logWarning(s"Expected Content-Length of $length, but sent $actualLength bytes") *> + NettyFutureExecutor.executed { + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + } + } + + case None => + stream.chunks.mapZIO { bytes => + NettyFutureExecutor.executed { + ctx.writeAndFlush(new DefaultHttpContent(Unpooled.wrappedBuffer(bytes.toArray))) + } + }.runDrain.zipRight { + NettyFutureExecutor.executed { + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + } + } }, ) case ChunkBody(data, _, _) => 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 f5e1cc14cf..0e6fcc828f 100644 --- a/zio-http/src/main/scala/zio/http/netty/NettyResponseEncoder.scala +++ b/zio-http/src/main/scala/zio/http/netty/NettyResponseEncoder.scala @@ -39,10 +39,6 @@ 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/netty/client/ClientInboundHandler.scala b/zio-http/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala index 4f57b773c1..a4ee256aa9 100644 --- a/zio-http/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala +++ b/zio-http/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala @@ -55,7 +55,7 @@ final class ClientInboundHandler( ctx.writeAndFlush(fullRequest) case _: HttpRequest => ctx.write(jReq) - NettyBodyWriter.writeAndFlush(req.body, ctx).foreach { effect => + NettyBodyWriter.writeAndFlush(req.body, None, ctx).foreach { effect => rtm.run(ctx, NettyRuntime.noopEnsuring)(effect)(Unsafe.unsafe, trace) } } diff --git a/zio-http/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala b/zio-http/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala index 5df9e87a94..f44c549d6a 100644 --- a/zio-http/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala +++ b/zio-http/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala @@ -172,9 +172,13 @@ private[zio] final case class ServerInboundHandler( val jResponse = NettyResponseEncoder.encode(ctx, response, runtime) // setServerTime(time, response, jResponse) ctx.writeAndFlush(jResponse) - if (!jResponse.isInstanceOf[FullHttpResponse]) - NettyBodyWriter.writeAndFlush(response.body, ctx) - else + if (!jResponse.isInstanceOf[FullHttpResponse]) { + val contentLength = jResponse.headers.get(HttpHeaderNames.CONTENT_LENGTH) match { + case null => None + case value => Some(value.toLong) + } + NettyBodyWriter.writeAndFlush(response.body, contentLength, ctx) + } else None } } diff --git a/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala b/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala index 445b680413..7499ab3c3f 100644 --- a/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala +++ b/zio-http/src/test/scala/zio/http/StaticFileServerSpec.scala @@ -100,6 +100,10 @@ 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/netty/NettyStreamBodySpec.scala b/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala index be95cb392d..11a93802d2 100644 --- a/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala +++ b/zio-http/src/test/scala/zio/http/netty/NettyStreamBodySpec.scala @@ -1,9 +1,8 @@ package zio.http.netty import zio._ -import zio.test.Assertion._ import zio.test.TestAspect.withLiveClock -import zio.test.{Spec, TestEnvironment, assert} +import zio.test.{Spec, TestEnvironment, assertTrue} import zio.stream.{ZStream, ZStreamAspect} @@ -20,7 +19,8 @@ object NettyStreamBodySpec extends HttpRunnableSpec { handler( http.Response( status = Status.Ok, - // Content-Length header will be removed when the body is a stream + // content length header is important, + // in this case the server will not use chunked transfer encoding even if response is a stream headers = Headers(Header.ContentLength(len)), body = Body.fromStream(streams.next()), ), @@ -77,7 +77,7 @@ object NettyStreamBodySpec extends HttpRunnableSpec { client <- ZIO.service[Client] firstResponse <- makeRequest(client, port) firstResponseBodyReceive <- firstResponse.body.asStream.chunks.mapZIO { chunk => - atLeastOneChunkReceived.succeed(()) *> ZIO.succeed(chunk.asString) + atLeastOneChunkReceived.succeed(()).as(chunk.asString) }.runCollect.fork _ <- firstResponseQueue.offerAll(message.getBytes.toList) _ <- atLeastOneChunkReceived.await @@ -91,23 +91,18 @@ object NettyStreamBodySpec extends HttpRunnableSpec { secondResponse <- makeRequest(client, port) 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 + 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, + ) + } }, ).provide( singleConnectionClient, From e27c016f66e2c5de0ea4725d1b0691178c3404d1 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 18:28:38 +0200 Subject: [PATCH 12/22] Update netty-codec-http, ... to 4.1.98.Final (#2456) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f065a2a497..e3a33b8568 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -3,7 +3,7 @@ import sbt.Keys.scalaVersion object Dependencies { val JwtCoreVersion = "9.1.1" - val NettyVersion = "4.1.93.Final" + val NettyVersion = "4.1.98.Final" val NettyIncubatorVersion = "0.0.20.Final" val ScalaCompactCollectionVersion = "2.11.0" val ZioVersion = "2.0.17" From 748957097827bd1f4c61c57b9d5e9d561812bcdb Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Sun, 24 Sep 2023 09:29:24 -0700 Subject: [PATCH 13/22] Do Not Continue Reading From Web Socket After Terminal Event (#2441) do not continue reading after terminal event --- .../src/main/scala/zio/http/TestChannel.scala | 12 ++++++ .../src/main/scala/zio/http/Channel.scala | 37 ++++++++++--------- .../scala/zio/http/WebSocketChannel.scala | 13 +++++++ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/zio-http-testkit/src/main/scala/zio/http/TestChannel.scala b/zio-http-testkit/src/main/scala/zio/http/TestChannel.scala index 05489d2895..6e3e118fa1 100644 --- a/zio-http-testkit/src/main/scala/zio/http/TestChannel.scala +++ b/zio-http-testkit/src/main/scala/zio/http/TestChannel.scala @@ -14,6 +14,18 @@ case class TestChannel( promise.await def receive(implicit trace: Trace): Task[WebSocketChannelEvent] = in.take + def receiveAll[Env, Err](f: WebSocketChannelEvent => ZIO[Env, Err, Any])(implicit + trace: Trace, + ): ZIO[Env, Err, Unit] = { + lazy val loop: ZIO[Env, Err, Unit] = + in.take.flatMap { + case event @ ChannelEvent.ExceptionCaught(_) => f(event).unit + case event @ ChannelEvent.Unregistered => f(event).unit + case event => f(event) *> ZIO.yieldNow *> loop + } + + loop + } def send(in: WebSocketChannelEvent)(implicit trace: Trace): Task[Unit] = out.offer(in).unit def sendAll(in: Iterable[WebSocketChannelEvent])(implicit trace: Trace): Task[Unit] = diff --git a/zio-http/src/main/scala/zio/http/Channel.scala b/zio-http/src/main/scala/zio/http/Channel.scala index 348f630ecd..a18527bc37 100644 --- a/zio-http/src/main/scala/zio/http/Channel.scala +++ b/zio-http/src/main/scala/zio/http/Channel.scala @@ -36,6 +36,12 @@ trait Channel[-In, +Out] { self => */ def receive(implicit trace: Trace): Task[Out] + /** + * Reads all messages from the channel, handling them with the specified + * function. + */ + def receiveAll[Env, Err](f: Out => ZIO[Env, Err, Any])(implicit trace: Trace): ZIO[Env, Err, Unit] + /** * Send a message to the channel. */ @@ -57,15 +63,17 @@ trait Channel[-In, +Out] { self => */ final def contramap[In2](f: In2 => In): Channel[In2, Out] = new Channel[In2, Out] { - def awaitShutdown(implicit trace: Trace): UIO[Unit] = + def awaitShutdown(implicit trace: Trace): UIO[Unit] = self.awaitShutdown - def receive(implicit trace: Trace): Task[Out] = + def receive(implicit trace: Trace): Task[Out] = self.receive - def send(in: In2)(implicit trace: Trace): Task[Unit] = + def receiveAll[Env, Err](g: Out => ZIO[Env, Err, Any])(implicit trace: Trace): ZIO[Env, Err, Unit] = + self.receiveAll(g) + def send(in: In2)(implicit trace: Trace): Task[Unit] = self.send(f(in)) - def sendAll(in: Iterable[In2])(implicit trace: Trace): Task[Unit] = + def sendAll(in: Iterable[In2])(implicit trace: Trace): Task[Unit] = self.sendAll(in.map(f)) - def shutdown(implicit trace: Trace): UIO[Unit] = + def shutdown(implicit trace: Trace): UIO[Unit] = self.shutdown } @@ -75,22 +83,17 @@ trait Channel[-In, +Out] { self => */ final def map[Out2](f: Out => Out2)(implicit trace: Trace): Channel[In, Out2] = new Channel[In, Out2] { - def awaitShutdown(implicit trace: Trace): UIO[Unit] = + def awaitShutdown(implicit trace: Trace): UIO[Unit] = self.awaitShutdown - def receive(implicit trace: Trace): Task[Out2] = + def receive(implicit trace: Trace): Task[Out2] = self.receive.map(f) - def send(in: In)(implicit trace: Trace): Task[Unit] = + def receiveAll[Env, Err](g: Out2 => ZIO[Env, Err, Any])(implicit trace: Trace): ZIO[Env, Err, Unit] = + self.receiveAll(f andThen g) + def send(in: In)(implicit trace: Trace): Task[Unit] = self.send(in) - def sendAll(in: Iterable[In])(implicit trace: Trace): Task[Unit] = + def sendAll(in: Iterable[In])(implicit trace: Trace): Task[Unit] = self.sendAll(in) - def shutdown(implicit trace: Trace): UIO[Unit] = + def shutdown(implicit trace: Trace): UIO[Unit] = self.shutdown } - - /** - * Reads all messages from the channel, handling them with the specified - * function. - */ - final def receiveAll[Env](f: Out => ZIO[Env, Throwable, Any])(implicit trace: Trace): ZIO[Env, Throwable, Nothing] = - receive.flatMap(f).forever } diff --git a/zio-http/src/main/scala/zio/http/WebSocketChannel.scala b/zio-http/src/main/scala/zio/http/WebSocketChannel.scala index 434b6ae5d5..6bc4bd3e42 100644 --- a/zio-http/src/main/scala/zio/http/WebSocketChannel.scala +++ b/zio-http/src/main/scala/zio/http/WebSocketChannel.scala @@ -38,6 +38,19 @@ private[http] object WebSocketChannel { def receive(implicit trace: Trace): Task[WebSocketChannelEvent] = queue.take + def receiveAll[Env, Err](f: WebSocketChannelEvent => ZIO[Env, Err, Any])(implicit + trace: Trace, + ): ZIO[Env, Err, Unit] = { + lazy val loop: ZIO[Env, Err, Unit] = + queue.take.flatMap { + case event @ ChannelEvent.ExceptionCaught(_) => f(event).unit + case event @ ChannelEvent.Unregistered => f(event).unit + case event => f(event) *> ZIO.yieldNow *> loop + } + + loop + } + def send(in: WebSocketChannelEvent)(implicit trace: Trace): Task[Unit] = { in match { case Read(message) => nettyChannel.writeAndFlush(frameToNetty(message)) From 06014f5e8c55e5cd98271ea6cf7c63ce5854ee70 Mon Sep 17 00:00:00 2001 From: ioleo Date: Sun, 24 Sep 2023 20:38:42 +0100 Subject: [PATCH 14/22] Update Handler.scala (#2453) * Update Handler.scala Fix #2372 * Update Handler.scala * Scalafmt * Fix sizeIs --------- Co-authored-by: ioleo --- zio-http/src/main/scala/zio/http/Handler.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/zio-http/src/main/scala/zio/http/Handler.scala b/zio-http/src/main/scala/zio/http/Handler.scala index fd62831833..08f7a72b2c 100644 --- a/zio-http/src/main/scala/zio/http/Handler.scala +++ b/zio-http/src/main/scala/zio/http/Handler.scala @@ -660,6 +660,16 @@ sealed trait Handler[-R, +Err, -In, +Out] { self => object Handler { + def asChunkBounded(request: Request, limit: Int)(implicit trace: Trace): Handler[Any, Throwable, Any, Chunk[Byte]] = + Handler.fromZIO( + request.body.asStream.chunks + .runFoldZIO(Chunk.empty[Byte]) { case (acc, bytes) => + ZIO + .succeed(acc ++ bytes) + .filterOrFail(_.size < limit)(new Exception("Too large input")) + }, + ) + /** * Attempts to create a Handler that succeeds with the provided value, * capturing all exceptions on it's way. From 798c17f9134498ccafd9e62a39b617dd7d9456b4 Mon Sep 17 00:00:00 2001 From: Jorge Aliss Date: Sun, 24 Sep 2023 16:39:43 -0300 Subject: [PATCH 15/22] Add missing traces to the http package (#2354) --- .../src/main/scala/zio/http/Handler.scala | 2 +- .../src/main/scala/zio/http/HttpApp.scala | 10 ++-- zio-http/src/main/scala/zio/http/Route.scala | 21 ++++---- zio-http/src/main/scala/zio/http/Routes.scala | 11 ++-- .../src/main/scala/zio/http/ZClient.scala | 52 ++++++++++--------- 5 files changed, 51 insertions(+), 45 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Handler.scala b/zio-http/src/main/scala/zio/http/Handler.scala index 08f7a72b2c..52bb0ea89a 100644 --- a/zio-http/src/main/scala/zio/http/Handler.scala +++ b/zio-http/src/main/scala/zio/http/Handler.scala @@ -586,7 +586,7 @@ sealed trait Handler[-R, +Err, -In, +Out] { self => * the handler has been appropriately sandboxed, turning all possible failures * into well-formed HTTP responses. */ - def toHttpApp(implicit err: Err <:< Response, in: Request <:< In, out: Out <:< Response): HttpApp[R] = { + def toHttpApp(implicit err: Err <:< Response, in: Request <:< In, out: Out <:< Response, trace: Trace): HttpApp[R] = { val handler: Handler[R, Response, Request, Response] = self.asInstanceOf[Handler[R, Response, Request, Response]] diff --git a/zio-http/src/main/scala/zio/http/HttpApp.scala b/zio-http/src/main/scala/zio/http/HttpApp.scala index f6d5ab9cd4..ad4fbec202 100644 --- a/zio-http/src/main/scala/zio/http/HttpApp.scala +++ b/zio-http/src/main/scala/zio/http/HttpApp.scala @@ -58,7 +58,7 @@ final case class HttpApp[-Env](routes: Routes[Env, Response]) * method and path of the specified request. */ def isDefinedAt(request: Request): Boolean = - tree.get(request.method, request.path).nonEmpty + tree(Trace.empty).get(request.method, request.path).nonEmpty /** * Provides the specified environment to the HTTP application, returning a new @@ -115,7 +115,7 @@ final case class HttpApp[-Env](routes: Routes[Env, Response]) /** * Accesses the underlying tree that provides fast dispatch to handlers. */ - def tree: HttpApp.Tree[Env] = { + def tree(implicit trace: Trace): HttpApp.Tree[Env] = { if (_tree eq null) { _tree = HttpApp.Tree.fromRoutes(routes) } @@ -150,10 +150,10 @@ object HttpApp { final def ++[Env1 <: Env](that: Tree[Env1]): Tree[Env1] = Tree(self.tree ++ that.tree) - final def add[Env1 <: Env](route: Route[Env1, Response]): Tree[Env1] = + final def add[Env1 <: Env](route: Route[Env1, Response])(implicit trace: Trace): Tree[Env1] = Tree(self.tree.add(route.routePattern, route.toHandler)) - final def addAll[Env1 <: Env](routes: Iterable[Route[Env1, Response]]): Tree[Env1] = + final def addAll[Env1 <: Env](routes: Iterable[Route[Env1, Response]])(implicit trace: Trace): Tree[Env1] = Tree(self.tree.addAll(routes.map(r => (r.routePattern, r.toHandler)))) final def get(method: Method, path: Path): Chunk[RequestHandler[Env, Response]] = @@ -162,7 +162,7 @@ object HttpApp { private[http] object Tree { val empty: Tree[Any] = Tree(RoutePattern.Tree.empty) - def fromRoutes[Env](routes: Routes[Env, Response]): Tree[Env] = + def fromRoutes[Env](routes: Routes[Env, Response])(implicit trace: Trace): Tree[Env] = empty.addAll(routes.routes) } } diff --git a/zio-http/src/main/scala/zio/http/Route.scala b/zio-http/src/main/scala/zio/http/Route.scala index e67a709168..0e0ec8334e 100644 --- a/zio-http/src/main/scala/zio/http/Route.scala +++ b/zio-http/src/main/scala/zio/http/Route.scala @@ -16,6 +16,7 @@ package zio.http import zio._ +import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.http.Route.Provided @@ -40,7 +41,7 @@ sealed trait Route[-Env, +Err] { self => * call this function when you have handled all errors produced by the route, * converting them into responses. */ - final def apply(request: Request)(implicit ev: Err <:< Response): ZIO[Env, Response, Response] = + final def apply(request: Request)(implicit ev: Err <:< Response, trace: Trace): ZIO[Env, Response, Response] = toHandler.apply(request) def asErrorType[Err2](implicit ev: Err <:< Err2): Route[Env, Err2] = self.asInstanceOf[Route[Env, Err2]] @@ -49,14 +50,14 @@ sealed trait Route[-Env, +Err] { self => * Handles the error of the route. This method can be used to convert a route * that does not handle its errors into one that does handle its errors. */ - final def handleError(f: Err => Response): Route[Env, Nothing] = + final def handleError(f: Err => Response)(implicit trace: Trace): Route[Env, Nothing] = self.handleErrorCause(Response.fromCauseWith(_)(f)) /** * Handles the error of the route. This method can be used to convert a route * that does not handle its errors into one that does handle its errors. */ - final def handleErrorCause(f: Cause[Err] => Response): Route[Env, Nothing] = + final def handleErrorCause(f: Cause[Err] => Response)(implicit trace: Trace): Route[Env, Nothing] = self match { case Provided(route, env) => Provided(route.handleErrorCause(f), env) case Augmented(route, aspect) => Augmented(route.handleErrorCause(f), aspect) @@ -107,10 +108,10 @@ sealed trait Route[-Env, +Err] { self => * using best-effort heuristics to determine the appropriate HTTP status code, * and attaching error details using the HTTP header `Warning`. */ - final def sandbox: Route[Env, Nothing] = + final def sandbox(implicit trace: Trace): Route[Env, Nothing] = handleErrorCause(Response.fromCause(_)) - def toHandler(implicit ev: Err <:< Response): Handler[Env, Response, Request, Response] + def toHandler(implicit ev: Err <:< Response, trace: Trace): Handler[Env, Response, Request, Response] final def toHttpApp(implicit ev: Err <:< Response): HttpApp[Env] = Routes(self).toHttpApp @@ -223,7 +224,7 @@ object Route { def routePattern: RoutePattern[_] = route.routePattern - override def toHandler(implicit ev: Err <:< Response): Handler[Any, Response, Request, Response] = + override def toHandler(implicit ev: Err <:< Response, trace: Trace): Handler[Any, Response, Request, Response] = route.toHandler.provideEnvironment(env) override def toString() = s"Route.Provided(${route}, ${env})" @@ -237,7 +238,7 @@ object Route { def routePattern: RoutePattern[_] = route.routePattern - override def toHandler(implicit ev: Err <:< Response): Handler[OutEnv, Response, Request, Response] = + override def toHandler(implicit ev: Err <:< Response, trace: Trace): Handler[OutEnv, Response, Request, Response] = aspect(route.toHandler) override def toString() = s"Route.Augmented(${route}, ${aspect})" @@ -248,7 +249,7 @@ object Route { handler: Handler[Env, Response, Request, Response], location: Trace, ) extends Route[Env, Nothing] { - override def toHandler(implicit ev: Nothing <:< Response): Handler[Env, Response, Request, Response] = + override def toHandler(implicit ev: Nothing <:< Response, trace: Trace): Handler[Env, Response, Request, Response] = handler override def toString() = s"Route.Handled(${routePattern}, ${location})" @@ -262,7 +263,7 @@ object Route { def routePattern = rpm.routePattern - override def toHandler(implicit ev: Err <:< Response): Handler[Env, Response, Request, Response] = { + override def toHandler(implicit ev: Err <:< Response, trace: Trace): Handler[Env, Response, Request, Response] = { convert(handler.asErrorType[Response]) } @@ -270,7 +271,7 @@ object Route { private def convert[Env1 <: Env]( handler: Handler[Env1, Response, Input, Response], - ): Handler[Env1, Response, Request, Response] = { + )(implicit trace: Trace): Handler[Env1, Response, Request, Response] = { implicit val z = zippable Route.handled(rpm)(handler).toHandler diff --git a/zio-http/src/main/scala/zio/http/Routes.scala b/zio-http/src/main/scala/zio/http/Routes.scala index b56e9abf1c..bc8845d1d1 100644 --- a/zio-http/src/main/scala/zio/http/Routes.scala +++ b/zio-http/src/main/scala/zio/http/Routes.scala @@ -16,6 +16,7 @@ package zio.http import zio._ +import zio.stacktracer.TracingImplicits.disableAutoTrace /** * Represents a collection of routes, each of which is defined by a pattern and @@ -65,14 +66,14 @@ final class Routes[-Env, +Err] private (val routes: Chunk[zio.http.Route[Env, Er /** * Handles all typed errors in the routes by converting them into responses. */ - def handleError(f: Err => Response): Routes[Env, Nothing] = + def handleError(f: Err => Response)(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleError(f))) /** * Handles all typed errors, as well as all non-recoverable errors, by * converting them into responses. */ - def handleErrorCause(f: Cause[Err] => Response): Routes[Env, Nothing] = + def handleErrorCause(f: Cause[Err] => Response)(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleErrorCause(f))) /** @@ -87,14 +88,14 @@ final class Routes[-Env, +Err] private (val routes: Chunk[zio.http.Route[Env, Er * responses, using best-effort heuristics to determine the appropriate HTTP * status code, and attaching error details using the HTTP header `Warning`. */ - def sandbox: Routes[Env, Nothing] = + def sandbox(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.sandbox)) /** * Returns new routes that are all timed out by the specified maximum * duration. */ - def timeout(duration: Duration): Routes[Env, Err] = + def timeout(duration: Duration)(implicit trace: Trace): Routes[Env, Err] = self @@ Middleware.timeout(duration) /** @@ -136,6 +137,6 @@ object Routes { * Constructs a singleton route from a handler that handles all possible * methods and paths. You would only use this method for testing. */ - def singleton[Env, Err](h: Handler[Env, Err, (Path, Request), Response]): Routes[Env, Err] = + def singleton[Env, Err](h: Handler[Env, Err, (Path, Request), Response])(implicit trace: Trace): Routes[Env, Err] = Routes(Route.route(RoutePattern.any)(h)) } diff --git a/zio-http/src/main/scala/zio/http/ZClient.scala b/zio-http/src/main/scala/zio/http/ZClient.scala index 91df9c244a..477ee85cc9 100644 --- a/zio-http/src/main/scala/zio/http/ZClient.scala +++ b/zio-http/src/main/scala/zio/http/ZClient.scala @@ -80,7 +80,7 @@ final case class ZClient[-Env, -In, +Err, +Out]( def addUrl(url: URL): ZClient[Env, In, Err, Out] = copy(url = self.url ++ url) - def contramap[In2](f: In2 => In): ZClient[Env, In2, Err, Out] = + def contramap[In2](f: In2 => In)(implicit trace: Trace): ZClient[Env, In2, Err, Out] = contramapZIO(in => ZIO.succeed(f(in))) def contramapZIO[Env1 <: Env, Err1 >: Err, In2](f: In2 => ZIO[Env1, Err1, In]): ZClient[Env1, In2, Err1, Out] = @@ -90,7 +90,7 @@ final case class ZClient[-Env, -In, +Err, +Out]( self.driver, ) - def delete(suffix: String)(implicit ev: Body <:< In): ZIO[Env & Scope, Err, Out] = + def delete(suffix: String)(implicit ev: Body <:< In, trace: Trace): ZIO[Env & Scope, Err, Out] = request(Method.DELETE, suffix)(ev(Body.empty)) def dieOn( @@ -98,26 +98,26 @@ final case class ZClient[-Env, -In, +Err, +Out]( )(implicit ev1: Err IsSubtypeOfError Throwable, ev2: CanFail[Err], trace: Trace): ZClient[Env, In, Err, Out] = refineOrDie { case e if !f(e) => e } - def disableStreaming(implicit ev: Err <:< Throwable): ZClient[Env, In, Throwable, Out] = + def disableStreaming(implicit ev: Err <:< Throwable, trace: Trace): ZClient[Env, In, Throwable, Out] = transform(bodyEncoder.widenError[Throwable], bodyDecoder.widenError[Throwable], driver.disableStreaming) - def get(suffix: String)(implicit ev: Body <:< In): ZIO[Env & Scope, Err, Out] = + def get(suffix: String)(implicit ev: Body <:< In, trace: Trace): ZIO[Env & Scope, Err, Out] = request(Method.GET, suffix)(ev(Body.empty)) - def head(suffix: String)(implicit ev: Body <:< In): ZIO[Env & Scope, Err, Out] = + def head(suffix: String)(implicit ev: Body <:< In, trace: Trace): ZIO[Env & Scope, Err, Out] = request(Method.HEAD, suffix)(ev(Body.empty)) def host(host: String): ZClient[Env, In, Err, Out] = copy(url = url.host(host)) - def map[Out2](f: Out => Out2): ZClient[Env, In, Err, Out2] = + def map[Out2](f: Out => Out2)(implicit trace: Trace): ZClient[Env, In, Err, Out2] = mapZIO(out => ZIO.succeed(f(out))) def mapError[Err2](f: Err => Err2): ZClient[Env, In, Err2, Out] = transform( bodyEncoder.mapError(f), new ZClient.BodyDecoder[Env, Err2, Out] { - def decode(response: Response): ZIO[Env, Err2, Out] = + def decode(response: Response)(implicit trace: Trace): ZIO[Env, Err2, Out] = self.bodyDecoder.decode(response).mapError(f) }, driver.mapError(f), @@ -135,7 +135,7 @@ final case class ZClient[-Env, -In, +Err, +Out]( def path(path: Path): ZClient[Env, In, Err, Out] = copy(url = url.copy(path = path)) - def patch(suffix: String)(implicit ev: Body <:< In): ZIO[Env & Scope, Err, Out] = + def patch(suffix: String)(implicit ev: Body <:< In, trace: Trace): ZIO[Env & Scope, Err, Out] = request(Method.PATCH, suffix)(ev(Body.empty)) def port(port: Int): ZClient[Env, In, Err, Out] = @@ -152,7 +152,7 @@ final case class ZClient[-Env, -In, +Err, +Out]( )(implicit ev1: Err IsSubtypeOfError Throwable, ev2: CanFail[Err], trace: Trace): ZClient[Env, In, Err2, Out] = transform(bodyEncoder.refineOrDie(pf), bodyDecoder.refineOrDie(pf), driver.refineOrDie(pf)) - def request(request: Request)(implicit ev: Body <:< In): ZIO[Env & Scope, Err, Out] = + def request(request: Request)(implicit ev: Body <:< In, trace: Trace): ZIO[Env & Scope, Err, Out] = if (bodyEncoder == ZClient.BodyEncoder.identity) bodyDecoder.decodeZIO( driver @@ -251,7 +251,7 @@ object ZClient { def configured( path: NonEmptyChunk[String] = NonEmptyChunk("zio", "http", "client"), - ): ZLayer[DnsResolver, Throwable, Client] = + )(implicit trace: Trace): ZLayer[DnsResolver, Throwable, Client] = ( ZLayer.service[DnsResolver] ++ ZLayer(ZIO.config(Config.config.nested(path.head, path.tail: _*))) ++ @@ -298,33 +298,36 @@ object ZClient { (NettyClientDriver.live ++ ZLayer.service[DnsResolver]) >>> customized }.fresh - def request(request: Request): ZIO[Client & Scope, Throwable, Response] = + def request(request: Request)(implicit trace: Trace): ZIO[Client & Scope, Throwable, Response] = ZIO.serviceWithZIO[Client](c => c(request)) - def socket[R](socketApp: WebSocketApp[R]): ZIO[R with Client & Scope, Throwable, Response] = + def socket[R](socketApp: WebSocketApp[R])(implicit trace: Trace): ZIO[R with Client & Scope, Throwable, Response] = ZIO.serviceWithZIO[Client](c => c.socket(socketApp)) trait BodyDecoder[-Env, +Err, +Out] { self => - def decode(response: Response): ZIO[Env, Err, Out] + def decode(response: Response)(implicit trace: Trace): ZIO[Env, Err, Out] - def decodeZIO[Env1 <: Env, Err1 >: Err](zio: ZIO[Env1, Err1, Response]): ZIO[Env1, Err1, Out] = + def decodeZIO[Env1 <: Env, Err1 >: Err](zio: ZIO[Env1, Err1, Response])(implicit + trace: Trace, + ): ZIO[Env1, Err1, Out] = zio.flatMap(decode) final def mapError[Err2](f: Err => Err2): BodyDecoder[Env, Err2, Out] = new BodyDecoder[Env, Err2, Out] { - def decode(response: Response): ZIO[Env, Err2, Out] = self.decode(response).mapError(f) + def decode(response: Response)(implicit trace: Trace): ZIO[Env, Err2, Out] = self.decode(response).mapError(f) } final def mapZIO[Env1 <: Env, Err1 >: Err, Out2](f: Out => ZIO[Env1, Err1, Out2]): BodyDecoder[Env1, Err1, Out2] = new BodyDecoder[Env1, Err1, Out2] { - def decode(response: Response): ZIO[Env1, Err1, Out2] = self.decode(response).flatMap(f) + def decode(response: Response)(implicit trace: Trace): ZIO[Env1, Err1, Out2] = self.decode(response).flatMap(f) } final def refineOrDie[Err2]( pf: PartialFunction[Err, Err2], )(implicit ev1: Err IsSubtypeOfError Throwable, ev2: CanFail[Err], trace: Trace): BodyDecoder[Env, Err2, Out] = new BodyDecoder[Env, Err2, Out] { - def decode(response: Response): ZIO[Env, Err2, Out] = self.decode(response).refineOrDie(pf) + def decode(response: Response)(implicit trace: Trace): ZIO[Env, Err2, Out] = + self.decode(response).refineOrDie(pf) } final def widenError[E1](implicit ev: Err <:< E1): BodyDecoder[Env, E1, Out] = @@ -333,32 +336,33 @@ object ZClient { object BodyDecoder { val identity: BodyDecoder[Any, Nothing, Response] = new BodyDecoder[Any, Nothing, Response] { - final def decode(response: Response): ZIO[Any, Nothing, Response] = Exit.succeed(response) + final def decode(response: Response)(implicit trace: Trace): ZIO[Any, Nothing, Response] = + Exit.succeed(response) override def decodeZIO[Env1 <: Any, Err1 >: Nothing]( zio: ZIO[Env1, Err1, Response], - ): ZIO[Env1, Err1, Response] = + )(implicit trace: Trace): ZIO[Env1, Err1, Response] = zio } } trait BodyEncoder[-Env, +Err, -In] { self => final def contramapZIO[Env1 <: Env, Err1 >: Err, In2](f: In2 => ZIO[Env1, Err1, In]): BodyEncoder[Env1, Err1, In2] = new BodyEncoder[Env1, Err1, In2] { - def encode(in: In2): ZIO[Env1, Err1, Body] = f(in).flatMap(self.encode) + def encode(in: In2)(implicit trace: Trace): ZIO[Env1, Err1, Body] = f(in).flatMap(self.encode) } - def encode(in: In): ZIO[Env, Err, Body] + def encode(in: In)(implicit trace: Trace): ZIO[Env, Err, Body] final def mapError[Err2](f: Err => Err2): BodyEncoder[Env, Err2, In] = new BodyEncoder[Env, Err2, In] { - def encode(in: In): ZIO[Env, Err2, Body] = self.encode(in).mapError(f) + def encode(in: In)(implicit trace: Trace): ZIO[Env, Err2, Body] = self.encode(in).mapError(f) } final def refineOrDie[Err2]( pf: PartialFunction[Err, Err2], )(implicit ev1: Err IsSubtypeOfError Throwable, ev2: CanFail[Err], trace: Trace): BodyEncoder[Env, Err2, In] = new BodyEncoder[Env, Err2, In] { - def encode(in: In): ZIO[Env, Err2, Body] = self.encode(in).refineOrDie(pf) + def encode(in: In)(implicit trace: Trace): ZIO[Env, Err2, Body] = self.encode(in).refineOrDie(pf) } final def widenError[E1](implicit ev: Err <:< E1): BodyEncoder[Env, E1, In] = @@ -367,7 +371,7 @@ object ZClient { object BodyEncoder { val identity: BodyEncoder[Any, Nothing, Body] = new BodyEncoder[Any, Nothing, Body] { - def encode(body: Body): ZIO[Any, Nothing, Body] = Exit.succeed(body) + def encode(body: Body)(implicit trace: Trace): ZIO[Any, Nothing, Body] = Exit.succeed(body) } } From 8559750f7ad012b517dd308ffb08a3bfbe759f0c Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 21:57:51 +0200 Subject: [PATCH 16/22] add note regarding classical "redirect after post" situations (#2434) --- zio-http/src/main/scala/zio/http/Response.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zio-http/src/main/scala/zio/http/Response.scala b/zio-http/src/main/scala/zio/http/Response.scala index b01d42cff5..75c33ed68b 100644 --- a/zio-http/src/main/scala/zio/http/Response.scala +++ b/zio-http/src/main/scala/zio/http/Response.scala @@ -263,8 +263,11 @@ object Response { def ok: Response = status(Status.Ok) /** - * Creates an empty response with status 301 or 302 depending on if it's + * Creates an empty response with status 307 or 308 depending on if it's * permanent or not. + * + * Note: if you intend to always redirect a browser with a HTTP GET to the + * given location you very likely should use `Response#seeOther` instead. */ def redirect(location: URL, isPermanent: Boolean = false): Response = { val status = if (isPermanent) Status.PermanentRedirect else Status.TemporaryRedirect @@ -272,7 +275,7 @@ object Response { } /** - * Creates an empty response with status 303 + * Creates an empty response with status 303. */ def seeOther(location: URL): Response = Response(status = Status.SeeOther, headers = Headers(Header.Location(location))) From 037bc9922c4fd90e9833358e6392ade1bd1aa84a Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 22:03:38 +0200 Subject: [PATCH 17/22] add flash scope (#2451) --- zio-http/src/main/scala/zio/http/Middleware.scala | 7 +++++++ zio-http/src/main/scala/zio/http/Request.scala | 7 +++++++ zio-http/src/main/scala/zio/http/Response.scala | 3 +++ 3 files changed, 17 insertions(+) diff --git a/zio-http/src/main/scala/zio/http/Middleware.scala b/zio-http/src/main/scala/zio/http/Middleware.scala index 335281b09f..a6daeec390 100644 --- a/zio-http/src/main/scala/zio/http/Middleware.scala +++ b/zio-http/src/main/scala/zio/http/Middleware.scala @@ -243,4 +243,11 @@ object Middleware extends HandlerAspects { ) } } + + /** + * Creates a middleware for managing the flash scope. + */ + def flashScopeHandling: HandlerAspect[Any, Unit] = Middleware.intercept { (req, resp) => + req.cookie("zio-http-flash").fold(resp)(flash => resp.addCookie(Cookie.clear(flash.name))) + } } diff --git a/zio-http/src/main/scala/zio/http/Request.scala b/zio-http/src/main/scala/zio/http/Request.scala index 397ccf87a9..8cc9ccfe09 100644 --- a/zio-http/src/main/scala/zio/http/Request.scala +++ b/zio-http/src/main/scala/zio/http/Request.scala @@ -106,6 +106,13 @@ final case class Request( */ def unnest(prefix: Path): Request = copy(url = self.url.copy(path = self.url.path.unnest(prefix))) + + def cookie(name: String): Option[Cookie] = + header(Header.Cookie).map(_.value).flatMap(_.filter(_.name == name).headOption) + + def flashMessage: Option[String] = + cookie("zio-http-flash").map(_.content) + } object Request { diff --git a/zio-http/src/main/scala/zio/http/Response.scala b/zio-http/src/main/scala/zio/http/Response.scala index 75c33ed68b..4a630f4d54 100644 --- a/zio-http/src/main/scala/zio/http/Response.scala +++ b/zio-http/src/main/scala/zio/http/Response.scala @@ -40,6 +40,9 @@ final case class Response( def addCookie(cookie: Cookie.Response): Response = self.copy(headers = self.headers ++ Headers(Header.SetCookie(cookie))) + def addFlashMessage(message: String): Response = + addCookie(Cookie.Response("zio-http-flash", message)) + /** * Collects the potentially streaming body of the response into a single * chunk. From 09673209b438efe894e850b9397e3732916712f6 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 22:20:59 +0200 Subject: [PATCH 18/22] Update sbt-scala3-migrate to 0.6.1 (#2435) * Update sbt-scala3-migrate to 0.6.1 * Revert commit(s) 08ab8ad0 * Update sbt-scala3-migrate to 0.6.1 --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 0940fb6eef..ed0460fa9b 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") 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.16.0") -addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.5.1") +addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.6.1") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.3.10") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") From ed8124bcdfad24d13eabc3d0ff19627f4f20e688 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 22:28:03 +0200 Subject: [PATCH 19/22] Update zio-schema, zio-schema-json, ... to 0.4.14 (#2436) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e3a33b8568..692852292c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -8,7 +8,7 @@ object Dependencies { val ScalaCompactCollectionVersion = "2.11.0" val ZioVersion = "2.0.17" val ZioCliVersion = "0.5.0" - val ZioSchemaVersion = "0.4.13" + val ZioSchemaVersion = "0.4.14" val SttpVersion = "3.3.18" val `jwt-core` = "com.github.jwt-scala" %% "jwt-core" % JwtCoreVersion From 1408f9c84875d17f6ede04a9274298df3bca0573 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 22:28:23 +0200 Subject: [PATCH 20/22] Update sbt to 1.9.6 (#2449) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 40b3b8e7b6..27430827bc 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.0 +sbt.version=1.9.6 From 48af67d7fe37d7414429abd42750ff7f6bfd17f8 Mon Sep 17 00:00:00 2001 From: TomTriple Date: Sun, 24 Sep 2023 22:49:54 +0200 Subject: [PATCH 21/22] use richtext codec for content type (#2455) --- zio-http/src/main/scala/zio/http/Header.scala | 163 +++++++++++++----- .../src/main/scala/zio/http/MediaType.scala | 1 + .../scala/zio/http/codec/RichTextCodec.scala | 12 +- .../zio/http/headers/ContentTypeSpec.scala | 86 +++++++++ 4 files changed, 218 insertions(+), 44 deletions(-) create mode 100644 zio-http/src/test/scala/zio/http/headers/ContentTypeSpec.scala diff --git a/zio-http/src/main/scala/zio/http/Header.scala b/zio-http/src/main/scala/zio/http/Header.scala index 0986872759..96df8c62d3 100644 --- a/zio-http/src/main/scala/zio/http/Header.scala +++ b/zio-http/src/main/scala/zio/http/Header.scala @@ -29,6 +29,7 @@ import scala.util.{Either, Failure, Success, Try} import zio._ +import zio.http.codec.RichTextCodec import zio.http.internal.DateEncoding sealed trait Header { @@ -2460,58 +2461,138 @@ object Header { final case class ContentType(mediaType: MediaType, boundary: Option[Boundary] = None, charset: Option[Charset] = None) extends Header { override type Self = ContentType - override def self: Self = this + + override def self: Self = this + override def headerType: HeaderType.Typed[ContentType] = ContentType } object ContentType extends HeaderType { + override type HeaderValue = ContentType + override def name: String = "content-type" - def parse(s: String): Either[String, ContentType] = { - Chunk.fromArray(s.split(";")).map(_.trim) match { - case Chunk(mediaType) => - MediaType.forContentType(mediaType).toRight("Invalid Content-Type header").map(ContentType(_, None, None)) - case Chunk(mediaType, directive) if directive.startsWith("charset=") => - for { - mediaType <- MediaType.forContentType(mediaType).toRight("Invalid Content-Type header") - charset <- - try Right(Charset.forName(directive.drop(8))) - catch { case _: UnsupportedCharsetException => Left("Invalid charset in Content-Type header") } - } yield ContentType(mediaType, None, Some(charset)) - case Chunk(mediaType, directive) if directive.startsWith("boundary=") => - for { - mediaType <- MediaType.forContentType(mediaType).toRight("Invalid Content-Type header") - boundary = directive.drop(9) - } yield ContentType(mediaType, Some(Boundary(boundary)), None) - case Chunk(mediaType, directive1, directive2) if directive1.startsWith("charset=") && directive2.startsWith("boundary=") => - for { - mediaType <- MediaType.forContentType(mediaType).toRight("Invalid Content-Type header") - charset <- - try Right(Charset.forName(directive1.drop(8))) - catch { case _: UnsupportedCharsetException => Left("Invalid charset in Content-Type header") } - boundary = directive2.drop(9) - } yield ContentType(mediaType, Some(Boundary(boundary)), Some(charset)) - case Chunk(mediaType, directive1, directive2) if directive1.startsWith("boundary=") && directive2.startsWith("charset=") => - for { - mediaType <- MediaType.forContentType(mediaType).toRight("Invalid Content-Type header") - charset <- - try Right(Charset.forName(directive2.drop(8))) - catch { case _: UnsupportedCharsetException => Left("Invalid charset in Content-Type header") } - boundary = directive1.drop(9) - } yield ContentType(mediaType, Some(Boundary(boundary)), Some(charset)) - case _ => - Left("Invalid Content-Type header") + def parse(s: String): Either[String, ContentType] = codec.decode(s) + + def render(contentType: ContentType): String = codec.encode(contentType).toOption.get + + private val codec: RichTextCodec[ContentType] = { + + // char `.` according to BNF not allowed as `token`, but here tolerated + val token = RichTextCodec.filter(_ => true).validate("not a token") { + case ' ' | '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '"' | '/' | '[' | ']' | '?' | '=' => false + case _ => true + } + val tokenQuoted = RichTextCodec.filter(_ => true).validate("not a quoted token") { + case ' ' | '"' => false + case _ => true + } + val type1 = RichTextCodec.string.collectOrFail("unsupported main type") { + case value if MediaType.mainTypeMap.get(value).isDefined => value + } + val type1x = (RichTextCodec.literalCI("x-") ~ token.repeat.string).transform[String](in => s"${in._1}${in._2}", in => ("x-", s"${in.substring(2)}")) + val codecType1 = (type1 | type1x).transform[String]( + _.merge, + { + case x if x.startsWith("x-") => Right(x) + case x => Left(x) + }, + ) + val codecType2 = token.repeat.string + val codecType = (codecType1 <~ RichTextCodec.char('/').const('/')) ~ codecType2 + val attribute = token.repeat.string + val valueUnquoted = token.repeat.string + val valueQuoted = RichTextCodec.char('"') ~ tokenQuoted.repeat.string ~ RichTextCodec.char('"') + val value = valueQuoted | valueUnquoted + + val param = (( + RichTextCodec.char(';').const(';') ~> + (RichTextCodec.whitespaceChar.repeat | RichTextCodec.empty).transform[Char](_ => ' ', _ => Left(Chunk(()))).const(' ') ~> + attribute <~ + RichTextCodec.char('=').const('=') + ) ~ value) + .transformOrFailLeft[ContentType.Parameter]( + in => ContentType.Parameter.fromCodec(in), + in => in.toCodec, + ) + val params = param.repeat + (codecType ~ params).transform[ContentType]( + { case (mainType, subType, params) => + ContentType( + MediaType.forContentType(s"$mainType/$subType").get, + params.collect { case p if p.key == ContentType.Parameter.Boundary.name => zio.http.Boundary(p.value) }.headOption, + params.collect { case p if p.key == ContentType.Parameter.Charset.name => java.nio.charset.Charset.forName(p.value) }.headOption, + ) + }, + in => + ( + in.mediaType.mainType, + in.mediaType.subType, + Chunk( + in.charset.map(in => Parameter.Charset(Parameter.Payload(Parameter.Charset.name, in, false))), + in.boundary.map(in => Parameter.Boundary(Parameter.Payload(Parameter.Boundary.name, in, false))), + ).flatten, + ), + ) + } + + private[Header] sealed trait Parameter { + self => + + def isQuoted: Boolean = self match { + case Parameter.Boundary(payload) => payload.isQuoted + case Parameter.Charset(payload) => payload.isQuoted + case Parameter.Value(payload) => payload.isQuoted } + + def key: String = self match { + case Parameter.Value(payload) => payload.key + case Parameter.Charset(payload) => payload.key + case Parameter.Boundary(payload) => payload.key + } + + def value: String = self match { + case Parameter.Value(payload) => payload.value + case Parameter.Boundary(payload) => payload.value.id + case Parameter.Charset(payload) => payload.value.toString.toLowerCase + } + + def toCodec: (String, Either[(Char, String, Char), String]) = + (self.key, if (self.isQuoted) Left(('"', self.value, '"')) else Right(self.value)) } - def render(contentType: ContentType): String = - (contentType.charset, contentType.boundary) match { - case (None, None) => contentType.mediaType.fullType - case (Some(charset), None) => contentType.mediaType.fullType + "; charset=" + charset.toString - case (None, Some(boundary)) => contentType.mediaType.fullType + "; boundary=" + boundary.id - case (Some(charset), Some(boundary)) => contentType.mediaType.fullType + "; charset=" + charset.toString + "; boundary=" + boundary.id + private[Header] object Parameter { + case class Payload[A](key: String, value: A, isQuoted: Boolean) + + case class Boundary(payload: Payload[zio.http.Boundary]) extends Parameter + + object Boundary { + val name = "boundary" + } + + case class Charset(payload: Payload[java.nio.charset.Charset]) extends Parameter + + object Charset { + val name = "charset" + } + + case class Value(payload: Payload[String]) extends Parameter + + def make(key: String, value: String, isQuoted: Boolean): Either[String, Parameter] = { + if (key == Parameter.Charset.name) { + try Right(Parameter.Charset(Payload(key, java.nio.charset.Charset.forName(value), isQuoted))) + catch { + case _: UnsupportedCharsetException => + Left(s"Invalid charset in Content-Type header: $value") + } + } else if (key == Parameter.Boundary.name) Right(Parameter.Boundary(Payload(key, zio.http.Boundary(value), isQuoted))) + else Right(Parameter.Value(Payload(key, value, isQuoted))) } + + def fromCodec(parse: (String, Either[(Char, String, Char), String])): Either[String, Parameter] = + Parameter.make(parse._1, parse._2.fold(a => a._2, identity), parse._2.isLeft) + } } final case class Date(value: ZonedDateTime) extends Header { diff --git a/zio-http/src/main/scala/zio/http/MediaType.scala b/zio-http/src/main/scala/zio/http/MediaType.scala index 3216ee9a09..97501dc88b 100644 --- a/zio-http/src/main/scala/zio/http/MediaType.scala +++ b/zio-http/src/main/scala/zio/http/MediaType.scala @@ -33,6 +33,7 @@ final case class MediaType( object MediaType extends MediaTypes { private val extensionMap: Map[String, MediaType] = allMediaTypes.flatMap(m => m.fileExtensions.map(_ -> m)).toMap private val contentTypeMap: Map[String, MediaType] = allMediaTypes.map(m => m.fullType -> m).toMap + val mainTypeMap = allMediaTypes.map(m => m.mainType -> m).toMap def forContentType(contentType: String): Option[MediaType] = { val index = contentType.indexOf(";") 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 d0dbefb322..6acbb08091 100644 --- a/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala +++ b/zio-http/src/main/scala/zio/http/codec/RichTextCodec.scala @@ -33,6 +33,9 @@ import zio.{Chunk, NonEmptyChunk} */ sealed trait RichTextCodec[A] { self => + final def string(implicit ev: A =:= Chunk[Char]): RichTextCodec[String] = + self.asType[Chunk[Char]].transform(_.mkString, a => Chunk(a.toList: _*)) + /** * Returns a new codec that is the sequential composition of this codec and * the specified codec, but which only produces the value of this codec. @@ -147,6 +150,7 @@ sealed trait RichTextCodec[A] { self => collectOrFail(failure) { case x if p(x) => x } + } object RichTextCodec { private[codec] case object Empty extends RichTextCodec[Unit] @@ -208,6 +212,8 @@ object RichTextCodec { */ val letter: RichTextCodec[Char] = filter(_.isLetter) ?! "letter" + val string: RichTextCodec[String] = letter.repeat.string + /** * A codec that describes a literal character sequence. */ @@ -494,7 +500,7 @@ object RichTextCodec { private def encode[A](value: A, self: RichTextCodec[A]): Either[String, String] = { self match { case RichTextCodec.Empty => Right("") - case RichTextCodec.CharIn(_) => { Right(value.asInstanceOf[Char].toString) } + case RichTextCodec.CharIn(_) => Right(value.asInstanceOf[Char].toString) case RichTextCodec.TransformOrFail(codec, _, from) => from(value) match { case Left(err) => Left(err) @@ -507,13 +513,12 @@ object RichTextCodec { case Right(b) => right.encode(b) } case RichTextCodec.Lazy(codec0) => codec0().encode(value) - case RichTextCodec.Zip(left, right, combiner) => { + case RichTextCodec.Zip(left, right, combiner) => val (a, b) = combiner.separate(value) for { l <- left.encode(a) r <- right.encode(b) } yield l + r - } case RichTextCodec.Tagged(_, codec, _) => codec.encode(value) } } @@ -552,6 +557,7 @@ object RichTextCodec { } yield (r._1, combiner.combine(l._2, r._2)) case RichTextCodec.Tagged(_, codec, _) => parse(value, codec) + } } diff --git a/zio-http/src/test/scala/zio/http/headers/ContentTypeSpec.scala b/zio-http/src/test/scala/zio/http/headers/ContentTypeSpec.scala new file mode 100644 index 0000000000..5093b4b0fd --- /dev/null +++ b/zio-http/src/test/scala/zio/http/headers/ContentTypeSpec.scala @@ -0,0 +1,86 @@ +/* + * 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.headers + +import zio.test._ + +import zio.http.Header.ContentType +import zio.http.{MediaType, ZIOHttpSpec} + +object ContentTypeSpec extends ZIOHttpSpec { + + override def spec = suite("ContentType header suite")( + test("parsing of invalid ContentType values") { + assertTrue( + ContentType.parse("").isLeft, + ContentType.parse("something").isLeft, + ) + }, + test("parsing of valid ContentType values") { + val charsetUtf8 = java.nio.charset.Charset.forName("utf-8") + val boundary = zio.http.Boundary("---------------------------974767299852498929531610575") + def checkBoth(in: String, result: ContentType) = + assertTrue( + ContentType.parse(in).toOption.get == result, + ) && assertTrue( + ContentType.render(result) == in, + ) + checkBoth("x-word/listing-text", ContentType(MediaType.parseCustomMediaType(s"x-word/listing-text").get)) && + checkBoth(MediaType.image.`png`.fullType, ContentType(MediaType.image.`png`)) && + checkBoth( + s"${MediaType.text.`html`.fullType}; charset=${charsetUtf8.toString.toLowerCase}", + ContentType(MediaType.text.`html`, None, Some(charsetUtf8)), + ) && + checkBoth( + s"${MediaType.text.`html`.fullType}; charset=${charsetUtf8.toString.toLowerCase}; boundary=${boundary.id}", + ContentType(MediaType.text.`html`, Some(boundary), Some(charsetUtf8)), + ) && + assertTrue( + ContentType + .parse(s"${MediaType.text.`html`.fullType};charset=${charsetUtf8.toString}") + .toOption + .get == ContentType(MediaType.text.`html`, None, Some(charsetUtf8)), + ) && assertTrue( + ContentType + .parse(s"""${MediaType.text.`html`.fullType};charset="${charsetUtf8.toString}"""") + .toOption + .get == ContentType(MediaType.text.`html`, None, Some(charsetUtf8)), + ) && assertTrue( + ContentType + .parse(s"""${MediaType.text.`html`.fullType};charset="${charsetUtf8.toString}"; version=1.0.0""") + .toOption + .get == ContentType(MediaType.text.`html`, None, Some(charsetUtf8)), + ) && assertTrue( + ContentType + .parse(s"""${MediaType.text.`html`.fullType};charset="${charsetUtf8.toString}"; version="1.0.0"""") + .toOption + .get == ContentType(MediaType.text.`html`, None, Some(charsetUtf8)), + ) && assertTrue( + ContentType + .parse(s"""${MediaType.text.`html`.fullType}; charset="${charsetUtf8.toString}"""") + .toOption + .get == ContentType(MediaType.text.`html`, None, Some(charsetUtf8)), + ) + }, + test("parsing and encoding is symmetrical") { + val results = + MediaType.allMediaTypes.map(mediaType => ContentType.render(ContentType.parse(mediaType.fullType).toOption.get)) + assertTrue(MediaType.allMediaTypes.map(_.fullType) == results) + }, + ) + +} From 4123b171724add70d5c75afbd2ef13385291d362 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 24 Sep 2023 22:51:46 +0200 Subject: [PATCH 22/22] Update sbt-shading to 2.1.3 (#2448) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index ed0460fa9b..f09a0551b4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -9,4 +9,4 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.3.10") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.8") -addSbtPlugin("io.get-coursier" % "sbt-shading" % "2.1.1") +addSbtPlugin("io.get-coursier" % "sbt-shading" % "2.1.3")