Netflix / dgs-framework

GraphQL for Java with Spring Boot made easy.
https://netflix.github.io/dgs
Apache License 2.0
3.06k stars 295 forks source link

bug: File upload doesn't work with Spring WebFlux #819

Closed shiyouping closed 10 months ago

shiyouping commented 2 years ago

Expected behavior

According to DGS document and GraphQL multipart request specification, file upload should work as expected.

Actual behavior

com.netflix.graphql.dgs.webflux.handlers.DefaultDgsWebfluxHttpHandler will treat form-data requests as application/json and use com.fasterxml.jackson.databind.ObjectMapper to parse the requests. Then the framework will throw the following exception:

Error has been observed at the following site(s):
    *_______________________________________Mono.map ⇢ at com.netflix.graphql.dgs.webflux.handlers.DefaultDgsWebfluxHttpHandler.graphql(DefaultDgsWebfluxHttpHandler.kt:35)
    |_                                  Mono.flatMap ⇢ at com.netflix.graphql.dgs.webflux.handlers.DefaultDgsWebfluxHttpHandler.graphql(DefaultDgsWebfluxHttpHandler.kt:48)
    |_                                  Mono.flatMap ⇢ at com.netflix.graphql.dgs.webflux.handlers.DefaultDgsWebfluxHttpHandler.graphql(DefaultDgsWebfluxHttpHandler.kt:61)
    *___________________________________Mono.flatMap ⇢ at org.springframework.cloud.sleuth.instrument.web.TraceHandlerFunction.handle(TraceHandlerFunction.java:54)
    |_                                Mono.doFinally ⇢ at org.springframework.cloud.sleuth.instrument.web.TraceHandlerFunction.handle(TraceHandlerFunction.java:54)
    |_                                      Mono.map ⇢ at org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle(HandlerFunctionAdapter.java:62)
    *___________________________________Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:153)
    |_                                  Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:154)
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *______________________________________Mono.then ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.onAuthenticationSuccess(AuthenticationWebFilter.java:135)
    *___________________________________Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.authenticate(AuthenticationWebFilter.java:124)
    |_                                Mono.doOnError ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.authenticate(AuthenticationWebFilter.java:126)
    *___________________________________Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:114)
    |_                            Mono.onErrorResume ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:115)
    |_                                    checkpoint ⇢ com.hohomalls.web.filter.AuthenticationFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *_____________________________Mono.switchIfEmpty ⇢ at org.springframework.security.web.server.authorization.AuthorizationWebFilter.filter(AuthorizationWebFilter.java:55)
    |_                                    checkpoint ⇢ org.springframework.security.web.server.authorization.AuthorizationWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                            Mono.onErrorResume ⇢ at org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter.filter(ExceptionTranslationWebFilter.java:58)
    |_                                    checkpoint ⇢ org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter [DefaultWebFilterChain]
    *__Operators$MultiSubscriptionSubscriber.onError ⇢ at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onError(ScopePassingSpanSubscriber.java:96)
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *___________________________________Mono.flatMap ⇢ at org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter.filter(ServerRequestCacheWebFilter.java:39)
    |_                                    checkpoint ⇢ org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                                    checkpoint ⇢ org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *______________________________________Mono.then ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.onAuthenticationSuccess(AuthenticationWebFilter.java:135)
    *___________________________________Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.authenticate(AuthenticationWebFilter.java:124)
    |_                                Mono.doOnError ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.authenticate(AuthenticationWebFilter.java:126)
    *___________________________________Mono.flatMap ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:114)
    |_                            Mono.onErrorResume ⇢ at org.springframework.security.web.server.authentication.AuthenticationWebFilter.filter(AuthenticationWebFilter.java:115)
    |_                                    checkpoint ⇢ com.hohomalls.web.filter.AuthenticationFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                                    checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                                    checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    *___________________________________Mono.flatMap ⇢ at org.springframework.security.web.server.WebFilterChainProxy.filter(WebFilterChainProxy.java:56)
    |_                                    checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                                    checkpoint ⇢ org.springframework.cloud.sleuth.instrument.web.TraceWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                                 Mono.doOnEach ⇢ at org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter.filter(MetricsWebFilter.java:87)
    |_                               Mono.doOnCancel ⇢ at org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter.filter(MetricsWebFilter.java:88)
    *_________________________Mono.transformDeferred ⇢ at org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter.filter(MetricsWebFilter.java:82)
    |_                                    checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                                    checkpoint ⇢ com.hohomalls.web.filter.CorsFilter [DefaultWebFilterChain]
    *_____________________________________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:119)
    |_                            Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:77)
    *_____________________________________Mono.error ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler$CheckpointInsertingHandler.handle(ExceptionHandlingWebHandler.java:98)
    |_                                    checkpoint ⇢ HTTP POST "/graphql" [ExceptionHandlingWebHandler]
