简体   繁体   中英

Can't upload files with Apollo-client GraphQL in Next.js app: POST body missing

I am trying to implement avatar upload in Next.js blog with Node.js server using Apollo client + apollo-upload-client on client side and apollo-express-server on server side.

I've got the next error:

POST body missing. Did you forget use body-parser middleware?

I am sure that I have body parser on my server.

Server.ts

import "reflect-metadata";
import "dotenv-safe/config";
import 'module-alias/register';
import { __prod__ } from "@/config/config";
import express from "express";
import Redis from "ioredis";
import session from "express-session";
import connectRedis from "connect-redis";
import { createConnection } from "typeorm";
import { User } from "@/entities/User";
import { Project } from "@/entities/Project";
import path from "path";

const server = async () => {


  await createConnection({
    type: "postgres",
    url: process.env.DATABASE_URL,
    logging: true,
    migrations: [path.join(__dirname, "./migrations/*")],
    entities: [User, Project]
  });

  const app = express();



  require("@/start/logger"); // log exceptions 

  const RedisStore = connectRedis(session); // connect redis store
  const redis = new Redis(process.env.REDIS_URL);
  
  require("@/start/apolloServer")(app, redis); // create apollo server
  require("@/start/appConfig")(app,redis,RedisStore) // configure app

  const PORT = process.env.PORT || 3007;
  app.listen(PORT, () => {
   console.log(`🚀 Server Started at PORT: ${PORT}`);
  });
};

server().catch((err) => {
  console.error(err);
});

My Apollo Server

I use apollo-server-express

import { ApolloServer, gql } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import ProfilePictureResolver from "@/resolvers/upload";
import { createUserLoader } from "@/utils/createUserLoader";
import { UserResolver } from "@/resolvers/user";
import { ProjectResolver } from "@/resolvers/project";
import {Express} from "express";
import { Redis } from "ioredis";

const typeDefs = gql`

scalar Upload

  type File {
    id: ID!
    filename: String!
    mimetype: String!
    path: String!
  }
  type Mutation {
    singleUpload(file: Upload!): File!
  }
`;

module.exports = async function(app:Express,redis:Redis){
    const apolloServer = new ApolloServer({
      typeDefs,
        schema: await buildSchema({
          resolvers: [UserResolver, ProjectResolver, ProfilePictureResolver],
          validate: false,
        }),
        context: ({ req, res }) => ({
          req,
          res,
          redis,
          userLoader: createUserLoader()
        }),
        uploads: false
      });
    
      apolloServer.applyMiddleware({
        app,
        cors: false,
      });
}

Resolver:

import { Resolver, Mutation, Arg } from 'type-graphql'
import { GraphQLUpload, FileUpload } from 'graphql-upload'
import os from 'os'
import { createWriteStream } from 'fs'
import path from 'path'


@Resolver()
export default class SharedResolver {
  @Mutation(() => Boolean)
  async uploadImage(
    @Arg('file', () => GraphQLUpload)
    file: FileUpload
  ): Promise<Boolean> {
    const { createReadStream, filename } = await file

    const destinationPath = path.join(os.tmpdir(), filename)

    const url = await new Promise((res, rej) =>
      createReadStream()
        .pipe(createWriteStream(destinationPath))
        .on('error', rej)
        .on('finish', () => {
            //stuff to do
        })
    );

    return true;
  }
}

Server config

import {Express} from 'express'
import { __prod__, COOKIE_NAME } from "@/config/config";
import cors from "cors";
import session from "express-session";
import { Redis } from 'ioredis';
import { RedisStore } from 'connect-redis';
import { bodyParserGraphQL } from 'body-parser-graphql'

module.exports = function(app:Express, redis:Redis, RedisStore:RedisStore){
    app.set("trust proxy", 1);
    app.use(bodyParserGraphQL());
    app.use(
        cors({
        origin: process.env.CORS_ORIGIN,
        credentials: true,
        })
    );
    app.use(
        session({
        name: COOKIE_NAME,
        store: new RedisStore({
            client: redis,
            disableTouch: true,
        }),
        cookie: {
            maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
            httpOnly: true,
            sameSite: "lax",
            secure: __prod__,
            domain: __prod__ ? ".heroku.com" : undefined,
        },
        saveUninitialized: false,
        secret: process.env.SESSION_SECRET,
        resave: false,
        })
    );
}

Client

App client

import { createWithApollo } from "@/utils/createWithApollo";
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { NextPageContext } from "next";
import { createUploadLink } from 'apollo-upload-client';

const createClient = (ctx: NextPageContext) =>
new ApolloClient({
  credentials: "include",
  headers: {
    cookie:
      (typeof window === "undefined"
        ? ctx?.req?.headers.cookie
        : undefined) || "",
  },
 
  cache: new InMemoryCache({
    typePolicies: {
      Query: {}
    }
  }),
  link: createUploadLink({uri:'http://localhost:4000/graphql'})
  
});

// const createClient: ApolloClient<NormalizedCacheObject> = new ApolloClient({
//   cache: new InMemoryCache({}),
//   uri: 'http://localhost:4000/graphql'
// });


export const withApollo = createWithApollo(createClient);

Query

import { gql } from '@apollo/client';

export const UPLOAD_IMAGE_MUTATION = gql`
mutation uploadImage($file: Upload!) {
    uploadImage(file: $file)
  }
`;

Page

import React, {useState} from 'react';
import {useSelector} from "react-redux";
import {Box} from "@/components/UI/Box/Box"
import {Header} from "@/components/UI/Text/Header"
import { withApollo } from "@/utils/withApollo";
import withPrivateRoute from "@/HOC/withPrivateRoute";
import { useMutation } from "@apollo/react-hooks";
import { UPLOAD_IMAGE_MUTATION } from "@/graphql/mutations/uploadImage";

interface IProps{};

const Profile:React.FC<IProps> = () => {

  const user = useSelector(state => state.user);
  const [file, setFileToUpload] = useState(null);
  const [uploadImage, {loading}] = useMutation(UPLOAD_IMAGE_MUTATION);

  const onAvatarUpload = (e) =>{
    setFileToUpload(e.target.files[0]);
  }

  const onSubmit = async (e) =>{
    e.preventDefault();
    const response = await uploadImage({
          variables: {file}
    });


  }

    return (
        <Box mt={20} pl={30} pr={30}>
          <Header>
            Edit Profile
          </Header>
          <input onChange={onAvatarUpload} type="file" placeholder="photo" />
          <button onClick={(e)=>onSubmit(e)}>Submit</button>
        </Box>
      
    )
};

export default withApollo({ ssr: false })(withPrivateRoute(Profile, true));

My Client package:

{
  "name": "app",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@apollo/client": "^3.2.5",
    "@apollo/react-hooks": "^4.0.0",
    "apollo-upload-client": "^14.1.3",
    "graphql": "^15.4.0",
    "graphql-tag": "^2.11.0",
    "graphql-upload": "^11.0.0",
    "isomorphic-unfetch": "^3.1.0",
    "next": "^9.5.5",
    "next-apollo": "^5.0.3",
    "next-redux-wrapper": "^6.0.2",
    "react": "^16.14.0",
    "react-dom": "^16.14.0",
    "react-is": "^16.13.1",
    "react-redux": "^7.2.2",
    "redux": "^4.0.5",
    "styled-components": "^5.2.1",
    "urql": "^1.10.3",
    "uuid": "^8.3.1"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^5.11.5",
    "@testing-library/react": "^11.1.1",
    "@types/graphql": "^14.5.0",
    "@types/jest": "^26.0.15",
    "@types/next": "^9.0.0",
    "@types/node": "^14.0.27",
    "@types/react": "^16.9.55",
    "@types/react-dom": "^16.9.9",
    "@types/styled-components": "^5.1.4",
    "@types/uniqid": "^5.2.0",
    "@types/uuid": "^8.3.0",
    "@welldone-software/why-did-you-render": "^5.0.0",
    "babel-plugin-inline-react-svg": "^1.1.2",
    "babel-plugin-module-resolver": "^4.0.0",
    "babel-plugin-styled-components": "^1.11.1",
    "redux-devtools-extension": "^2.13.8",
    "typescript": "^4.0.5"
  }
}

