From c948d8958041c9a8141f72194ab6c4b2f092d42d Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Wed, 15 Jan 2025 06:50:59 +0100 Subject: [PATCH] Use empty body for `Unit` payload in Endpoint (#3258) --- .../zio/http/endpoint/RoundtripSpec.scala | 5 +++ .../zio/http/codec/HttpContentCodec.scala | 2 ++ .../zio/http/codec/internal/BodyCodec.scala | 34 +++++++++++++------ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala b/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala index 3b6fa237f..d02e01565 100644 --- a/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/endpoint/RoundtripSpec.scala @@ -176,6 +176,11 @@ object RoundtripSpec extends ZIOHttpSpec { Post(20, "title", "body", 10), ) }, + test("simple get without payload") { + val healthCheckAPI = Endpoint(GET / "health-check").out[Unit] + val healthCheckHandler = healthCheckAPI.implementAs(()) + testEndpoint(healthCheckAPI, Routes(healthCheckHandler), (), ()) + }, test("simple get with query params from case class") { val endpoint = Endpoint(GET / "query") .query(HttpCodec.queryAll[Params]) diff --git a/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala index e846225e5..2661bfd30 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/HttpContentCodec.scala @@ -78,6 +78,8 @@ sealed trait HttpContentCodec[A] { self => } } + private[http] val isUnitCodec: Boolean = choices.values.forall(_.schema == Schema[Unit]) + def only(mediaType: MediaType): HttpContentCodec[A] = if (lookup(mediaType).isEmpty) { throw new IllegalArgumentException(s"MediaType $mediaType is not supported by $self") diff --git a/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala b/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala index feb9f64ee..2de9d6ee3 100644 --- a/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala +++ b/zio-http/shared/src/main/scala/zio/http/codec/internal/BodyCodec.scala @@ -117,9 +117,10 @@ private[http] object BodyCodec { .lookup(field.contentType) .toRight(HttpCodecError.CustomError("UnsupportedMediaType", s"MediaType: ${field.contentType}")) codec0 match { - case Left(error) => ZIO.fail(error) + case Left(error) => + Exit.fail(error) case Right((_, BinaryCodecWithSchema(_, schema))) if schema == Schema[Unit] => - ZIO.unit.asInstanceOf[IO[Throwable, A]] + Exit.unit.asInstanceOf[IO[Throwable, A]] case Right((_, bc @ BinaryCodecWithSchema(_, schema))) => field.asChunk.flatMap { chunk => ZIO.fromEither(bc.codec(config).decode(chunk)) }.flatMap(validateZIO(schema)) } @@ -128,9 +129,11 @@ private[http] object BodyCodec { override def decodeFromBody(body: Body, config: CodecConfig)(implicit trace: Trace): IO[Throwable, A] = { val codec0 = codecForBody(codec, body) codec0 match { - case Left(error) => ZIO.fail(error) + case Left(error) => + Exit.fail(error) case Right((_, BinaryCodecWithSchema(_, schema))) if schema == Schema[Unit] => - ZIO.unit.asInstanceOf[IO[Throwable, A]] + if (body.isEmpty) Exit.unit.asInstanceOf[IO[Throwable, A]] + else ZIO.fail(HttpCodecError.CustomError("InvalidBody", "Non-empty body cannot be decoded as Unit")) case Right((_, bc @ BinaryCodecWithSchema(_, schema))) => body.asChunk.flatMap { chunk => ZIO.fromEither(bc.codec(config).decode(chunk)) }.flatMap(validateZIO(schema)) } @@ -139,7 +142,9 @@ private[http] object BodyCodec { def encodeToField(value: A, mediaTypes: Chunk[MediaTypeWithQFactor], name: String, config: CodecConfig)(implicit trace: Trace, ): FormField = { - val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 if (mediaType.binary) { FormField.binaryField( name, @@ -158,8 +163,11 @@ private[http] object BodyCodec { def encodeToBody(value: A, mediaTypes: Chunk[MediaTypeWithQFactor], config: CodecConfig)(implicit trace: Trace, ): Body = { - val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes) - Body.fromChunk(bc.codec(config).encode(value), mediaType) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 + if (bc.schema == Schema[Unit]) Body.empty.contentType(mediaType) + else Body.fromChunk(bc.codec(config).encode(value), mediaType) } type Element = A @@ -200,7 +208,9 @@ private[http] object BodyCodec { )(implicit trace: Trace, ): FormField = { - val (mediaType, bc) = codec.chooseFirstOrDefault(mediaTypes) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 FormField.streamingBinaryField( name, value >>> bc.codec(config).streamEncoder, @@ -215,7 +225,9 @@ private[http] object BodyCodec { )(implicit trace: Trace, ): Body = { - val (mediaType, bc @ BinaryCodecWithSchema(_, _)) = codec.chooseFirstOrDefault(mediaTypes) + val selected = codec.chooseFirstOrDefault(mediaTypes) + val mediaType = selected._1 + val bc = selected._2 Body.fromStreamChunked(value >>> bc.codec(config).streamEncoder).contentType(mediaType) } @@ -232,8 +244,8 @@ private[http] object BodyCodec { private[internal] def validateZIO[A](schema: Schema[A])(e: A)(implicit trace: Trace): ZIO[Any, HttpCodecError, A] = { val errors = Schema.validate(e)(schema) - if (errors.isEmpty) ZIO.succeed(e) - else ZIO.fail(HttpCodecError.InvalidEntity.wrap(errors)) + if (errors.isEmpty) Exit.succeed(e) + else Exit.fail(HttpCodecError.InvalidEntity.wrap(errors)) } private[internal] def validateStream[E](schema: Schema[E])(implicit