From e75382e099c2fb2450601c34edcd5cfb158d22f9 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:42:47 +0100 Subject: [PATCH] Schema based `HeaderOps` (#3310) --- .github/workflows/ci.yml | 2 +- project/MimaSettings.scala | 2 + .../src/test/scala/zio/http/HeaderSpec.scala | 127 +++++++++++++++++- .../src/main/scala/zio/http/Handler.scala | 4 +- .../main/scala/zio/http/HandlerAspect.scala | 3 + .../src/main/scala/zio/http/Header.scala | 11 +- .../src/main/scala/zio/http/Headers.scala | 3 + .../src/main/scala/zio/http/Middleware.scala | 5 +- .../src/main/scala/zio/http/Response.scala | 2 +- .../zio/http/internal/HeaderGetters.scala | 64 ++++++++- .../zio/http/internal/HeaderModifier.scala | 25 +++- .../zio/http/internal/QueryGetters.scala | 29 +--- .../zio/http/internal/StringSchemaCodec.scala | 2 +- .../http/multipart/mixed/MultipartMixed.scala | 2 +- 14 files changed, 237 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4652205bd..5c82a7ebc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -603,7 +603,7 @@ jobs: matrix: os: [ubuntu-latest] scala: [2.13.16] - java: [temurin@8] + java: [zulu@8] runs-on: ${{ matrix.os }} steps: - uses: coursier/setup-action@v1 diff --git a/project/MimaSettings.scala b/project/MimaSettings.scala index b4f0f4e0a9..ae59301b6a 100644 --- a/project/MimaSettings.scala +++ b/project/MimaSettings.scala @@ -23,6 +23,8 @@ object MimaSettings { exclude[Problem]("zio.http.endpoint.openapi.OpenAPIGen#AtomizedMetaCodecs.apply"), exclude[Problem]("zio.http.endpoint.openapi.OpenAPIGen#AtomizedMetaCodecs.this"), exclude[Problem]("zio.http.endpoint.openapi.OpenAPIGen#AtomizedMetaCodecs.copy"), + exclude[IncompatibleMethTypeProblem]("zio.http.Middleware.addHeader"), + exclude[IncompatibleMethTypeProblem]("zio.http.HandlerAspect.addHeader") ), mimaFailOnProblem := failOnProblem, ) diff --git a/zio-http/jvm/src/test/scala/zio/http/HeaderSpec.scala b/zio-http/jvm/src/test/scala/zio/http/HeaderSpec.scala index 1ef0606ecf..52bebe1fec 100644 --- a/zio-http/jvm/src/test/scala/zio/http/HeaderSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/HeaderSpec.scala @@ -16,12 +16,22 @@ package zio.http -import zio.NonEmptyChunk +import java.time.Instant +import java.util.UUID + +import zio._ import zio.test.Assertion._ -import zio.test.assert +import zio.test._ + +import zio.schema._ object HeaderSpec extends ZIOHttpSpec { + case class SimpleWrapper(a: String) + implicit val simpleWrapperSchema: Schema[SimpleWrapper] = DeriveSchema.gen[SimpleWrapper] + case class Foo(a: Int, b: SimpleWrapper, c: NonEmptyChunk[String], chunk: Chunk[String]) + implicit val fooSchema: Schema[Foo] = DeriveSchema.gen[Foo] + def spec = suite("Header")( suite("getHeader")( test("should not return header that doesn't exist in list") { @@ -83,6 +93,117 @@ object HeaderSpec extends ZIOHttpSpec { assert(actual)(isFalse) }, ), + suite("add typed")( + test("primitives") { + val uuid = "123e4567-e89b-12d3-a456-426614174000" + assertTrue( + Headers.empty.addHeader("a", 1).rawHeader("a").get == "1", + Headers.empty.addHeader("a", 1.0d).rawHeader("a").get == "1.0", + Headers.empty.addHeader("a", 1.0f).rawHeader("a").get == "1.0", + Headers.empty.addHeader("a", 1L).rawHeader("a").get == "1", + Headers.empty.addHeader("a", 1.toShort).rawHeader("a").get == "1", + Headers.empty.addHeader("a", true).rawHeader("a").get == "true", + Headers.empty.addHeader("a", 'a').rawHeader("a").get == "a", + Headers.empty.addHeader("a", Instant.EPOCH).rawHeader("a").get == "1970-01-01T00:00:00Z", + Headers.empty + .addHeader("a", UUID.fromString(uuid)) + .rawHeader("a") + .get == uuid, + ) + + }, + test("collections") { + assertTrue( + // Chunk + Headers.empty.addHeader("a", Chunk.empty[Int]).rawHeader("a").isEmpty, + Headers.empty.addHeader("a", Chunk(1)).rawHeaders("a") == Chunk("1"), + Headers.empty.addHeader("a", Chunk(1, 2)).rawHeaders("a") == Chunk("1", "2"), + Headers.empty.addHeader("a", Chunk(1.0, 2.0)).rawHeaders("a") == Chunk("1.0", "2.0"), + // List + Headers.empty.addHeader("a", List.empty[Int]).rawHeader("a").isEmpty, + Headers.empty.addHeader("a", List(1)).rawHeaders("a") == Chunk("1"), + // NonEmptyChunk + Headers.empty.addHeader("a", NonEmptyChunk(1)).rawHeaders("a") == Chunk("1"), + Headers.empty.addHeader("a", NonEmptyChunk(1, 2)).rawHeaders("a") == Chunk("1", "2"), + ) + }, + test("case class") { + val foo = Foo(1, SimpleWrapper("foo"), NonEmptyChunk("1", "2"), Chunk("foo", "bar")) + val fooEmpty = Foo(0, SimpleWrapper(""), NonEmptyChunk("1"), Chunk.empty) + assertTrue( + Headers.empty.addHeader(foo).rawHeader("a").get == "1", + Headers.empty.addHeader(foo).rawHeader("b").get == "foo", + Headers.empty.addHeader(foo).rawHeaders("c") == Chunk("1", "2"), + Headers.empty.addHeader(foo).rawHeaders("chunk") == Chunk("foo", "bar"), + Headers.empty.addHeader(fooEmpty).rawHeader("a").get == "0", + Headers.empty.addHeader(fooEmpty).rawHeader("b").get == "", + Headers.empty.addHeader(fooEmpty).rawHeaders("c") == Chunk("1"), + Headers.empty.addHeader(fooEmpty).rawHeaders("chunk").isEmpty, + ) + }, + ), + suite("schema based getters")( + test("pure") { + val typed = "typed" + val default = 3 + val invalidTyped = "invalidTyped" + val unknown = "non-existent" + val headers = Headers(typed -> "1", typed -> "2", "invalid-typed" -> "str") + val single = Headers(typed -> "1") + val headersFoo = Headers("a" -> "1", "b" -> "foo", "c" -> "2", "chunk" -> "foo", "chunk" -> "bar") + assertTrue( + single.header[Int](typed) == Right(1), + headers.header[Int](invalidTyped).isLeft, + headers.header[Int](unknown).isLeft, + single.headerOrElse[Int](typed, default) == 1, + headers.headerOrElse[Int](invalidTyped, default) == default, + headers.headerOrElse[Int](unknown, default) == default, + headers.header[Chunk[Int]](typed) == Right(Chunk(1, 2)), + headers.header[Chunk[Int]](invalidTyped).isLeft, + headers.header[Chunk[Int]](unknown) == Right(Chunk.empty), + headers.header[NonEmptyChunk[Int]](unknown).isLeft, + headers.headerOrElse[Chunk[Int]](typed, Chunk(default)) == Chunk(1, 2), + headers.headerOrElse[Chunk[Int]](invalidTyped, Chunk(default)) == Chunk(default), + headers.headerOrElse[Chunk[Int]](unknown, Chunk(default)) == Chunk.empty, + headers.headerOrElse[NonEmptyChunk[Int]](unknown, NonEmptyChunk(default)) == NonEmptyChunk(default), + // case class + headersFoo.header[Foo] == Right(Foo(1, SimpleWrapper("foo"), NonEmptyChunk("2"), Chunk("foo", "bar"))), + headersFoo.header[SimpleWrapper] == Right(SimpleWrapper("1")), + headersFoo.header[SimpleWrapper]("b") == Right(SimpleWrapper("foo")), + headers.header[Foo].isLeft, + headersFoo.headerOrElse[Foo](Foo(0, SimpleWrapper(""), NonEmptyChunk("1"), Chunk.empty)) == Foo( + 1, + SimpleWrapper("foo"), + NonEmptyChunk("2"), + Chunk("foo", "bar"), + ), + headers.headerOrElse[Foo](Foo(0, SimpleWrapper(""), NonEmptyChunk("1"), Chunk.empty)) == Foo( + 0, + SimpleWrapper(""), + NonEmptyChunk("1"), + Chunk.empty, + ), + ) + }, + test("as ZIO") { + val typed = "typed" + val invalidTyped = "invalidTyped" + val unknown = "non-existent" + val headers = Headers(typed -> "1", typed -> "2", "invalid-typed" -> "str") + val single = Headers(typed -> "1") + assertZIO(single.headerZIO[Int](typed))(equalTo(1)) && + assertZIO(single.headerZIO[Int](unknown).exit)(fails(anything)) && + assertZIO(single.headerZIO[Chunk[Int]](typed))(hasSize(equalTo(1))) && + assertZIO(single.headerZIO[Chunk[Int]](unknown).exit)(succeeds(equalTo(Chunk.empty[Int]))) && + assertZIO(single.headerZIO[NonEmptyChunk[Int]](unknown).exit)(fails(anything)) && + assertZIO(headers.headerZIO[Int](invalidTyped).exit)(fails(anything)) && + assertZIO(headers.headerZIO[Int](unknown).exit)(fails(anything)) && + assertZIO(headers.headerZIO[Chunk[Int]](typed))(hasSize(equalTo(2))) && + assertZIO(headers.headerZIO[Chunk[Int]](invalidTyped).exit)(fails(anything)) && + assertZIO(headers.headerZIO[Chunk[Int]](unknown).exit)(succeeds(equalTo(Chunk.empty[Int]))) && + assertZIO(headers.headerZIO[NonEmptyChunk[Int]](unknown).exit)(fails(anything)) + }, + ), suite("cookie")( test("should be able to extract more than one header with the same name") { val firstCookie = Cookie.Response("first", "value") @@ -97,7 +218,7 @@ object HeaderSpec extends ZIOHttpSpec { ) }, test("should return an empty sequence if no headers in the response") { - val headers = Headers() + val headers = Headers.empty assert(headers.getAll(Header.SetCookie))(hasSameElements(Seq.empty)) }, ), diff --git a/zio-http/shared/src/main/scala/zio/http/Handler.scala b/zio-http/shared/src/main/scala/zio/http/Handler.scala index e99f602c7e..25632184be 100644 --- a/zio-http/shared/src/main/scala/zio/http/Handler.scala +++ b/zio-http/shared/src/main/scala/zio/http/Handler.scala @@ -29,7 +29,7 @@ import zio.stream.ZStream import zio.http.Handler.ApplyContextAspect import zio.http.Header.HeaderType -import zio.http.internal.HeaderModifier +import zio.http.internal.{HeaderGetters, HeaderModifier} import zio.http.template._ sealed trait Handler[-R, +Err, -In, +Out] { self => @@ -1139,7 +1139,7 @@ object Handler extends HandlerPlatformSpecific with HandlerVersionSpecific { * Updates the current Headers with new one, using the provided update * function passed. */ - override def updateHeaders(update: Headers => Headers)(implicit trace: Trace): RequestHandler[R, Err] = + def updateHeaders(update: Headers => Headers)(implicit trace: Trace): RequestHandler[R, Err] = self.map(_.updateHeaders(update)) } diff --git a/zio-http/shared/src/main/scala/zio/http/HandlerAspect.scala b/zio-http/shared/src/main/scala/zio/http/HandlerAspect.scala index 7f28352559..a194d23d99 100644 --- a/zio-http/shared/src/main/scala/zio/http/HandlerAspect.scala +++ b/zio-http/shared/src/main/scala/zio/http/HandlerAspect.scala @@ -184,6 +184,9 @@ final case class HandlerAspect[-Env, +CtxOut]( } object HandlerAspect extends HandlerAspects { + final protected override def addHeader(name: CharSequence, value: CharSequence): HandlerAspect[Any, Unit] = + HandlerAspect.addHeader[String](name.toString, value.toString) + final class InterceptPatch[State](val fromRequest: Request => State) extends AnyVal { def apply(result: (Response, State) => Response.Patch): HandlerAspect[Any, Unit] = HandlerAspect.interceptHandlerStateful( diff --git a/zio-http/shared/src/main/scala/zio/http/Header.scala b/zio-http/shared/src/main/scala/zio/http/Header.scala index cc7e24834e..1b0a09b6ab 100644 --- a/zio-http/shared/src/main/scala/zio/http/Header.scala +++ b/zio-http/shared/src/main/scala/zio/http/Header.scala @@ -198,7 +198,7 @@ object Header { override def headerType: HeaderType.Typed[Custom] = new Header.HeaderType { override type HeaderValue = Custom - override def name: String = self.customName.toString + override def name: String = self.customName.toString.toLowerCase override def parse(value: String): Either[String, HeaderValue] = Right(Custom(self.customName, value)) @@ -228,14 +228,17 @@ object Header { override def equals(that: Any): Boolean = { that match { case Custom(k, v) => - def eqs(l: CharSequence, r: CharSequence): Boolean = { + def eqs(l: CharSequence, r: CharSequence, caseSensitive: Boolean): Boolean = { if (l.length() != r.length()) false else { var i = 0 var equal = true while (i < l.length()) { - if (l.charAt(i) != r.charAt(i)) { + if ( + (caseSensitive && l.charAt(i) != r + .charAt(i)) || (!caseSensitive && l.charAt(i).toLower != r.charAt(i).toLower) + ) { equal = false i = l.length() } @@ -245,7 +248,7 @@ object Header { } } - eqs(self.customName, k) && eqs(self.value, v) + eqs(self.customName, k, caseSensitive = false) && eqs(self.value, v, caseSensitive = true) case _ => false } diff --git a/zio-http/shared/src/main/scala/zio/http/Headers.scala b/zio-http/shared/src/main/scala/zio/http/Headers.scala index 08c24ea016..af2ef147a1 100644 --- a/zio-http/shared/src/main/scala/zio/http/Headers.scala +++ b/zio-http/shared/src/main/scala/zio/http/Headers.scala @@ -141,6 +141,9 @@ object Headers { def apply(tuple2: (CharSequence, CharSequence)): Headers = apply(tuple2._1, tuple2._2) + def apply(value: (CharSequence, CharSequence), values: (CharSequence, CharSequence)*): Headers = + Headers.FromIterable((value +: values).map { case (k, v) => Header.Custom(k, v) }) + def apply(headers: Header*): Headers = FromIterable(headers) def apply(iter: Iterable[Header]): Headers = FromIterable(iter) diff --git a/zio-http/shared/src/main/scala/zio/http/Middleware.scala b/zio-http/shared/src/main/scala/zio/http/Middleware.scala index 9898968c23..76d7768df7 100644 --- a/zio-http/shared/src/main/scala/zio/http/Middleware.scala +++ b/zio-http/shared/src/main/scala/zio/http/Middleware.scala @@ -49,6 +49,9 @@ trait Middleware[-UpperEnv] { self => @nowarn("msg=shadows type") object Middleware extends HandlerAspects { + final protected override def addHeader(name: CharSequence, value: CharSequence): HandlerAspect[Any, Unit] = + HandlerAspect.addHeader[String](name.toString, value.toString) + /** * Configuration for the CORS aspect. */ @@ -191,7 +194,7 @@ object Middleware extends HandlerAspects { routes.transform[Env1] { h => handler { (req: Request) => if (req.headers.contains(headerName)) h(req) - else h(req.addHeader(headerName, make)) + else h(req.addHeader[String](headerName, make)) } } } diff --git a/zio-http/shared/src/main/scala/zio/http/Response.scala b/zio-http/shared/src/main/scala/zio/http/Response.scala index 5793bba2ad..285d3fed6b 100644 --- a/zio-http/shared/src/main/scala/zio/http/Response.scala +++ b/zio-http/shared/src/main/scala/zio/http/Response.scala @@ -61,7 +61,7 @@ final case class Response( } def contentType(mediaType: MediaType): Response = - self.addHeader("content-type", mediaType.fullType) + self.addHeader[String]("content-type", mediaType.fullType) /** * Consumes the streaming body fully and then discards it while also ignoring diff --git a/zio-http/shared/src/main/scala/zio/http/internal/HeaderGetters.scala b/zio-http/shared/src/main/scala/zio/http/internal/HeaderGetters.scala index 2fb9b2c441..4187a065e2 100644 --- a/zio-http/shared/src/main/scala/zio/http/internal/HeaderGetters.scala +++ b/zio-http/shared/src/main/scala/zio/http/internal/HeaderGetters.scala @@ -16,10 +16,13 @@ package zio.http.internal -import zio.Chunk +import zio._ + +import zio.schema.Schema import zio.http.Header.HeaderType import zio.http.Headers +import zio.http.codec.HttpCodecError /** * Maintains a list of operators that parse and extract data from the headers. @@ -39,6 +42,65 @@ trait HeaderGetters { self => parsed.toOption } + /** + * Retrieves the header with the specified name as a value of the specified + * type. The type must have a schema and can be a primitive type (e.g. Int, + * String, UUID, Instant etc.), a case class with a single field or a + * collection of either of these. + */ + final def header[T](name: String)(implicit schema: Schema[T]): Either[HttpCodecError.HeaderError, T] = + try + Right( + StringSchemaCodec + .headerFromSchema(schema, ErrorConstructor.header, name) + .decode(headers), + ) + catch { + case e: HttpCodecError.HeaderError => Left(e) + } + + /** + * Retrieves headers as a value of the specified type. The type must have a + * schema and be a case class and all fields must be headers. So fields must + * be of primitive types (e.g. Int, String, UUID, Instant etc.), a case class + * with a single field or a collection of either of these. Headers are + * selected by field names. + */ + final def header[T](implicit schema: Schema[T]): Either[HttpCodecError.HeaderError, T] = + try + Right( + StringSchemaCodec + .headerFromSchema(schema, ErrorConstructor.header, null) + .decode(headers), + ) + catch { + case e: HttpCodecError.HeaderError => Left(e) + } + + /** + * Retrieves the header with the specified name as a value of the specified + * type T, or returns a default value if the header is not present or could + * not be parsed. The type T must have a schema and can be a primitive type + * (e.g. Int, String, UUID, Instant etc.), a case class with a single field or + * a collection of either of these. + */ + final def headerOrElse[T](name: String, default: => T)(implicit schema: Schema[T]): T = + header[T](name).getOrElse(default) + + /** + * Retrieves headers as a value of the specified type T, or returns a default + * value if the headers are not present or could not be parsed. The type T + * must have a schema and be a case class and all fields must be headers. So + * fields must be of primitive types (e.g. Int, String, UUID, Instant etc.), a + * case class with a single field or a collection of either of these. Headers + * are selected by field names. + */ + final def headerOrElse[T](default: => T)(implicit schema: Schema[T]): T = + header[T].getOrElse(default) + + final def headerZIO[T](name: String)(implicit schema: Schema[T]): IO[HttpCodecError.HeaderError, T] = + ZIO.fromEither(header[T](name)) + final def headers(headerType: HeaderType): Chunk[headerType.HeaderValue] = Chunk.fromIterator( headers.iterator diff --git a/zio-http/shared/src/main/scala/zio/http/internal/HeaderModifier.scala b/zio-http/shared/src/main/scala/zio/http/internal/HeaderModifier.scala index 629f497f01..58a73d8c14 100644 --- a/zio-http/shared/src/main/scala/zio/http/internal/HeaderModifier.scala +++ b/zio-http/shared/src/main/scala/zio/http/internal/HeaderModifier.scala @@ -18,6 +18,8 @@ package zio.http.internal import zio.Trace +import zio.schema.Schema + import zio.http.Header.HeaderType import zio.http._ @@ -34,7 +36,7 @@ trait HeaderModifier[+A] { self => final def addHeader(header: Header): A = addHeaders(Headers(header)) - final def addHeader(name: CharSequence, value: CharSequence): A = + protected def addHeader(name: CharSequence, value: CharSequence): A = addHeaders(Headers.apply(name, value)) final def addHeaders(headers: Headers): A = updateHeaders(_ ++ headers) @@ -42,12 +44,31 @@ trait HeaderModifier[+A] { self => final def addHeaders(headers: Iterable[(CharSequence, CharSequence)]): A = addHeaders(Headers.fromIterable(headers.map { case (k, v) => Header.Custom(k, v) })) + /** + * Adds a header / headers with the specified name and based on the given + * value. The value type must have a schema and can be a primitive type (e.g. + * Int, String, UUID, Instant etc.), a case class with a single field or a + * collection of either of these. + */ + final def addHeader[T](name: String, value: T)(implicit schema: Schema[T]): A = + updateHeaders(StringSchemaCodec.headerFromSchema(schema, ErrorConstructor.header, name).encode(value, _)) + + /** + * Adds headers based on the given value. The type of the value must have a + * schema and be a case class and all fields will be added as headers. So + * fields must be of primitive types (e.g. Int, String, UUID, Instant etc.), a + * case class with a single field or a collection of either of these. The + * header names are the field names. + */ + final def addHeader[T](value: T)(implicit schema: Schema[T]): A = + updateHeaders(StringSchemaCodec.headerFromSchema(schema, ErrorConstructor.header, null).encode(value, _)) + final def removeHeader(headerType: HeaderType): A = removeHeader(headerType.name) final def removeHeader(name: String): A = removeHeaders(Set(name)) final def removeHeaders(headers: Set[String]): A = - updateHeaders(orig => Headers(orig.filterNot(h => headers.contains(h.headerName)))) + updateHeaders(orig => Headers(orig.filterNot(h => headers.exists(h.headerName.equalsIgnoreCase)))) final def setHeaders(headers: Headers): A = self.updateHeaders(_ => headers) diff --git a/zio-http/shared/src/main/scala/zio/http/internal/QueryGetters.scala b/zio-http/shared/src/main/scala/zio/http/internal/QueryGetters.scala index fc8cd0096b..6ce01c2e32 100644 --- a/zio-http/shared/src/main/scala/zio/http/internal/QueryGetters.scala +++ b/zio-http/shared/src/main/scala/zio/http/internal/QueryGetters.scala @@ -27,23 +27,6 @@ import zio.http.codec.{HttpCodecError, TextCodec} trait QueryGetters[+A] { self: QueryOps[A] => - private val errorConstructor = new ErrorConstructor { - override def missing(fieldName: String): HttpCodecError = - HttpCodecError.MissingQueryParam(fieldName) - - override def missingAll(fieldNames: Chunk[String]): HttpCodecError = - HttpCodecError.MissingQueryParams(fieldNames) - - override def invalid(errors: Chunk[ValidationError]): HttpCodecError = - HttpCodecError.InvalidEntity.wrap(errors) - - override def malformed(fieldName: String, error: DecodeError): HttpCodecError = - HttpCodecError.MalformedQueryParam(fieldName, error) - - override def invalidCount(fieldName: String, expected: Int, actual: Int): HttpCodecError = - HttpCodecError.InvalidQueryParamCount(fieldName, expected, actual) - } - def queryParameters: QueryParams /** @@ -84,11 +67,7 @@ trait QueryGetters[+A] { self: QueryOps[A] => try Right( StringSchemaCodec - .queryFromSchema( - schema, - errorConstructor, - key, - ) + .queryFromSchema(schema, ErrorConstructor.query, key) .decode(queryParameters), ) catch { @@ -106,11 +85,7 @@ trait QueryGetters[+A] { self: QueryOps[A] => try Right( StringSchemaCodec - .queryFromSchema( - schema, - errorConstructor, - null, - ) + .queryFromSchema(schema, ErrorConstructor.query, null) .decode(queryParameters), ) catch { diff --git a/zio-http/shared/src/main/scala/zio/http/internal/StringSchemaCodec.scala b/zio-http/shared/src/main/scala/zio/http/internal/StringSchemaCodec.scala index 8d43c2c496..5d73999acc 100644 --- a/zio-http/shared/src/main/scala/zio/http/internal/StringSchemaCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/internal/StringSchemaCodec.scala @@ -418,7 +418,7 @@ private[http] object StringSchemaCodec { override def schema: Schema[A] = schema1.asInstanceOf[Schema[A]] override private[http] def add(headers: Headers, key: String, value: String): Headers = - headers.addHeader(key, value) + headers.addHeaders(Headers.apply(key, value)) override private[http] def addAll(headers: Headers, kvs: Iterable[(String, String)]): Headers = headers.addHeaders(kvs) diff --git a/zio-http/shared/src/main/scala/zio/http/multipart/mixed/MultipartMixed.scala b/zio-http/shared/src/main/scala/zio/http/multipart/mixed/MultipartMixed.scala index 32f7a65a42..a1ef96263a 100644 --- a/zio-http/shared/src/main/scala/zio/http/multipart/mixed/MultipartMixed.scala +++ b/zio-http/shared/src/main/scala/zio/http/multipart/mixed/MultipartMixed.scala @@ -129,7 +129,7 @@ object MultipartMixed { FormAST.Header .fromBytes(h.toArray, StandardCharsets.UTF_8) .map { case FormAST.Header(name, value) => - parseHeaders(rest, res.addHeader(name, value)) + parseHeaders(rest, res.addHeader[String](name, value)) } .getOrElse(parseHeaders(rest, res)) }