graphql: File uploads not working with graphql-upload package

I’m submitting a…


[ ] Regression 
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request

Current behavior

I was going through tutorials on how to use NestJS with graphql. I wanted to create a mutation to upload a file to the server. I utilized the graphql-upload package to handle this per this NestJS tutorial: https://stephen-knutter.github.io/2020-02-07-nestjs-graphql-file-upload/

When you request the mutation as displayed in the tutorial, the request returns an error not liking the null value in the operation. It seems the GraphQL validators are running too soon not allowing an interceptor or resolver to handle the request.

It is possible this error is in a lower level dependancy or a miss configuration within Nest. I am unsure.

Expected behavior

I should be able to recieve the file within my resolver using graphql-upload package in my resolver.

Minimal reproduction of the problem with instructions

Clone the following Repo, install dependacies, and start dev. Try creating the CURL request or use the postman request showin in the tutorial: https://stephen-knutter.github.io/2020-02-07-nestjs-graphql-file-upload/

Error Reproduction: https://github.com/aaronhawkey48/gqlupload-error

What is the motivation / use case for changing the behavior?

This should be fixed because it is breaking file uploads within nest.

Environment


Nest version :7.0.0
Nestjs/graphql: 7.3.11
graphql-upload: 11.0.0

For Tooling issues:
- Node version: v12.13.0
- Platform:  Mac, Windows

Others:
NA

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 28 (5 by maintainers)

Most upvoted comments

For any future readers, here is how to fix the issue once and for all.

The problem is that @nestjs/graphql’s dependency, apollo-server-core, depends on an old version of graphql-upload (v8.0) which has conflicts with newer versions of Node.js and various packages. Apollo Server v2.21.0 seems to have fixed this but @nestjs/graphql is still on v2.16.1. Furthermore, Apollo Server v3 will be removing the built-in graphql-upload.

The solution suggested in this comment is to disable Apollo Server’s built-in handling of uploads and use your own. This can be done in 3 simple steps:

1. package.json

Remove the fs-capacitor and graphql-upload entries from the resolutions section if you added them, and install the latest version of graphql-upload (v11.0.0 at this time) package as a dependency.

2. src/app.module.ts

Disable Apollo Server’s built-in upload handling and add the graphqlUploadExpress middleware to your application.

import { graphqlUploadExpress } from "graphql-upload"
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"

@Module({
  imports: [
    GraphQLModule.forRoot({
      uploads: false, // disable built-in upload handling
    }),
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(graphqlUploadExpress()).forRoutes("graphql")
  }
}

3. src/blog/post.resolver.ts (example resolver)

Remove the GraphQLUpload import from apollo-server-core and import from graphql-upload instead

// import { GraphQLUpload } from "apollo-server-core" <-- remove this
import { FileUpload, GraphQLUpload } from "graphql-upload"

@Mutation(() => Post)
async postCreate(
  @Args("title") title: string,
  @Args("body") body: string,
  @Args("attachment", { type: () => GraphQLUpload }) attachment: Promise<FileUpload>,
) {
  const { filename, mimetype, encoding, createReadStream } = await attachment
  console.log("attachment:", filename, mimetype, encoding)

  const stream = createReadStream()
  stream.on("data", (chunk: Buffer) => /* do stuff with data here */)
}

Note that there is an upstream problem with graphql-upload (and/or apollo-server-express) when used with node 13 (probably 14 too). See: https://github.com/jaydenseric/graphql-upload/issues/170#issuecomment-562759227

I had the same issues, and was able to resolve it like this:

  import { FileUpload } from "graphql-upload";
  import { GraphQLUpload } from "apollo-server-express"; // notice this is not imported from graphql-upload

  @Mutation(() => User)
  async uploadAvatar(
    @Args("file", { type: () => GraphQLUpload })
    upload: FileUpload
  ) {
    // you can use upload here
  }

Also I added this in package.json, as per the linked issue above:

  "// @resolutions comment": "https://github.com/jaydenseric/graphql-upload/issues/170#issuecomment-562759227",
  "resolutions": {
    "**/**/fs-capacitor": "^5.0.0",
    "**/graphql-upload": "^9.0.0"
  }

Edit: @msheakoski fixed up their example above, have a look. 👆
If you’d like an example that encapsulates the logic in a reusable module, keep reading:

~I tried @msheakoski’s great suggestion - mostly worked! But the middleware needs to be applied to the same path as the GraphQLModule (so, not globally).~

Replaced Step 2. with something like:

graphql-with-upload.module.ts

import {
  DynamicModule,
  MiddlewareConsumer,
  Module,
  NestModule,
} from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { graphqlUploadExpress } from 'graphql-upload';

/** Wraps the GraphQLModule with an up-to-date graphql-upload middleware. */
@Module({})
export class GraphQLWithUploadModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(graphqlUploadExpress())
      .forRoutes('graphql');
  }

  static forRoot(): DynamicModule {
    return {
      module: GraphQLWithUploadModule,
      imports: [
        GraphQLModule.forRoot({
          uploads: false,
          path: '/graphql',
        }),
      ],
    };
  }
}

