无法在 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 正文的主要内容,如果未能解决你的问题,请参考以下文章