Original Stack Trace:
        at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2391) ~[jackson-core-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:735) ~[jackson-core-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.core.base.ParserMinimalBase.reportUnexpectedNumberChar(ParserMinimalBase.java:557) ~[jackson-core-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleInvalidNumberStart(ReaderBasedJsonParser.java:1718) ~[jackson-core-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._parseNegNumber(ReaderBasedJsonParser.java:1467) ~[jackson-core-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(ReaderBasedJsonParser.java:784) ~[jackson-core-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4762) ~[jackson-databind-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4668) ~[jackson-databind-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3630) ~[jackson-databind-2.13.0.jar:2.13.0]
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3613) ~[jackson-databind-2.13.0.jar:2.13.0]
        at com.netflix.graphql.dgs.webflux.handlers.DefaultDgsWebfluxHttpHandler.graphql$lambda-0(DefaultDgsWebfluxHttpHandler.kt:79) ~[graphql-dgs-spring-webflux-autoconfigure-4.9.14.jar:4.9.14]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:113) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:295) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:159) ~[reactor-core-3.4.12.jar:3.4.12]
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:103) ~[spring-cloud-sleuth-instrumentation-3.1.0.jar:3.1.0]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:150) ~[reactor-core-3.4.12.jar:3.4.12]
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:103) ~[spring-cloud-sleuth-instrumentation-3.1.0.jar:3.1.0]
        at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onComplete(FluxPeekFuseable.java:277) ~[reactor-core-3.4.12.jar:3.4.12]
        at org.springframework.cloud.sleuth.instrument.reactor.ScopePassingSpanSubscriber.onComplete(ScopePassingSpanSubscriber.java:103) ~[spring-cloud-sleuth-instrumentation-3.1.0.jar:3.1.0]
        at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.12.jar:3.4.12]
        at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:400) ~[reactor-netty-core-1.0.13.jar:1.0.13]
        at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:419) ~[reactor-netty-core-1.0.13.jar:1.0.13]
        at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:590) ~[reactor-netty-http-1.0.13.jar:1.0.13]
        at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93) ~[reactor-netty-core-1.0.13.jar:1.0.13]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:264) ~[reactor-netty-http-1.0.13.jar:1.0.13]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324) ~[netty-codec-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) ~[netty-codec-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) ~[netty-transport-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[netty-common-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.70.Final.jar:4.1.70.Final]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.70.Final.jar:4.1.70.Final]
        at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

Steps to reproduce

I have used cURL, Postman and Altair GraphQL Client to test the file upload without luck.

type Mutation {

Upload a file and return the URL on the server

uploadFile(file: Upload!, rootDir: String!, subDir: String!): String

}


