Skip to content

Commit

Permalink
Merge branch 'main' into issue-2942
Browse files Browse the repository at this point in the history
  • Loading branch information
gregor-rayman authored Jul 31, 2024
2 parents de4a02b + 025bf90 commit e077e55
Show file tree
Hide file tree
Showing 21 changed files with 165 additions and 334 deletions.
2 changes: 1 addition & 1 deletion docs/guides/testing-http-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Using the `TestServer` we can write tests for our HTTP applications by starting
Using the following methods we can define the behavior of the `TestServer`:

- `TestServer.addRequestResponse` - Adds an exact 1-1 behavior. It takes a request and a response and returns a `ZIO[TestServer, Nothing, Unit]`.
- `TestServer.addRoute` and `TestServer.addRouts` - Adds a route definition to handle requests that are submitted by test cases. It takes a `Route` or `Routes` and returns a `ZIO[R with TestServer, Nothing, Unit]`.
- `TestServer.addRoute` and `TestServer.addRoutes` - Adds a route definition to handle requests that are submitted by test cases. It takes a `Route` or `Routes` and returns a `ZIO[R with TestServer, Nothing, Unit]`.
- `TestServer.install` - Installs a `HttpApp` to the `TestServer`.

After defining the behavior of the test server, we can use the `TestServer.layer` to provide the `TestServer` to any test cases that require `Server`:
Expand Down
16 changes: 4 additions & 12 deletions zio-http/js/src/main/scala/zio/http/internal/FetchBody.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import org.scalajs.dom.ReadableStream

case class FetchBody(
content: ReadableStream[Uint8Array],
mediaType: Option[MediaType],
private[zio] val boundary: Option[Boundary],
contentType: Option[Body.ContentType],
) extends Body {

/**
Expand Down Expand Up @@ -65,19 +64,12 @@ case class FetchBody(
* Updates the media type attached to this body, returning a new Body with the
* updated media type
*/
override def contentType(newMediaType: MediaType): Body =
copy(mediaType = Some(newMediaType))

override def contentType(newMediaType: MediaType, newBoundary: Boundary): Body =
copy(mediaType = Some(newMediaType), boundary = Some(newBoundary))
override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType))
}

object FetchBody {

def fromResponse(result: org.scalajs.dom.Response): Body = {
val mediaType =
if (result.headers.has("Content-Type")) MediaType.forContentType(result.headers.get("Content-Type"))
else None
FetchBody(result.body, mediaType, None)
def fromResponse(result: org.scalajs.dom.Response, contentType: Option[Body.ContentType]): Body = {
FetchBody(result.body, contentType)
}
}
14 changes: 9 additions & 5 deletions zio-http/js/src/main/scala/zio/http/internal/FetchDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ final case class FetchDriver() extends ZClient.Driver[Any, Throwable] {
},
)
.toFuture
} yield Response(
status = Status.fromInt(response.status),
headers = Headers.fromIterable(response.headers.map(h => Header.Custom(h(0), h(1)))),
body = FetchBody.fromResponse(response),
)
} yield {
val respHeaders = Headers.fromIterable(response.headers.map(h => Header.Custom(h(0), h(1))))
val ct = respHeaders.get(Header.ContentType)
Response(
status = Status.fromInt(response.status),
headers = respHeaders,
body = FetchBody.fromResponse(response, ct.map(Body.ContentType.fromHeader)),
)
}

}
} yield response
Expand Down
35 changes: 9 additions & 26 deletions zio-http/jvm/src/main/scala/zio/http/netty/NettyBody.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import zio.stream.ZStream

import zio.http.Body.UnsafeBytes
import zio.http.internal.BodyEncoding
import zio.http.{Body, Boundary, MediaType}
import zio.http.{Body, Boundary, Header, MediaType}

