[ ] Regression
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request
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.
I should be able to recieve the file within my resolver using graphql-upload package in my resolver.
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
This should be fixed because it is breaking file uploads within nest.
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
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:
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.
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")
}
}
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 {}
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.tsimport { 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?
0.4.0 release, a fix was made to how transforms handle promises that should remedy what you're seeing.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.tsimport { 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.
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()
...
}
{ type: () => GraphQLUpload } in @Args() decorator)upload.promise)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.
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.
3. src/blog/post.resolver.ts (example resolver)
Remove the GraphQLUpload import from apollo-server-core and import from graphql-upload instead