app.module.ts

import { GraphQLWithUploadModule } from 'graphql-with-upload.module';

@Module({
  imports: [
    GraphQLWithUploadModule.forRoot(),
  ],
})
export class AppModule {}
  • Fixed usage of “GraphQLWithUploadModule” in app.module.ts - thanks @felinto-dev!

Good jobs guys ❤️ @msheakoski @loklaan

I didn’t understand how to apply the solution shown here the first time, so after some trial and error, I got it. I hope you can help someone.

@loklaan I couldn’t understand why you import “GraphQLWithUploadModule” in this file if you don’t use it.

I tried @msheakoski’s great suggestion - mostly worked! But the middleware needs to be applied to the same path as the GraphQLModule (so, not globally).

app.module.ts

import { GraphQLWithUploadModule } from 'graphql-with-upload.module';

@Module({
  imports: [
    GraphQLModule.forRoot(),
  ],
})
export class AppModule {}

This is what worked for me followed by what was shown above:

app.module.ts

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { graphqlUploadExpress } from 'graphql-upload';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      path: '/graphql',
      uploads: false,
    }),
  ],
})
export class BaseModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(graphqlUploadExpress()).forRoutes('graphql');
  }
}

file.resolver.ts

import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { createWriteStream } from 'fs';

@Resolver()
export class FileResolver {
  @Mutation(() => Boolean)
  async uploadFile(
    @Args({ name: 'file', type: () => GraphQLUpload })
    file: FileUpload,
  ) {
    const { filename, mimetype, encoding, createReadStream } = file;
    console.log('attachment:', filename, mimetype, encoding);

    return new Promise((resolve, reject) =>
      createReadStream()
        .pipe(createWriteStream(`./uploads/${filename}`))
        .on('finish', () => resolve(true))
        .on('error', (error) => reject(error)),
    );
  }
}

PS: You need to create a folder called “uploads” at the root of your application, or it will trigger an error.

cheers 🍕😍

Thanks it works, but I wish we wouldn’t have to implement our own upload middleware for such a basic feature. I hope it gets fixed soon to work out of the box.

@loklaan I actually spoke a bit to @jmcdo29 a bit on this issue. Made a reproduction of the issue: https://github.com/luke-cbs/nestjs-upaload-reproduction

This does not use the above solution but actually uses resolutions of the fs-capacitor module. However we noticed although the error was gone and uploading text files and other simple types worked correctly. A new issue comes in when uploading images. These then seem to hang the request.

  • Uploading a .png works (the readstream never finishes and the promise never resolves).
  • Uploading a .jpg only uploads it partially and breaks the image (the readstream never finishes and the promise never resolves).
  • Uploading a package.json/.txt file works correctly and everything works fine

There seems to be some issues that @msheakoski eluded to quite well with Apollo + graphql-upload and then NestJS seems to be caught in all this as a result of other issues.

