-
Notifications
You must be signed in to change notification settings - Fork 422
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
Conversation
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]) { |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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())) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think yes
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :-|
There was a problem hiding this comment.
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, ChunkedInput
s or a list of buffers, and call .write
multiple times in NettyServerHandler
?
s""" | ||
$boundary | ||
$contentTypeStr | ||
Content-Disposition: form-data; $fileNameStr name="$name" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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]]] = { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider placing convertToBuffs
after wrap
s, so that this file is easier to read.
There was a problem hiding this comment.
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]) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ByteBuf
s 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))) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
Thank you for your work on this <3 |
Closing old PRs |
No description provided.