import io.netty.buffer.{ByteBuf, ByteBufUtil}
import io.netty.util.AsciiString
Expand All @@ -40,41 +40,31 @@ object NettyBody extends BodyEncoding {
private[zio] def fromAsync(
unsafeAsync: UnsafeAsync => Unit,
knownContentLength: Option[Long],
contentTypeHeader: Option[String] = None,
contentTypeHeader: Option[Header.ContentType] = None,
): Body = {
val (mediaType, boundary) = mediaTypeAndBoundary(contentTypeHeader)
AsyncBody(
unsafeAsync,
knownContentLength,
mediaType,
boundary,
contentTypeHeader.map(Body.ContentType.fromHeader),
)
}

/**
* Helper to create Body from ByteBuf
*/
private[zio] def fromByteBuf(byteBuf: ByteBuf, contentTypeHeader: Option[String]): Body = {
private[zio] def fromByteBuf(byteBuf: ByteBuf, contentTypeHeader: Option[Header.ContentType]): Body = {
if (byteBuf.readableBytes() == 0) Body.EmptyBody
else {
val (mediaType, boundary) = mediaTypeAndBoundary(contentTypeHeader)
Body.ArrayBody(ByteBufUtil.getBytes(byteBuf), mediaType, boundary)
Body.ArrayBody(ByteBufUtil.getBytes(byteBuf), contentTypeHeader.map(Body.ContentType.fromHeader))
}
}

private def mediaTypeAndBoundary(contentTypeHeader: Option[String]) = {
val mediaType = contentTypeHeader.flatMap(MediaType.forContentType)
val boundary = mediaType.flatMap(_.parameters.get("boundary")).map(Boundary(_))
(mediaType, boundary)
}

override def fromCharSequence(charSequence: CharSequence, charset: Charset): Body =
fromAsciiString(new AsciiString(charSequence, charset))

private[zio] final case class AsciiStringBody(
asciiString: AsciiString,
override val mediaType: Option[MediaType] = None,
override val boundary: Option[Boundary] = None,
override val contentType: Option[Body.ContentType] = None,
) extends Body
with UnsafeBytes {

Expand All @@ -94,19 +84,15 @@ object NettyBody extends BodyEncoding {

private[zio] override def unsafeAsArray(implicit unsafe: Unsafe): Array[Byte] = asciiString.array()

override def contentType(newMediaType: MediaType): Body = copy(mediaType = Some(newMediaType))

override def contentType(newMediaType: MediaType, newBoundary: Boundary): Body =
copy(mediaType = Some(newMediaType), boundary = Some(newBoundary))
override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType))

override def knownContentLength: Option[Long] = Some(asciiString.length().toLong)
}

private[zio] final case class AsyncBody(
unsafeAsync: UnsafeAsync => Unit,
knownContentLength: Option[Long],
override val mediaType: Option[MediaType] = None,
override val boundary: Option[Boundary] = None,
override val contentType: Option[Body.ContentType] = None,
) extends Body {

override def asArray(implicit trace: Trace): Task[Array[Byte]] = asChunk.map {
Expand Down Expand Up @@ -150,10 +136,7 @@ object NettyBody extends BodyEncoding {

override def toString(): String = s"AsyncBody($unsafeAsync)"

override def contentType(newMediaType: MediaType): Body = copy(mediaType = Some(newMediaType))

override def contentType(newMediaType: MediaType, newBoundary: Boundary): Body =
copy(mediaType = Some(newMediaType), boundary = Some(newBoundary))
override def contentType(newContentType: Body.ContentType): Body = copy(contentType = Some(newContentType))
}

private[zio] trait UnsafeAsync {
Expand Down
16 changes: 8 additions & 8 deletions zio-http/jvm/src/main/scala/zio/http/netty/NettyBodyWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ object NettyBodyWriter {
}

body match {
case body: FileBody =>
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 s = StreamBody(stream, None, mediaType = body.mediaType)
val s = StreamBody(stream, None, contentType = body.contentType)
NettyBodyWriter.writeAndFlush(s, None, ctx)
case AsyncBody(async, _, _, _) =>
case AsyncBody(async, _, _) =>
async(
new UnsafeAsync {
override def apply(message: Chunk[Byte], isLast: Boolean): Unit = {
Expand All @@ -76,10 +76,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 @@ -120,13 +120,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 @@ -33,7 +33,7 @@ object NettyResponse {
def apply(jRes: FullHttpResponse)(implicit unsafe: Unsafe): Response = {
val status = Conversions.statusFromNetty(jRes.status())
val headers = Conversions.headersFromNetty(jRes.headers())
val data = NettyBody.fromByteBuf(jRes.content(), headers.headers.get(Header.ContentType.name))
val data = NettyBody.fromByteBuf(jRes.content(), headers.get(Header.ContentType))

Response(status, headers, data)
}
Expand Down Expand Up @@ -68,7 +68,7 @@ object NettyResponse {
val data = NettyBody.fromAsync(
callback => responseHandler.connect(callback),
knownContentLength,
contentType.map(_.renderedValue),
contentType,
)
Response(status, headers, data)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ private[zio] final case class ServerInboundHandler(
}

val headers = Conversions.headersFromNetty(nettyReq.headers())
val contentTypeHeader = headers.headers.get(Header.ContentType.name)
val contentTypeHeader = headers.get(Header.ContentType)

nettyReq match {
case nettyReq: FullHttpRequest =>
Expand Down
17 changes: 17 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 @@ -386,6 +386,23 @@ object FormSpec extends ZIOHttpSpec {
.runCollect
.map { c => assertTrue(c == expected) }
},
test("crlf not stripped") {
val content = "1,2\r\n3,4\r\n"
val form = Form(
Chunk(
FormField.textField("csv", content, MediaType.text.`csv`),
),
)
val boundary = Boundary("X-INSOMNIA-BOUNDARY")
val formByteStream = form.multipartBytes(boundary)
val streamingForm = StreamingForm(formByteStream, boundary)
streamingForm.fields.mapZIO { field =>
field.asChunk
}.runCollect.map { chunks =>
val s = chunks.headOption.map(_.asString)
assertTrue(s.contains(content))
}
},
) @@ sequential

def spec =
Expand Down
4 changes: 2 additions & 2 deletions zio-http/jvm/src/test/scala/zio/http/RoutesSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ object RoutesSpec extends ZIOHttpSpec {
val routes = literal("to") / Routes(
Method.GET / "other" -> handler(ZIO.fail(IdFormatError)),
Method.GET / "do" / string("id") -> handler { (id: String, _: Request) => Response.text(s"GET /to/do/${id}") },
).handleError { case IdFormatError =>
)
routes.handleError { case IdFormatError =>
Response.badRequest
}
routes
.run(
path = Path.root / "to" / "do" / "123",
)
Expand Down
15 changes: 15 additions & 0 deletions zio-http/jvm/src/test/scala/zio/http/untitled.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import zio.Chunk
import zio.http.Headers

new String(
Chunk(45,45,105,110,110,101,114,95,98,111,117,110,100,97,114,121,13,10,99,111,110,116,101,110,116,45,116,121,112,101,58,32,116,101,120,116,47,112,108,97,105,110,13,10,13,10,115,111,109,101,32,116,101,120,116,117,97,108,32,99,111,110,116,101,110,116,13,10,109,97,107,101,32,105,116,32,109,117,108,116,105,108,105,110,101,32,116,101,120,116,117,97,108,32,99,111,110,116,101,110,116,33,13,10,98,117,116,32,110,111,116,32,101,110,100,105,110,103,32,119,105,116,104,32,97,32,67,82,76,70,13,10)
.map(_.toByte).toArray
)

new String(
Chunk(45,45,40,40,40,98,97,52,53,56,52,102,56,45,99,97,49,54,45,52,53,55,97,45,57,50,49,101,45,97,100,102,49,97,48,49,57,57,54,97,55,41,41,41,13,10,13,10,13,10)
.map(_.toByte).toArray
)


Headers() == Headers.empty
Loading

0 comments on commit e077e55

Please sign in to comment.