From ae467ddf7d23fb050f1e1b9da76cad71676cfaad Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Sun, 15 Dec 2024 03:15:10 +1100 Subject: [PATCH 1/5] Optimise `zio.http.endpoint.openapi.JsonSchema.fromZSchemaMulti` (#3239) Replace `.map(..).getOrElse(..)` with `.fold` Co-authored-by: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> --- .../src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala index d2063c2bc..0dc9d674c 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala @@ -425,7 +425,7 @@ object JsonSchema { refType, seenWithCurrent, ) - nested.rootRef.map(k => nested.children + (k -> nested.root)).getOrElse(nested.children) + nested.rootRef.fold(ifEmpty = nested.children)(k => nested.children + (k -> nested.root)) } .toMap JsonSchemas(fromZSchema(record, SchemaStyle.Inline), ref, children) From 265f503c8794dc7f6e7bbead46297b9c5ba3f9eb Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Sun, 15 Dec 2024 04:56:49 +1100 Subject: [PATCH 2/5] Further optimise `zio.http.Body.FileBody.asStream` code (#3234) Following https://github.com/zio/zio-http/pull/3215 Co-authored-by: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> --- .../shared/src/main/scala/zio/http/Body.scala | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/zio-http/shared/src/main/scala/zio/http/Body.scala b/zio-http/shared/src/main/scala/zio/http/Body.scala index 00ba269aa..0ccfe858d 100644 --- a/zio-http/shared/src/main/scala/zio/http/Body.scala +++ b/zio-http/shared/src/main/scala/zio/http/Body.scala @@ -462,7 +462,8 @@ object Body { override def asChunk(implicit trace: Trace): Task[Chunk[Byte]] = zioEmptyChunk override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.empty - override def isComplete: Boolean = true + + override def isComplete: Boolean = true override def isEmpty: Boolean = true @@ -471,7 +472,8 @@ object Body { override private[zio] def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = Array.empty[Byte] override def contentType(newContentType: Body.ContentType): Body = this - override def contentType: Option[Body.ContentType] = None + + override def contentType: Option[Body.ContentType] = None override def knownContentLength: Option[Long] = Some(0L) } @@ -568,27 +570,35 @@ object Body { override def asStream(implicit trace: Trace): ZStream[Any, Throwable, Byte] = ZStream.unwrap { ZIO.blocking { - for { - r <- ZIO.attempt { + ZIO.suspendSucceed { + try { val fs = new FileInputStream(file) val size = Math.min(chunkSize.toLong, file.length()).toInt - (fs, size) - } - (fs, size) = r - } yield ZStream - .repeatZIOOption[Any, Throwable, Chunk[Byte]] { - for { - buffer <- ZIO.succeed(new Array[Byte](size)) - len <- ZIO.attempt(fs.read(buffer)).mapError(Some(_)) - bytes <- - if (len > 0) ZIO.succeed(Chunk.fromArray(buffer.slice(0, len))) - else ZIO.fail(None) - } yield bytes + val read: Task[Option[Chunk[Byte]]] = + ZIO.suspendSucceed { + try { + val buffer = new Array[Byte](size) + val len = fs.read(buffer) + if (len > 0) Exit.succeed(Some(Chunk.fromArray(buffer.slice(0, len)))) + else Exit.none + } catch { + case e: Throwable => Exit.fail(e) + } + } + + Exit.succeed { + // Optimised for our needs version of `ZIO.repeatZIOChunkOption` + ZStream + .unfoldChunkZIO(read)(_.map(_.map(_ -> read))) + .ensuring(ZIO.attempt(fs.close()).ignoreLogged) + } + } catch { + case e: Throwable => Exit.fail(e) } - .ensuring(ZIO.attempt(fs.close()).ignoreLogged) + } } - }.flattenChunks + } override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType)) From 869a2303728ea90dcb0320d39ed14982cc55ad0d Mon Sep 17 00:00:00 2001 From: Jakub Czuchnowski Date: Sat, 14 Dec 2024 18:57:53 +0100 Subject: [PATCH 3/5] Handler factory method for functions returning Either (#3230) Add Handler factory method for functions returning Either Co-authored-by: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> --- .../src/main/scala/zio/http/Handler.scala | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 2bbdae003..e99f602c7 100644 --- a/zio-http/shared/src/main/scala/zio/http/Handler.scala +++ b/zio-http/shared/src/main/scala/zio/http/Handler.scala @@ -853,6 +853,11 @@ object Handler extends HandlerPlatformSpecific with HandlerVersionSpecific { def fromFunctionHandler[In]: FromFunctionHandler[In] = new FromFunctionHandler[In](()) + /** + * Creates a Handler from an pure function from A to Either[E,B] + */ + def fromFunctionEither[In]: FromFunctionEither[In] = new FromFunctionEither[In](()) + /** * Creates a Handler from an pure function from A to HExit[R,E,B] */ @@ -1222,6 +1227,18 @@ object Handler extends HandlerPlatformSpecific with HandlerVersionSpecific { } } + final class FromFunctionEither[In](val self: Unit) extends AnyVal { + def apply[R, Err, Out](f: In => Either[Err, Out]): Handler[Any, Err, In, Out] = + new Handler[Any, Err, In, Out] { + override def apply(in: In): ZIO[Any, Err, Out] = + try { + Exit.fromEither(f(in)) + } catch { + case error: Throwable => Exit.die(error) + } + } + } + final class FromFunctionExit[In](val self: Unit) extends AnyVal { def apply[R, Err, Out](f: In => Exit[Err, Out]): Handler[Any, Err, In, Out] = new Handler[Any, Err, In, Out] { From 7db7f7fe556aabe4f4c3bca23c912a75f4a4ebc4 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Sun, 15 Dec 2024 04:59:37 +1100 Subject: [PATCH 4/5] Optimise `zio.http.endpoint.Endpoint.implementHandler` (#3236) Replace `.map(..).catchAll(..)` with `.foldZIO` Co-authored-by: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> --- .../shared/src/main/scala/zio/http/endpoint/Endpoint.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala index 7d3f09426..b1b6f6c6f 100644 --- a/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala +++ b/zio-http/shared/src/main/scala/zio/http/endpoint/Endpoint.scala @@ -303,9 +303,10 @@ final case class Endpoint[PathInput, Input, Err, Output, Auth <: AuthType]( ) .getOrElse(defaultMediaTypes) (endpoint.input ++ authCodec(endpoint.authType)).decodeRequest(request, config).orDie.flatMap { value => - original(value).map(endpoint.output.encodeResponse(_, outputMediaTypes, config)).catchAll { error => - ZIO.succeed(endpoint.error.encodeResponse(error, outputMediaTypes, config)) - } + original(value).foldZIO( + success = output => Exit.succeed(endpoint.output.encodeResponse(output, outputMediaTypes, config)), + failure = error => Exit.succeed(endpoint.error.encodeResponse(error, outputMediaTypes, config)), + ) } } -> condition } From 1519ce67883ac8adf9822b0e4adbc7552b3632ec Mon Sep 17 00:00:00 2001 From: Vamshi Maskuri <117595548+varshith257@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:31:54 +0530 Subject: [PATCH 5/5] Fix: Mimetype application/x-zip-compressed interpreted as a text field (#3203) * Fix: Treat unrecognized MIME types as binary in MediaType * Fix: Ensure wildcard MIME types in Accept header default to non-binary * Update unsafeParseCustomMediaType to set binary for unrecognized MIME types --------- Co-authored-by: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> --- .../jvm/src/test/scala/zio/http/MediaTypeSpec.scala | 10 +++++++++- .../shared/src/main/scala/zio/http/MediaType.scala | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/zio-http/jvm/src/test/scala/zio/http/MediaTypeSpec.scala b/zio-http/jvm/src/test/scala/zio/http/MediaTypeSpec.scala index fd0780fa2..ce43b84d4 100644 --- a/zio-http/jvm/src/test/scala/zio/http/MediaTypeSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/MediaTypeSpec.scala @@ -37,7 +37,7 @@ object MediaTypeSpec extends ZIOHttpSpec { } }, test("custom mime type parsing") { - assertTrue(MediaType.parseCustomMediaType("custom/mime").contains(MediaType("custom", "mime"))) + assertTrue(MediaType.parseCustomMediaType("custom/mime").contains(MediaType("custom", "mime", binary = true))) }, test("optional parameter parsing") { assertTrue( @@ -48,5 +48,13 @@ object MediaTypeSpec extends ZIOHttpSpec { ), ) }, + test("application/x-zip-compressed should be binary") { + val mediaType = MediaType.forContentType("application/x-zip-compressed") + assertTrue(mediaType.exists(_.binary)) + }, + test("text/plain should not be binary") { + val mediaType = MediaType.forContentType("text/plain") + assertTrue(mediaType.exists(!_.binary)) + }, ) } diff --git a/zio-http/shared/src/main/scala/zio/http/MediaType.scala b/zio-http/shared/src/main/scala/zio/http/MediaType.scala index 8a38bf9ac..b98c22322 100644 --- a/zio-http/shared/src/main/scala/zio/http/MediaType.scala +++ b/zio-http/shared/src/main/scala/zio/http/MediaType.scala @@ -64,11 +64,14 @@ object MediaType extends MediaTypes { val contentTypeParts = customMediaType.split('/') if (contentTypeParts.length == 2) { val subtypeParts = contentTypeParts(1).split(';') + // Default binary to true for unknown types unless they belong to text families + val isBinary = customMediaType != "*/*" && customMediaType != "text/*" && !customMediaType.startsWith("text/") if (subtypeParts.length >= 1) { Some( MediaType( mainType = contentTypeParts.head, subType = subtypeParts.head, + binary = isBinary, parameters = if (subtypeParts.length >= 2) parseOptionalParameters(subtypeParts.tail) else Map.empty, ), ) @@ -80,10 +83,12 @@ object MediaType extends MediaTypes { val contentTypeParts = customMediaType.split('/') if (contentTypeParts.length == 2) { val subtypeParts = contentTypeParts(1).split(';') + val isBinary = customMediaType != "*/*" && customMediaType != "text/*" && !customMediaType.startsWith("text/") if (subtypeParts.length >= 1) { MediaType( mainType = contentTypeParts.head, subType = subtypeParts.head, + binary = isBinary, parameters = if (subtypeParts.length >= 2) parseOptionalParameters(subtypeParts.tail) else Map.empty, ) } else throw new IllegalArgumentException(s"Invalid media type $customMediaType")