- Java method snippet or check it on [FileDataFetcher.java](https://github.com/shiyouping/hohomalls/blob/db9d1174d44434121eaebfbe0cd9df5143095379/server/hohomalls-web/src/main/java/com/hohomalls/web/datafetcher/FileDataFetcher.java):
````java
  @DgsData(parentType = "Mutation")
  public Mono<String> uploadFile(DataFetchingEnvironment env) {
    FileDataFetcher.log.info("Received a request to upload a file");
}
berngp commented 2 years ago

Thanks for reporting the issue @shiyouping.

srinivasankavitha commented 2 years ago

Thanks, we do not support File uploads with webflux yet.

On Mon, Jan 17, 2022 at 12:11 PM Bernardo Gomez Palacio < @.***> wrote:

Thanks for reporting the issue @shiyouping https://github.com/shiyouping .

— Reply to this email directly, view it on GitHub https://github.com/Netflix/dgs-framework/issues/819#issuecomment-1014856346, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJ5JPXLEEMMBET5AMWYPBDDUWRZXRANCNFSM5LTYPEUA . You are receiving this because you are subscribed to this thread.Message ID: @.***>

shiyouping commented 2 years ago

Any schedule to support it? Thanks in advance. @srinivasankavitha @berngp

srinivasankavitha commented 2 years ago

We do not plan on adding support in the next few months at least, since WebFlux is not used internally. You are also the first to request this feature. We do welcome contributions from folks willing to help out with this.

On Fri, Jan 21, 2022 at 12:23 AM Shi Youping @.***> wrote:

Any schedule to support it? Thanks in advance.

— Reply to this email directly, view it on GitHub https://github.com/Netflix/dgs-framework/issues/819#issuecomment-1018284109, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJ5JPXMT34YNS74RYUHZKOTUXEJZDANCNFSM5LTYPEUA . You are receiving this because you commented.Message ID: @.***>

ghost commented 2 years ago

I am having the same issue Would be great if this is supported in the future Thanks

ghost commented 2 years ago

Is there any way of doing a temporal fix or some hack in order to still use webflux but just use spring web mvc in the file uploads or I must have all the project with spring mvc? Because right now I am forced to use Base64 as inputs in my file uploads and this is not really well performant.

shiyouping commented 2 years ago

@zanonena You can still use Spring Webflux to upload the files without dgs-framework. Here is a sample code https://github.com/shiyouping/hohomalls/blob/master/server/hohomalls-web/src/main/java/com/hohomalls/web/controller/FileController.java

ghost commented 2 years ago

@zanonena You can still use Spring Webflux to upload the files without dgs-framework. Here is a sample code https://github.com/shiyouping/hohomalls/blob/master/server/hohomalls-web/src/main/java/com/hohomalls/web/controller/FileController.java

Thanks! but I must use GraphQL

bartebor commented 1 year ago

I must admit that DGS library is well designed and I have never had any problems using it in WebFlux mode. That is, until I had to implement file uploads. The lack of file uploads in WebFlux is not documented, so I was very disappointed when I found this issue open.

I need file uploads, so I came up with workaround. This is essentially custom HTTP handler bean with few classes operating on Part class instead of MultipartFile. I'm not familiar with Kotlin unfortunately, so I can't make a PR at the moment. For those who may find it handy I'm leaving a link to my Java hack.

It would be great if maintainers implemented file uploads in WebFlux flavor!

Pretty, pretty please with sugar on top :wink:

srinivasankavitha commented 1 year ago

@bartebor - Thanks for posting your solution for the workaround. Unfortunately, this feature is unlikely to be prioritized anytime soon due to internal conflicting requests. We do not use Webflux within Netflix yet. We are open to contributions though, so if anyone if interested, we would welcome the support!

hwhh commented 10 months ago

Any chance this will be added soon or has been added?

srinivasankavitha commented 10 months ago

Unfortunately this is not a priority for us since we do not use the webflux stack internally, and therefore unlikely we will support this in the future for webflux. We'll update our documentation to reflect this.

On Wed, Nov 8, 2023 at 6:29 PM hwhh @.***> wrote:

Any chance this will be added soon or has been added?

— Reply to this email directly, view it on GitHub https://github.com/Netflix/dgs-framework/issues/819#issuecomment-1803066427, or unsubscribe https://github.com/notifications/unsubscribe-auth/AJ5JPXLYDA7USO32EF56ZELYDQ5YPAVCNFSM5LTYPEUKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOBQGMYDMNRUGI3Q . You are receiving this because you were mentioned.Message ID: @.***>

lthoulon-locala commented 8 months ago

Hello.

Let me first say that I really like the DGS framework. It's great to work with. However I'm disappointed to find out that you're not considering handling this. This seems like a must have for anyone who wants to use DGS and works with webflux (which is probably an increasing number of users).

☝🏼In any case it would be great if you could add this limitation to the file upload documentation because it's not obvious where the issue comes from at first and it's only once I included "multipart" in my search terms that I found this issue.

Although I do understand why Netflix chooses not to do this, I'm wondering about what this would actually cost from its point of view considering @bartebor seems to have provided some code that would serve as a base for this and that the dev teams working on this are probably used to the code and can do that in a timely fashion. Is the cost that great ? It feels that it would be of great value for the community at least. Considering you would still have to take the time to review the code of any new contributor and that this new contributor would have to go through the process of understanding how the DGS code works, isn't there any amount of value in doing this ?

If I knew where to start I might actually be that contributor but I'm not sure what I would be getting into and I can't afford that right now. Maybe in a couple weeks once I've set up a workaround and delivered a first version of my project. But we all know how that generally goes.

I won't hide it, I hope you change you mind on the subject. 😁

Thanks again for all the work you're doing. It's much appreciated. 🫶🏼

paulbakker commented 8 months ago

As a side effect of some bigger news coming soon, this will actually be supported soon.

lthoulon-locala commented 8 months ago

Awesome. Meanwhile if anyone is looking for a Kotlin version of the hack initially provided by @bartebor You'll find it here: https://gist.github.com/lthoulon-locala/02efaf339d9f6b8795bc48f425926efe

I reworked this based on the original Netflix files so that the code is as close as possible to theirs.

lthoulon-locala commented 1 week ago

If anyone is looking up how to update the code with the latest integration with spring-graphql (dgs 9.1.0) it now almost out of the box as explained here: https://netflix.github.io/dgs/spring-graphql-integration/#file-uploads

You need to use this dependency:

            <dependency>
                <groupId>name.nkonev.multipart-spring-graphql</groupId>
                <artifactId>multipart-spring-graphql</artifactId>
                <version>${multipart-spring-graphql.version}</version>
            </dependency>

Then you can do the following

    @DgsMutation
    fun myMutationWithAFileUpload(@InputArgument myFile: FilePart) = 
        myFile.content().let { mergeDataBuffers(it) }.flatMap { doSomethingWithThatFile(it) }

    companion object {
        fun mergeDataBuffers(dataBufferFlux: Flux<DataBuffer>): Mono<ByteArray> =
            DataBufferUtils.join(dataBufferFlux).map { dataBuffer ->
                val bytes = ByteArray(dataBuffer.readableByteCount())
                dataBuffer.read(bytes)
                DataBufferUtils.release(dataBuffer)
                bytes
            }
    }

You can remove all the rest of the specific code provided in the previous comments. And that's it!