无法在 Next.js 应用程序中使用 Apollo-client GraphQL 上传文件:缺少 POST 正文

Posted

技术标签:

【中文标题】无法在 Next.js 应用程序中使用 Apollo-client GraphQL 上传文件:缺少 POST 正文【英文标题】:Can't upload files with Apollo-client GraphQL in Next.js app: POST body missing 【发布时间】:2021-03-06 08:22:36 【问题描述】:

我正在尝试使用 Apollo 客户端 + apollo-upload-client 在客户端和 apollo-express-server 使用 Node.js 服务器在 Next.js 博客中实现头像上传strong> 在服务器端。

我遇到了下一个错误:

POST 正文丢失。你忘记使用 body-parser 中间件了吗?

我确定我的服务器上有正文解析器。

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);
);

我的阿波罗服务器

我用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,
      );

解析器:

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;
  

服务器配置

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,
        )
    );

客户

应用客户端

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);

查询

import  gql  from '@apollo/client';

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

页面

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( s-s-r: false )(withPrivateRoute(Profile, true));

我的客户包:


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

服务器包:


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


注意!

当我尝试从 apolloServer 配置中删除 uploads: false 时,我收到另一个错误:

“变量“$file”的值无效;上传值无效。”

确实在我看到的表单数据中

------WebKitFormBoundarybNufV7QLX3EU1SN6 Content-Disposition: form-data;名称="操作"

"operationName":"uploadImage","variables":"file":null,"query":"mutation uploadImage($file: 上传!) \n uploadImage(file: $file)\n\n" ------WebKitFormBoundarybNufV7QLX3EU1SN6 内容处置:表单数据;名称="地图"

"1":["variables.file"] ------WebKitFormBoundarybNufV7QLX3EU1SN6 内容处置:表单数据;名称=“1”; filename="屏幕截图 2020-11-20 在 17.56.14.png" 内容类型:image/png

-----WebKitFormBoundarybNufV7QLX3EU1SN6--

我 100% 确定我通过了文件。

【问题讨论】:

【参考方案1】:

我在 NextJs 项目中遇到了同样的问题,我发现 Upload 的解析器检查值是否为 instanceOf Upload,这在某种程度上是行不通的。

我通过创建自己的解析器而不使用像这样的“graphql-upload”包来修复它:

解决方案 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.');
        ,
    )
;

解决方案 2:

或者你可以不为这种类型声明任何解析器。


注意: 确保您在架构中声明了 Upload 的标量类型,并且您需要将上传字段添加到您的 Apollo 服务器配置中:

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

【讨论】:

以上是关于无法在 Next.js 应用程序中使用 Apollo-client GraphQL 上传文件:缺少 POST 正文的主要内容,如果未能解决你的问题,请参考以下文章

未找到模块:无法在 Next.js 应用程序中解析“fs”

TailwindCSS 和 next.js - 无法应用自定义颜色

我无法在 Next.js 中引用图像

我无法在 Next.js 中引用图像

Next.js 10版内置Image组件无法加载

无法使用开放图形元标记从我的 React next.js 网站共享 Facebook/Twitter 内容