Graphql: File uploads not working with graphql-upload package

Created on 26 May 2020  路  23Comments  路  Source: nestjs/graphql

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

Most helpful comment

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 */)
}

All 23 comments

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.

cc @jmcdo29 did you investigate this issue already by chance?

I did what I could with it. I saw that there is a buffer coming in which looks correct, but something about the mapping ends up making the variable that should be related to the file undefined, which then leaves gql's validation to fail. I can reproduce the curl if you'd like

There is a repo with the minimum needed to produce the error: https://github.com/aaronhawkey48/gqlupload-error

The curl to recreate the error is in the read me.

Thanks again for looking into it 馃檹

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"
  }

Might be noteworthy - looks like GraphQLUpload can be undefined.

Just incase this is helpful - it did work in previous versions but for the newer version it comes through ok in a custom scalar but the resolver isn't waiting for the async parseValue to finish:

import { BadRequestException } from '@nestjs/common'
import { Scalar, CustomScalar } from '@nestjs/graphql'
import { ValueNode } from 'graphql'
import FileType from 'file-type'
import { FileUpload } from 'graphql-upload'
import { isUndefined } from 'lodash'

export type ImageProps = Promise<FileUpload>

@Scalar('Image', () => Image)
export class Image implements CustomScalar<ImageProps, ImageProps> {
  description = 'Image upload type.'
  supportedFormats = ['image/jpeg', 'image/png']

  parseLiteral(file: ValueNode) {
    if (file.kind === 'ObjectValue') {
      const fileObject = file as any
      if (
        typeof fileObject.filename === 'string' &&
        typeof fileObject.mimetype === 'string' &&
        typeof fileObject.encoding === 'string' &&
        typeof fileObject.createReadStream === 'function'
      )
        return Promise.resolve(fileObject)
    }

    return null
  }

  async parseValue(value: ImageProps) {
    // Comes through ok here

    const upload = await value
    const stream = upload.createReadStream()
    const fileType = await FileType.fromStream(stream)

    if (isUndefined(fileType))
      throw new BadRequestException('Mime type is unknown.')

    if (fileType.mime !== upload.mimetype)
      throw new BadRequestException('Mime type does not match file content.')

    if (!this.supportedFormats.includes(fileType.mime))
      throw new BadRequestException(
        `Unsupported file format. Supports: ${this.supportedFormats.join(' ')}.`
      )

    return upload
  }

  serialize(value: ImageProps) {
    return value
  }
}

Thanks for all the feedback. Will test.
@andreialecu It seems the issue is closed. I will test that work around, but do you see any movement to resolve the actual issue?

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.

Hi!

when used with node 13 (_probably 14 too_)

If you are using it on node 14, it works just fine. I've tested it. Just don't forget

"resolutions": {
    "**/**/fs-capacitor":"^6.2.0",
    "**/graphql-upload": "^11.0.0"
}

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 */)
}

_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!

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!

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

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.

Good jobs guys <3 @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 馃崟馃槏

Hi there,
So I was previously getting:

TypeError: Cannot read property 'error' of undefined at new ReadStream (node_modules\@apollographql\graphql-upload-8-fork\node_mod ules\fs-capacitor\lib\index.js:27:36) at TransformOperationExecutor.transform (node_modules\class-transformer\cjs\T ransformOperationExecutor.js:138:32) at TransformOperationExecutor.transform (node_modules\class-transformer\cjs\T ransformOperationExecutor.js:277:47)

I then implemented the above and seems to resolve most issues. However when saving images and what have you it is now making the image format unreadable. This is all on Node 14.

Example is I upload .jpg and then after doing a createWriteStream from the createReadStream in a pipe the image is now broken.

The above solution works fine if I go back to Node V12 however it then does something like this.

node:24976) UnhandledPromiseRejectionWarning: Error [ERR_MULTIPLE_CALLBACK]: Callback called m ultiple times at onwrite (_stream_writable.js:437:11) at TransformOperationExecutor.transform (node_modules\clas s-transformer\cjs\TransformOperationExecutor.js:173:47) at TransformOperationExecutor.transform (node_modules\clas s-transformer\cjs\TransformOperationExecutor.js:277:47) at TransformOperationExecutor.transform (node_modules\clas s-transformer\cjs\TransformOperationExecutor.js:277:47) at TransformOperationExecutor.transform (node_modules\clas s-transformer\cjs\TransformOperationExecutor.js:277:47) at node_modules\class-transformer\cjs\TransformOperationEx ecutor.js:111:51

Image upload is actually working and I can write the file fine, just this weird error being thrown for no reason I can see or catch. Not sure if any info on this?

I _think_ what you're seeing is a bug in class-transformer. What version are you using?

  • In the 0.4.0 release, a fix was made to how transforms handle promises that should remedy what you're seeing.
  • It's been a few months, but I think I ran into this issue and came to a solution. I tried to upgrade to 0.4.0 but couldn't because of breaking API changes that messed with the rest of NestJS's decorators, so I instead took the above commit and applied it as a patch against my installed 0.3.1 version with yarn patch's workflow. Gl!

Good jobs guys <3 @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?

@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.

Work-arounding this isuue is strange indeed and definitely has some quirks. My solution I ended up with:

@Mutation()
async createTemplateFile(
  @Args('file') fileUpload: any,  // weird Promise<FileUpload> plus something else
  @Args('data') input: CreateDto
): Promise<FindOneDto>
{
  const file = (await fileUpload.promise) as FileUpload;
  // use file.mimetype, file.filename, file.createReadStream()
  ...
}
  • No class-transformer bugs (but there are if I use { type: () => GraphQLUpload } in @Args() decorator)
  • No corrupted files (but there are if I don't wait for upload.promise)
  • Works on latest NodeJS (no need to switch back to 12)

The part with graphqlUploadExpress() is same as in previous posts.

Is there any up-to-date docs on the NestJS side? Looks like community solutions either don't work ATM or have challenges with implementing all necessary steps

Is there any up-to-date docs on the NestJS side? Looks like community solutions either don't work ATM or have challenges with implementing all necessary steps

@nick4fake What part is giving you trouble? I can confirm that the approach that I took in https://github.com/nestjs/graphql/issues/901#issuecomment-780007582 has been working perfectly in my projects since the time I posted it.

I think where a lot of people are running into issues is that they don't realize the uploaded file details are a promise and need to be awaited in order to use.

Was this page helpful?
0 / 5 - 0 ratings