Server package:

{
   "name": "server",
   "version": "1.0.0",
   "description": "",
   "main": "server.ts",
   "scripts": {
      "build": "tsc",
      "watch": "tsc -w",
      "nodemon": "nodemon dist/server.js",
      "dev": "npm-run-all --parallel  watch nodemon",
      "start": "ts-node src/server.ts",
      "client": "cd ../ && npm run dev --prefix client",
      "runall": "npm-run-all --parallel client  dev",
      "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
      "migration:up": "typeorm migration:run",
      "migration:down": "typeorm migration:revert",
      "migration:generate": "typeorm migration:generate -n 'orm_migrations'"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "dependencies": {
      "apollo-server-express": "^2.16.1",
      "argon2": "^0.26.2",
      "connect-redis": "^5.0.0",
      "cors": "^2.8.5",
      "dataloader": "^2.0.0",
      "dotenv-safe": "^8.2.0",
      "express": "^4.17.1",
      "express-async-errors": "^3.1.1",
      "express-session": "^1.17.1",
      "graphql": "^15.3.0",
      "ioredis": "^4.17.3",
      "module-alias": "^2.2.2",
      "path": "^0.12.7",
      "pgtools": "^0.3.0",
      "reflect-metadata": "^0.1.13",
      "type-graphql": "^1.0.0-rc.3",
      "typeorm": "^0.2.25",
      "uuid": "^8.3.0",
      "winston": "^3.3.3"
   },
   "devDependencies": {
      "@types/connect-redis": "0.0.14",
      "@types/cors": "^2.8.8",
      "@types/express": "^4.17.8",
      "@types/express-session": "^1.17.0",
      "@types/graphql": "^14.5.0",
      "@types/ioredis": "^4.17.7",
      "@types/node": "^8.10.66",
      "@types/nodemailer": "^6.4.0",
      "@types/pg": "^7.14.6",
      "@types/uuid": "^8.3.0",
      "gen-env-types": "^1.0.4",
      "nodemon": "^2.0.6",
      "npm-run-all": "^4.1.5",
      "pg": "^8.4.2",
      "ts-node": "^8.10.2",
      "typescript": "^3.9.7"
   },
   "_moduleAliases": {
      "@": "dist/"
   }
}

NOTE!

When I try to remove uploads: false from apolloServer configuration I receive another error:

"Variable "$file" got invalid value {}; Upload value invalid."

And indeed in form data I see

------WebKitFormBoundarybNufV7QLX3EU1SN6 Content-Disposition: form-data; name="operations"

{"operationName":"uploadImage","variables":{"file":null},"query":"mutation uploadImage($file: Upload!) {\\n uploadImage(file: $file)\\n}\\n"} ------WebKitFormBoundarybNufV7QLX3EU1SN6 Content-Disposition: form-data; name="map"

{"1":["variables.file"]} ------WebKitFormBoundarybNufV7QLX3EU1SN6 Content-Disposition: form-data; name="1"; filename="Screen Shot 2020-11-20 at 17.56.14.png" Content-Type: image/png

------WebKitFormBoundarybNufV7QLX3EU1SN6--

I am 100% sure that I pass the file.

I faced the same problem in my NextJs project, I found that the resolver of Upload checks if the value is instanceOf Upload , and that is somehow not working.

I fix it by creating my own resolver without using the 'graphql-upload' package like this:

Solution 1 :

export const resolvers: Resolvers = {
    Upload: new GraphQLScalarType({
        name: 'Upload',
        description: 'The `Upload` scalar type represents a file upload.',
        parseValue(value) {
            return value;
        },
        parseLiteral(ast) {
            throw new GraphQLError('Upload literal unsupported.', ast);
        },
        serialize() {
            throw new GraphQLError('Upload serialization unsupported.');
        },
    })
};

Solution 2 :

Or you can just don't declare any resolver for this type.


Note: Be sure that you declared scalar type of Upload in your schema and you need to add the uploads field to your Apollo Server configuration:

const apolloServer = new ApolloServer({
    uploads: {
        maxFileSize: 10000000, // 10 MB
        maxFiles: 20
    },
.
.
.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM