diff --git a/README.md b/README.md index edd2f749f5..5ebf20b4a4 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,29 @@ ZIO HTTP is designed in terms of **HTTP as function**, where both server and cli Some of the key features of ZIO HTTP are: **ZIO Native**: ZIO HTTP is built atop ZIO, a type-safe, composable, and asynchronous effect system for Scala. It inherits all the benefits of ZIO, including testability, composability, and type safety. + **Cloud-Native**: ZIO HTTP is designed for cloud-native environments and supports building highly scalable and performant web applications. Built atop ZIO, it features built-in support for concurrency, parallelism, resource management, error handling, structured logging, configuration management, and metrics instrumentation. + **Imperative and Declarative Endpoints**: ZIO HTTP provides a declarative API for defining HTTP endpoints besides the imperative API. With imperative endpoints, both the shape of the endpoint and the logic are defined together, while with declarative endpoints, the description of the endpoint is separated from its logic. Developers can choose the style that best fit their needs. + **Type-Driven API Design**: Beside the fact that ZIO HTTP supports declarative endpoint descriptions, it also provides a type-driven API design that leverages Scala's type system to ensure correctness and safety at compile time. So the implementation of the endpoint is type-checked against the description of the endpoint. + **Middleware Support**: ZIO HTTP offers middleware support for incorporating cross-cutting concerns such as logging, metrics, authentication, and more into your services. + **Error Handling**: Built-in support exists for handling errors at the HTTP layer, distinguishing between handled and unhandled errors. + **WebSockets**: Built-in support for WebSockets allows for the creation of real-time applications using ZIO HTTP. + **Testkit**: ZIO HTTP provides first-class testing utilities that facilitate test writing without requiring a live server instance. + **Interoperability**: Interoperability with existing Scala/Java libraries is provided, enabling seamless integration with functionality from the Scala/Java ecosystem through the importation of blocking and non-blocking operations. + **JSON and Binary Codecs**: Built-in support for ZIO Schema enables encoding and decoding of request/response bodies, supporting various data types including JSON, Protobuf, Avro, and Thrift. + **Template System**: A built-in DSL facilitates writing HTML templates using Scala code. + **OpenAPI Support**: Built-in support is available for generating OpenAPI documentation for HTTP applications, and conversely, for generating HTTP endpoints from OpenAPI documentation. + **ZIO HTTP CLI**: Command-line applications can be built to interact with HTTP APIs by leveraging the power of [ZIO CLI](https://zio.dev/zio-cli) and ZIO HTTP. ## Installation @@ -91,7 +103,7 @@ object GreetingClient extends ZIOAppDefault { ## Documentation -Learn more on the [ZIO Http homepage](https://github.com/zio/zio-http)! +Learn more on the [ZIO Http Docs](https://zio.dev/zio-http/)! ## Contributing diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/NettyBodyWriter.scala b/zio-http/jvm/src/main/scala/zio/http/netty/NettyBodyWriter.scala index f63c20c044..93acabad5a 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/NettyBodyWriter.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/NettyBodyWriter.scala @@ -40,7 +40,6 @@ object NettyBodyWriter { body: Body, contentLength: Option[Long], ctx: ChannelHandlerContext, - compressionEnabled: Boolean, )(implicit trace: Trace, ): Option[Task[Unit]] = { @@ -57,21 +56,16 @@ object NettyBodyWriter { } body match { - case body: ByteBufBody => + case body: ByteBufBody => ctx.write(new DefaultHttpContent(body.byteBuf)) ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) None - case body: FileBody if compressionEnabled => + case body: FileBody => // We need to stream the file when compression is enabled otherwise the response encoding fails val stream = ZStream.fromFile(body.file) - val size = Some(body.fileSize) - val s = StreamBody(stream, knownContentLength = size, mediaType = body.mediaType) - NettyBodyWriter.writeAndFlush(s, size, ctx, compressionEnabled) - case body: FileBody => - ctx.write(new DefaultFileRegion(body.file, 0, body.fileSize)) - ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) - None - case AsyncBody(async, _, _, _) => + val s = StreamBody(stream, None, mediaType = body.mediaType) + NettyBodyWriter.writeAndFlush(s, None, ctx) + case AsyncBody(async, _, _, _) => async( new UnsafeAsync { override def apply(message: Chunk[Byte], isLast: Boolean): Unit = { @@ -87,10 +81,10 @@ object NettyBodyWriter { }, ) None - case AsciiStringBody(asciiString, _, _) => + case AsciiStringBody(asciiString, _, _) => writeArray(asciiString.array(), isLast = true) None - case StreamBody(stream, _, _, _) => + case StreamBody(stream, _, _, _) => Some( contentLength.orElse(body.knownContentLength) match { case Some(length) => @@ -131,13 +125,13 @@ object NettyBodyWriter { } }, ) - case ArrayBody(data, _, _) => + case ArrayBody(data, _, _) => writeArray(data, isLast = true) None - case ChunkBody(data, _, _) => + case ChunkBody(data, _, _) => writeArray(data.toArray, isLast = true) None - case EmptyBody => + case EmptyBody => ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) None } diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala b/zio-http/jvm/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala index b1021d924e..db8438a4b8 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/client/ClientInboundHandler.scala @@ -52,7 +52,7 @@ final class ClientInboundHandler( ctx.writeAndFlush(fullRequest): Unit case _: HttpRequest => ctx.write(jReq) - NettyBodyWriter.writeAndFlush(req.body, None, ctx, compressionEnabled = false).foreach { effect => + NettyBodyWriter.writeAndFlush(req.body, None, ctx).foreach { effect => rtm.run(ctx, NettyRuntime.noopEnsuring)(effect)(Unsafe.unsafe, trace) } } diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala b/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala index 5298e14c6f..65e425cd68 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/server/ServerInboundHandler.scala @@ -187,7 +187,7 @@ private[zio] final case class ServerInboundHandler( } ctx.writeAndFlush(jResponse) - NettyBodyWriter.writeAndFlush(response.body, contentLength, ctx, isResponseCompressible(jRequest)) + NettyBodyWriter.writeAndFlush(response.body, contentLength, ctx) } else { ctx.writeAndFlush(jResponse) None diff --git a/zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala b/zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala index c9fc132d6e..c9774e8150 100644 --- a/zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala @@ -18,7 +18,7 @@ package zio.http import zio._ import zio.test.Assertion.{equalTo, isNone, isSome} -import zio.test.TestAspect.withLiveClock +import zio.test.TestAspect.{sequential, withLiveClock} import zio.test._ import zio.http.internal.{DynamicServer, HttpRunnableSpec, serverTestLayer} @@ -74,6 +74,8 @@ object ContentTypeSpec extends HttpRunnableSpec { override def spec = { suite("Content-type") { serve.as(List(contentSpec)) - }.provideShared(DynamicServer.live, serverTestLayer, Client.default, Scope.default) @@ withLiveClock + } + .provideSome[DynamicServer & Server & Client](Scope.default) + .provideShared(DynamicServer.live, serverTestLayer, Client.default) @@ withLiveClock @@ sequential } } diff --git a/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala b/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala index 9e1b0117fc..1169919168 100644 --- a/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/FormSpec.scala @@ -271,6 +271,33 @@ object FormSpec extends ZIOHttpSpec { collected.get("file").get.asInstanceOf[FormField.Binary].data == bytes, ) }, + test("StreamingForm dynamically resizes") { + val N = 1000 + val expected = Chunk.fromArray(Array.fill(N)(scala.util.Random.nextInt()).map(_.toByte)) + val form = + Form( + Chunk( + FormField.binaryField( + name = "identifier", + data = Chunk(10.toByte), + mediaType = MediaType.application.`octet-stream`, + ), + FormField.StreamingBinary( + name = "blob", + data = ZStream.fromChunk(expected), + contentType = MediaType.application.`octet-stream`, + ), + ), + ) + val boundary = Boundary("X-INSOMNIA-BOUNDARY") + for { + formBytes <- form.multipartBytes(boundary).runCollect + formByteStream = ZStream.fromChunk(formBytes) + streamingForm = StreamingForm(formByteStream, boundary, 16) + out <- streamingForm.collectAll + res = out.get("blob").get.asInstanceOf[FormField.Binary].data + } yield assertTrue(res == expected) + } @@ timeout(3.seconds), test("decoding random form") { check(Gen.chunkOfBounded(2, 8)(formField)) { fields => for { diff --git a/zio-http/jvm/src/test/scala/zio/http/KeepAliveSpec.scala b/zio-http/jvm/src/test/scala/zio/http/KeepAliveSpec.scala index b8f50f4047..ab825669c5 100644 --- a/zio-http/jvm/src/test/scala/zio/http/KeepAliveSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/KeepAliveSpec.scala @@ -16,7 +16,7 @@ package zio.http -import zio.Scope +import zio._ import zio.test.Assertion.{equalTo, isNone, isSome} import zio.test.TestAspect.{sequential, withLiveClock} import zio.test.{Spec, assert} @@ -66,7 +66,9 @@ object KeepAliveSpec extends HttpRunnableSpec { override def spec: Spec[Any, Throwable] = { suite("KeepAliveSpec") { keepAliveSpec - }.provide(DynamicServer.live, serverTestLayer, Client.default, Scope.default) @@ withLiveClock @@ sequential + } + .provideSome[DynamicServer & Server & Client](Scope.default) + .provide(DynamicServer.live, serverTestLayer, Client.default) @@ withLiveClock @@ sequential } } diff --git a/zio-http/jvm/src/test/scala/zio/http/SSLSpec.scala b/zio-http/jvm/src/test/scala/zio/http/SSLSpec.scala index a75c0234f7..16cb728ef8 100644 --- a/zio-http/jvm/src/test/scala/zio/http/SSLSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/SSLSpec.scala @@ -17,6 +17,7 @@ package zio.http import zio.test.Assertion.equalTo +import zio.test.TestAspect.withLiveClock import zio.test.{Gen, assertCompletes, assertNever, assertZIO} import zio.{Scope, ZLayer} @@ -35,6 +36,7 @@ object SSLSpec extends ZIOHttpSpec { val app: Routes[Any, Response] = Routes( Method.GET / "success" -> handler(Response.ok), + Method.GET / "file" -> Handler.fromResource("TestStatic/TestFile1.txt"), ).sandbox val httpUrl = @@ -43,6 +45,9 @@ object SSLSpec extends ZIOHttpSpec { val httpsUrl = URL.decode("https://localhost:8073/success").toOption.get + val staticFileUrl = + URL.decode("https://localhost:8073/file").toOption.get + override def spec = suite("SSL")( Server .install(app) @@ -110,6 +115,19 @@ object SSLSpec extends ZIOHttpSpec { ZLayer.succeed(NettyConfig.defaultWithFastShutdown), Scope.default, ), + test("static files") { + val actual = Client + .request(Request.get(staticFileUrl)) + .flatMap(_.body.asString) + assertZIO(actual)(equalTo("This file is added for testing Static File Server.")) + }.provide( + Client.customized, + ZLayer.succeed(ZClient.Config.default.ssl(ClientSSLConfig.Default)), + NettyClientDriver.live, + DnsResolver.default, + ZLayer.succeed(NettyConfig.defaultWithFastShutdown), + Scope.default, + ) @@ withLiveClock, ), ), ).provideShared( diff --git a/zio-http/jvm/src/test/scala/zio/http/StaticFileServerSpec.scala b/zio-http/jvm/src/test/scala/zio/http/StaticFileServerSpec.scala index 89807edf02..1b9c8ade6f 100644 --- a/zio-http/jvm/src/test/scala/zio/http/StaticFileServerSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/StaticFileServerSpec.scala @@ -20,7 +20,7 @@ import java.io.File import zio._ import zio.test.Assertion._ -import zio.test.TestAspect.{unix, withLiveClock} +import zio.test.TestAspect.{sequential, unix, withLiveClock} import zio.test.assertZIO import zio.http.internal.{DynamicServer, HttpRunnableSpec, serverTestLayer} @@ -46,7 +46,9 @@ object StaticFileServerSpec extends HttpRunnableSpec { override def spec = suite("StaticFileServerSpec") { serve.as(List(staticSpec)) - }.provideShared(DynamicServer.live, serverTestLayer, Client.default, Scope.default) @@ withLiveClock + } + .provideSome[DynamicServer & Server & Client](Scope.default) + .provideShared(DynamicServer.live, serverTestLayer, Client.default) @@ withLiveClock @@ sequential private def staticSpec = suite("Static RandomAccessFile Server")( suite("fromResource")( diff --git a/zio-http/jvm/src/test/scala/zio/http/WebSocketSpec.scala b/zio-http/jvm/src/test/scala/zio/http/WebSocketSpec.scala index 197bce56ee..3f87b59f73 100644 --- a/zio-http/jvm/src/test/scala/zio/http/WebSocketSpec.scala +++ b/zio-http/jvm/src/test/scala/zio/http/WebSocketSpec.scala @@ -211,7 +211,8 @@ object WebSocketSpec extends HttpRunnableSpec { override def spec = suite("Server") { serve.as(List(websocketSpec)) } - .provideShared(DynamicServer.live, serverTestLayer, Client.default, Scope.default) @@ + .provideSome[DynamicServer & Server & Client](Scope.default) + .provideShared(DynamicServer.live, serverTestLayer, Client.default) @@ diagnose(30.seconds) @@ withLiveClock @@ sequential final class MessageCollector[A](ref: Ref[List[A]], promise: Promise[Nothing, Unit]) { diff --git a/zio-http/shared/src/main/scala/zio/http/StreamingForm.scala b/zio-http/shared/src/main/scala/zio/http/StreamingForm.scala index 51cc105691..610c535ae6 100644 --- a/zio-http/shared/src/main/scala/zio/http/StreamingForm.scala +++ b/zio-http/shared/src/main/scala/zio/http/StreamingForm.scala @@ -18,6 +18,8 @@ package zio.http import java.nio.charset.Charset +import scala.annotation.tailrec + import zio._ import zio.stacktracer.TracingImplicits.disableAutoTrace @@ -172,14 +174,31 @@ object StreamingForm { new State(FormState.fromBoundary(boundary), None, _inNonStreamingPart = false) } - private final class Buffer(bufferSize: Int) { - private val buffer: Array[Byte] = new Array[Byte](bufferSize) + private final class Buffer(initialSize: Int) { + private var buffer: Array[Byte] = new Array[Byte](initialSize) private var length: Int = 0 + private def ensureHasCapacity(requiredCapacity: Int): Unit = { + @tailrec + def calculateNewCapacity(existing: Int, required: Int): Int = { + val newCap = existing * 2 + if (newCap < required) calculateNewCapacity(newCap, required) + else newCap + } + + val l = buffer.length + if (l <= requiredCapacity) { + val newArray = Array.ofDim[Byte](calculateNewCapacity(l, requiredCapacity)) + java.lang.System.arraycopy(buffer, 0, newArray, 0, l) + buffer = newArray + } else () + } + def addByte( crlfBoundary: Chunk[Byte], byte: Byte, ): Chunk[Take[Nothing, Byte]] = { + ensureHasCapacity(length + crlfBoundary.length) buffer(length) = byte if (length < (crlfBoundary.length - 1)) { // Not enough bytes to check if we have the boundary