Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implmented multiparts support for netty #2560

Closed
wants to merge 8 commits into from

Conversation

Pask423
Copy link
Contributor

@Pask423 Pask423 commented Nov 10, 2022

No description provided.

@Pask423
Copy link
Contributor Author

Pask423 commented Nov 10, 2022

Fourth task in #499

import sttp.monad.syntax._

// FIXME: Move to sttp-shared after zio 2.0.3 start working ?
class SequenceSupport[F[_]](implicit me: MonadError[F]) {
Copy link
Member

Choose a reason for hiding this comment

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

let's maybe add this as an implicit value class in sttp.tapir.internal?

@@ -20,7 +20,8 @@ class NettyCatsServerTest extends TestSuite with EitherValues {
val interpreter = new NettyCatsTestServerInterpreter(eventLoopGroup, dispatcher)
val createServerTest = new DefaultCreateServerTest(backend, interpreter)

val tests = new AllServerTests(createServerTest, interpreter, backend, staticContent = false, multipart = false).tests()
val tests = new AllServerTests(createServerTest, interpreter, backend, staticContent = false, multipart = false).tests() ++
new ServerMultipartTests(createServerTest, partOtherHeaderSupport = false).tests()
Copy link
Member

Choose a reason for hiding this comment

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

can we support the additional part headers? seems this might be a matter of adding the headers when serialising the part?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far I as check there is no way to get those headers while parsing

Copy link
Member

Choose a reason for hiding this comment

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

hm it would be weird if the data just got lost

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will check it once more

Copy link
Contributor Author

@Pask423 Pask423 Nov 14, 2022

Choose a reason for hiding this comment

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

After some more digging I cannot see any way to get parts headers from netty - they are not included in overall request headers nor in HttpDataType's used in getParts method.

Copy link
Member

Choose a reason for hiding this comment

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

yeah it seems so; so we'll have to leave this as is, thanks for checking

case RawBodyType.FileBody =>
createFile(serverRequest)
.map(file => {
Files.write(file.toPath, ByteBufUtil.getBytes(upload.content()))
Copy link
Member

Choose a reason for hiding this comment

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

doesn't this read the whole file content into memory?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think yes

Copy link
Member

Choose a reason for hiding this comment

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

well that's not good then. We need to transfer these bytes without reading the content into memory; and if there's any blocking code, surround this with monadError.blocking

Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I understand we could extract the package-private method readBytes from netty (https://github.com/netty/netty/blob/4.1/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java#L1878) and use it here so, that bytes from ByteBuffer could be written to FileOutputStream, depending on the size it is done directly or chunk by chunk.

toPart(part.body, part.contentType, part.name, None)
case RawBodyType.FileBody =>
val fileRange = part.body.asInstanceOf[FileRange]
toPart(Files.readString(fileRange.file.toPath), part.contentType, part.name, Some(fileRange.file.getName))
Copy link
Member

Choose a reason for hiding this comment

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

again, this reads the whole file content into memory - can we provide the data to netty in another way? @rafalambrozewicz you did sth similar, maybe you'll be able to help :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not sure if it is possible to do in other way but any help would be welcome.

Copy link
Contributor

Choose a reason for hiding this comment

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

We've checked whether netty has some support for multipart out of the box, but it looks like it doesn't. Not reading everything into memory forces usage of Chunked class i.e. io.netty.handler.streamChunkedStream, but turning all parts of multipart into such entity seems not trivial or simply not possible :-|

Copy link
Member

Choose a reason for hiding this comment

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

can't we generate a list of chunks, ChunkedInputs or a list of buffers, and call .write multiple times in NettyServerHandler?

s"""
$boundary
$contentTypeStr
Content-Disposition: form-data; $fileNameStr name="$name"
Copy link
Member

Choose a reason for hiding this comment

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

I think here we should be able to add more headers ... however, maybe the names/file names would need some escaping?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If i will find the way to read those headers from request then no problem.

}
}

private def getParts(serverRequest: ServerRequest, m: RawBodyType.MultipartBody): List[F[Part[Any]]] = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider placing nettyRequest function before getParts to match the order of appearance in toRaw and make it easier to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}
}

private def convertToBuffs(bodyType: RawBodyType[_], part: Part[Any]): ByteBuf = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider placing convertToBuffs after wraps, so that this file is easier to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

import sttp.monad.MonadError
import sttp.monad.syntax._

class SequenceSupport[F[_]](implicit me: MonadError[F]) {
Copy link
Member

Choose a reason for hiding this comment

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

just add this as an implicit class to the internal package object :)

private def nettyRequest(serverRequest: ServerRequest): FullHttpRequest = serverRequest.underlying.asInstanceOf[FullHttpRequest]

private def getParts(serverRequest: ServerRequest, m: RawBodyType.MultipartBody): List[F[Part[Any]]] = {
new HttpPostMultipartRequestDecoder(nettyRequest(serverRequest)).getBodyHttpDatas.asScala
Copy link
Member

Choose a reason for hiding this comment

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

javadocs for this class:

/**
 * This decoder will decode Body and can handle POST BODY.
 *
 * You <strong>MUST</strong> call {@link #destroy()} after completion to release all resources.
 *
 */

are we? :)

.flatMap(httpData =>
httpData.getHttpDataType match {
case HttpDataType.Attribute =>
val part: F[Part[Any]] = monadError.unit(Part(name = httpData.getName, body = httpData.asInstanceOf[Attribute].getValue))
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't we inspect the rawbodytype here as well, and convert the String attribute accordingly?

Copy link
Contributor Author

@Pask423 Pask423 Nov 18, 2022

Choose a reason for hiding this comment

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

I am not sure, httpData.asInstanceOf[Attribute].getValue will return String no matter the body type and I do not know how losing information about the original body type will affect our flow.
Moreover it is a possibility that string body will be the only bodyType ever handle by Attribute as for example ByteArrayBody or FileBody are mark as FileUpload type

Copy link
Member

Choose a reason for hiding this comment

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

they are? I though it's up to netty to decide if a part is an attribute, or a file body?

toPart(part.body, part.contentType, part.name, None)
case RawBodyType.FileBody =>
val fileRange = part.body.asInstanceOf[FileRange]
toPart(Files.readString(fileRange.file.toPath), part.contentType, part.name, Some(fileRange.file.getName))
Copy link
Member

Choose a reason for hiding this comment

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

Here the same, we can't read the file into memory

Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I understand we would need to introduce our own ChunkedMultipart data structure. It needs to be constructed by passing a list of ByteBufs with small content and multipart parts' like headers and footers, and chunked streams and chunked files. And it needs to behave like ChunkedFile or ChunkedStream i.e. to implement ChunkedInput<ByteBuf>, a way to treat all the parts as something homogeneous, that could be read as a one whole. At the first glance, it seems to be quite tricky.

case _: RawBodyType.MultipartBody => ???
case m: RawBodyType.MultipartBody =>
monadError
.unit(new HttpPostMultipartRequestDecoder(nettyRequest(serverRequest)))
Copy link
Member

Choose a reason for hiding this comment

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

there's a .unit syntax; or, if this can throw an exception, we should use .eval

.sequence()
.map(RawValue.fromParts)
.map(a => {
decoder.destroy()
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be in the "finally" caluse using .ensure

case HttpDataType.FileUpload =>
m.partType(httpData.getName)
.map(c => {
val upload = httpData.asInstanceOf[FileUpload]
Copy link
Member

Choose a reason for hiding this comment

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

why do we cast to FileUpload if we just need a HttpData?

val mediaType = contentType.flatMap(c => MediaType.parse(c).toOption)
m match {
case RawBodyType.StringBody(charset) =>
monadError.unit(Part(name = upload.getName, body = upload.getString(charset), contentType = mediaType))
Copy link
Member

@adamw adamw Nov 29, 2022

Choose a reason for hiding this comment

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

I think we need two variants, for parts which are in-memory and file-based (as reported by netty). If they are in memory, then this should be a .unit or monad.eval(...) (depending if exceptions are possible).

If these are in a file, then these should be .blocking, as reading from the file is done in a blocking way?

case RawBodyType.FileBody =>
createFile(serverRequest)
.map(file => {
upload.renameTo(file)
Copy link
Member

Choose a reason for hiding this comment

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

this definitely should be in a monad.blocking call

or maybe, there's a way to configure how Netty creates temporary files? We could use then whatever is provided in the options and bypass this step entirely

@max-peroch
Copy link

Thank you for your work on this <3

@adamw
Copy link
Member

adamw commented Apr 3, 2024

Closing old PRs

@adamw adamw closed this Apr 3, 2024
@Pask423 Pask423 deleted the feature/netty-multiparts branch May 7, 2024 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants