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

Apollo Uploads don't work correctly #518

Closed
erikhofer opened this issue Dec 17, 2019 · 8 comments
Closed

Apollo Uploads don't work correctly #518

erikhofer opened this issue Dec 17, 2019 · 8 comments
Labels
type: bug Something isn't working

Comments

@erikhofer
Copy link

Library Version
1.4.2

Describe the bug
Im using graphql-kotlin to build the schema for graphql-java (because I can't switch to WebFlux at the moment). I'd like to use Apollo uploads but they don't quite work. The schema is created correctly but the datafetcher throws at runtime.

Stack trace
java.lang.IllegalArgumentException: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.apache.catalina.core.ApplicationPart["inputStream"]->java.io.FileInputStream["fd"])
	at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:3922)
	at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:3853)
	at com.expediagroup.graphql.execution.FunctionDataFetcher.convertParameterValue(FunctionDataFetcher.kt:85)
	at com.expediagroup.graphql.execution.FunctionDataFetcher.mapParameterToValue(FunctionDataFetcher.kt:79)
	at com.expediagroup.graphql.execution.FunctionDataFetcher.get(FunctionDataFetcher.kt:58)
	at graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation.lambda$instrumentDataFetcher$0(DataLoaderDispatcherInstrumentation.java:86)
	at graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:270)
	at graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:198)
	at graphql.execution.AsyncExecutionStrategy.execute(AsyncExecutionStrategy.java:74)
	at graphql.execution.Execution.executeOperation(Execution.java:161)
	at graphql.execution.Execution.execute(Execution.java:102)
	at graphql.GraphQL.execute(GraphQL.java:605)
	at graphql.GraphQL.parseValidateAndExecute(GraphQL.java:538)
	at graphql.GraphQL.executeAsync(GraphQL.java:502)
	at graphql.servlet.core.GraphQLQueryInvoker.query(GraphQLQueryInvoker.java:106)
	at graphql.servlet.core.GraphQLQueryInvoker.query(GraphQLQueryInvoker.java:102)
	at graphql.servlet.core.GraphQLQueryInvoker.queryAsync(GraphQLQueryInvoker.java:51)
	at graphql.servlet.core.GraphQLQueryInvoker.query(GraphQLQueryInvoker.java:47)
	at graphql.servlet.AbstractGraphQLHttpServlet.query(AbstractGraphQLHttpServlet.java:374)
	at graphql.servlet.AbstractGraphQLHttpServlet.lambda$init$4(AbstractGraphQLHttpServlet.java:220)
	at graphql.servlet.AbstractGraphQLHttpServlet.doRequest(AbstractGraphQLHttpServlet.java:342)
	at graphql.servlet.AbstractGraphQLHttpServlet.doRequestAsync(AbstractGraphQLHttpServlet.java:333)
	at graphql.servlet.AbstractGraphQLHttpServlet.doPost(AbstractGraphQLHttpServlet.java:365)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:92)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter.doFilterInternal(OpenEntityManagerInViewFilter.java:186)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:320)
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:126)
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:90)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:118)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:158)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:92)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:77)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:215)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:108)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:526)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1579)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.apache.catalina.core.ApplicationPart["inputStream"]->java.io.FileInputStream["fd"])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
	at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191)
	at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:403)
	at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71)
	at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33)
	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:721)
	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
	at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:721)
	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
	at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:3900)
	... 105 common frames omitted

The object passed to FunctionDataFetcher.convertParameterValue() already is of type ApplicationPart. It is passed to the jackson object mapper anyway. The object mapper then fails to deserialize the object.

private fun convertParameterValue(param: KParameter, environment: DataFetchingEnvironment): Any? {
val name = param.getName()
val klazz = param.javaTypeClass()
val value = objectMapper.convertValue(environment.arguments[name], klazz)
val predicateResult = executionPredicate?.evaluate(value = value, parameter = param, environment = environment)
return predicateResult ?: value
}

Code changed in master but has the same behavior:

private fun convertParameterValue(param: KParameter, environment: DataFetchingEnvironment): Any? {
val name = param.getName()
val klazz = param.javaTypeClass()
val argument = environment.arguments[name]
return objectMapper.convertValue(argument, klazz)
}

To Reproduce
Provide graphql.servlet.core.ApolloScalars.Upload as a ScalarType with Java type org.apache.catalina.core.ApplicationPart via SchemaGeneratorHooks. The correct Java type should be javax.servlet.http.Part but interfaces cannot be used as parameters. (Side question: why are interfaces forbidden for scalars?)

Create a mutation function that takes an ApplicationPart as a parameter.

Call the mutation, see https://github.com/jaydenseric/apollo-upload-examples

Expected behavior
The Part object should not be deserialized but passed to the function directly. Maybe skip mapping if actual and desired type are the same?

@erikhofer erikhofer added the type: bug Something isn't working label Dec 17, 2019
@dariuszkuc
Copy link
Collaborator

File upload is not really part of the GraphQL spec so that was not something we were looking at. That being said, it should be possible to support it through the schema generator hooks and custom data fetcher.

Can you provide sample project with the test setup?

@erikhofer
Copy link
Author

I currently don't have time to create a full example project but I manage to fix the issue with a custom data fetcher. Since FunctionDataFetcher can't be extended, I had to override the ObjectMapper that is passed to it.

class ShortCircuitObjectMapper : ObjectMapper() {

  @Suppress("UNCHECKED_CAST")
  override fun <T : Any?> convertValue(fromValue: Any?, toValueType: Class<T>?): T {
    if (fromValue?.javaClass == toValueType) {
      return fromValue as T
    }
    return super.convertValue(fromValue, toValueType)
  }
}
@Component
class DataFetcherFactoryProvider(@Autowired private val hooks: SchemaGeneratorHooks) : KotlinDataFetcherFactoryProvider(hooks) {

  private val shortCircuitObjectMapper = ShortCircuitObjectMapper()

  override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any> =
      DataFetcherFactories.useDataFetcher(
          FunctionDataFetcher(
              target = target,
              fn = kFunction,
              objectMapper = shortCircuitObjectMapper,
              executionPredicate = hooks.dataFetcherExecutionPredicate))
}

The JavaDoc of ObjectMapper.convertValue() states the following:

If given value is already of requested type, value is returned as is.

But that is misleading. The short-circuit mechanism I implemented in the fix has been removed, see FasterXML/jackson-databind#2220

The caller is now responsible for checking if conversion is actually needed. I would suggest to inlcude such a check in FunctionDataFetcher.convertParameterValue(). Something like

if (argument.javaClass == klazz) {
  return argument
}

@erikhofer
Copy link
Author

Another caveat: if the declared parameter is of type Array<ApplicationPart>, the object is of type List<ApplicationPart> so the above check fails. Since we need conversion here (also for complex input objects), the check for type equality doesn't suffice. Maybe it is possible to configure the object mapper so that it does not attempt to deserialize Parts in general 🤔

@dariuszkuc
Copy link
Collaborator

@erikhofer with #525 default data fetcher will correctly use proper jackson object mapper from the context.

@smyrick
Copy link
Contributor

smyrick commented Feb 22, 2020

Resolved in #525

@gonzalt03
Copy link

@erikhofer Do you have an example of the implementation you have done to manage the sending of files using this Kotlin library?

@erikhofer
Copy link
Author

@gonzalt03 It's not a small example but you can look at the project here https://github.com/codefreak/codefreak/tree/master/src/main/kotlin/org/codefreak/codefreak/graphql

@Eboubaker
Copy link

I had problems with ObjectMapper trying to serialize ApplicationPart (uploaded file). This is how I did it ( I intercept the _convert call of ObjectMapper)

import com.coxautodev.graphql.tools.SchemaParserOptions;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.schema.*;
import graphql.servlet.core.ApolloScalars;
import graphql.servlet.core.DefaultObjectMapperConfigurer;
import org.apache.catalina.core.ApplicationPart;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GraphQLConfig {
    @Bean
    public GraphQLScalarType uploadScalar() {
        return ApolloScalars.Upload;
    }

    @Bean
    public SchemaParserOptions schemaParserOptions() {
        var defaultObjectMapperConfigurer = new DefaultObjectMapperConfigurer();
        return SchemaParserOptions.newOptions()
            .objectMapperProvider(fieldDefinition -> {
                var mapper = new ObjectMapper() {
                    @Override
                    protected Object _convert(Object fromValue, JavaType toValueType) throws IllegalArgumentException {
                        if (toValueType.getRawClass().equals(ApplicationPart.class)) {
                            return fromValue;
                        }
                        if (toValueType.hasContentType()
                            && toValueType.getContentType().getRawClass().equals(ApplicationPart.class)) {
                            return fromValue;
                        }
                        return super._convert(fromValue, toValueType);
                    }
                };
                defaultObjectMapperConfigurer.configure(mapper);
                return mapper;
            })
            .build();
    }
}

This is what I have in the graphql Mutation

scalar Upload
type Mutation {
    saveOrder(attachments: [Upload!]!, order: OrderInput!): Order
}

In the service:

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService implements GraphQLMutationResolver {
    @Transactional
    public Order saveOrder(List<ApplicationPart> attachments, @NotNull Order order) {
        // 
    }
}

my build.gradle:

dependencies {
    implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:5.10.0")
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Something isn't working
Development

No branches or pull requests

5 participants