The end result is I created a REST API endpoint to handle uploads until such time something like these uploads works consistently.

It is an interesting note that this approach is also suggested in @apollographql/graphql-upload-8-fork that apollo-server-core uses. All the more reasons to go with your approach, @msheakoski

I tried @msheakoski’s great suggestion - mostly worked! But the middleware needs to be applied to the same path as the GraphQLModule (so, not globally).

Good catch, @loklaan! I missed that part of the graphql-upload instructions. I updated the “app.use()” with your path suggestion to keep the example simple.

I also like your approach because it keeps the middleware config together with the GraphQLModule config!

I was using the resolutions approach on node v14.15 (downgrading fs-capacitor and graphql-upload), but i keep getting issue with the createReadStream’s finish event not emitted, and the stream keep getting closed prematurely resulting in broken image and promise not resolved properly.

@msheakoski’s solution (https://github.com/nestjs/graphql/issues/901#issuecomment-780007582 ) worked for me with a tiny adjustment to my solutions which used schema first method.

import { GraphQLUpload , graphqlUploadExpress } from "graphql-upload"
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"

@Module({
  imports: [
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'], //graphql schema
      resolvers: { Upload: GraphQLUpload }, // NOTE : Adding this adjustment solved my issue
      uploads: false, 
    }),
  ],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(graphqlUploadExpress()).forRoutes("graphql")
  }
}

before i made the adjustment i keep getting createReadStream is not a function error, look like the file object wasn’t passed into my resolver.

Here is my trimmed package.json to show the version i am using.

"dependencies": {
    "@nestjs/common": "^8.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/graphql": "^8.0.2",
    "@nestjs/platform-express": "^8.0.0",
    "apollo-server-express": "^2.25.2",
    "graphql": "^15.5.1",
    "graphql-upload": "^12.0.0",
  }

Hope this help someone somewhere.

Good jobs guys ❤️ @msheakoski @loklaan

I didn’t understand how to apply the solution shown here the first time, so after some trial and error, I got it. I hope you can help someone.

@loklaan I couldn’t understand why you import “GraphQLWithUploadModule” in this file if you don’t use it.

I tried @msheakoski’s great suggestion - mostly worked! But the middleware needs to be applied to the same path as the GraphQLModule (so, not globally).

app.module.ts

import { GraphQLWithUploadModule } from 'graphql-with-upload.module';

@Module({
  imports: [
    GraphQLModule.forRoot(),
  ],
})
export class AppModule {}

This is what worked for me followed by what was shown above:

app.module.ts

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { graphqlUploadExpress } from 'graphql-upload';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    GraphQLModule.forRoot({
      path: '/graphql',
      uploads: false,
    }),
  ],
})
export class BaseModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(graphqlUploadExpress()).forRoutes('graphql');
  }
}

file.resolver.ts

import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { createWriteStream } from 'fs';

@Resolver()
export class FileResolver {
  @Mutation(() => Boolean)
  async uploadFile(
    @Args({ name: 'file', type: () => GraphQLUpload })
    file: FileUpload,
  ) {
    const { filename, mimetype, encoding, createReadStream } = file;
    console.log('attachment:', filename, mimetype, encoding);

    return new Promise((resolve, reject) =>
      createReadStream()
        .pipe(createWriteStream(`./uploads/${filename}`))
        .on('finish', () => resolve(true))
        .on('error', (error) => reject(error)),
    );
  }
}

PS: You need to create a folder called “uploads” at the root of your application, or it will trigger an error.

cheers 🍕😍

Hey thanks for the snippet. while running that code, I’m getting POST body missing. Did you forget use body-parser middleware? error whenever I tried to run a muatation that got a file upload. Do you have any idea why this is happening?

It seems that (as I’ve initially mentioned), this issue isn’t specifically related to NestJS. Please, use our Discord channel (support) for such questions. We are using GitHub to track bugs, feature requests, and potential improvements.

@kamilmysliwiec I did and they said to open this issue because there is a bug per a core team member jmcdo29. This is a bug report.