Skip to content

Commit

Permalink
Merge branch 'main' into update-doc
Browse files Browse the repository at this point in the history
  • Loading branch information
jdegoes authored Jun 6, 2024
2 parents 8c77082 + d4168a7 commit e9a3351
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 28 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
26 changes: 10 additions & 16 deletions zio-http/jvm/src/main/scala/zio/http/netty/NettyBodyWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ object NettyBodyWriter {
body: Body,
contentLength: Option[Long],
ctx: ChannelHandlerContext,
compressionEnabled: Boolean,
)(implicit
trace: Trace,
): Option[Task[Unit]] = {
Expand All @@ -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 = {
Expand All @@ -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) =>
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions zio-http/jvm/src/test/scala/zio/http/ContentTypeSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
}
}
27 changes: 27 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/FormSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions zio-http/jvm/src/test/scala/zio/http/KeepAliveSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
}

}
18 changes: 18 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/SSLSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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 =
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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")(
Expand Down
3 changes: 2 additions & 1 deletion zio-http/jvm/src/test/scala/zio/http/WebSocketSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
23 changes: 21 additions & 2 deletions zio-http/shared/src/main/scala/zio/http/StreamingForm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package zio.http

import java.nio.charset.Charset

import scala.annotation.tailrec

import zio._
import zio.stacktracer.TracingImplicits.disableAutoTrace

Expand Down Expand Up @@ -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
Expand Down

2 comments on commit e9a3351

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 : Performance Benchmarks (SimpleEffectBenchmarkServer)

concurrency: 256
requests/sec: 325091

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 : Performance Benchmarks (PlainTextBenchmarkServer)

concurrency: 256
requests/sec: 342449

Please sign in to comment.