jaydenseric / graphql-upload

Middleware and an Upload scalar to add support for GraphQL multipart requests (file uploads via queries and mutations) to various Node.js GraphQL servers.
https://npm.im/graphql-upload
MIT License
1.42k stars 131 forks source link

Don't know why is not working in NextJs #350

Closed javiash closed 1 year ago

javiash commented 1 year ago

Hi, I've tryed a lot to make this work, but still nothing. First I had the exporting issue but now that is working, thanks for all the previous issues and answers. But now I'm getting a Error: Unexpected end of form. This is my code:

/api/graphql.ts

import connectDb from './mongoose.config';
import resolvers from './resolvers';
import typeDefs from './schemas';

import { getToken } from 'next-auth/jwt';

import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import processRequest from 'graphql-upload/processRequest.mjs';

connectDb();

const server = new ApolloServer({
  resolvers,
  typeDefs,
});

export default startServerAndCreateNextHandler(server, {
  context: async (req, res) => {
    const contentType = req.headers['content-type'];
    console.log(
      '🚀 ~ file: graphql.ts ~ line 22 ~ context: ~ contentType',
      contentType
    );

    if (contentType && contentType.indexOf('multipart/form-data') > -1) {
      try {
        await processRequest(req, res);
      } catch (error) {
        console.log('🚀 ~ file: graphql.ts:37 ~ context: ~ error', error);
        return { req, res, user: await getToken({ req }) };
      }
    }
    return { req, res, user: await getToken({ req }) };
  },
});

graphqlClient.ts

import { ApolloClient, InMemoryCache } from '@apollo/client';
import { createUploadLink } from 'apollo-upload-client';

const server = process.env.VERCEL_URL
  ? `https://${process.env.VERCEL_URL}`
  : 'http://localhost:3000';

const apolloClient = new ApolloClient({
  ssrMode: typeof window === 'undefined',
  cache: new InMemoryCache(),
  connectToDevTools: true,
  link: createUploadLink({
    uri: server + '/api/graphql',
    headers: { 'apollo-require-preflight': 'true' },
  }),
});

export default apolloClient;

resolvers.ts

import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { EventsMutation, EventsQuery } from './events';
import { FollowsMutation, FollowsQuery } from './follow';
import { GamesMutation, GamesQuery } from './games';
import { SponsorsMutation, SponsorsQuery } from './sponsors';
import { SystemsMutation, SystemstQuery } from './systems';
import { UsersMutation, UsersQuery } from './users';

const resolvers = {
  Upload: GraphQLUpload,
  RootQuery: {
    ...EventsQuery,
    ...GamesQuery,
    ...UsersQuery,
    ...SystemstQuery,
    ...SponsorsQuery,
    ...FollowsQuery,
  },

  RootMutation: {
    ...EventsMutation,
    ...GamesMutation,
    ...UsersMutation,
    ...SystemsMutation,
    ...SponsorsMutation,
    ...FollowsMutation,
  },
};

export default resolvers;

schema

const typeDefs = `
  scalar Upload
 `

request

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

{"operationName":"updateAvatar","variables":{"file":null},"query":"mutation updateAvatar($file: Upload) {\n  updateAvatar(file: $file) {\n    status\n    urlAvatar\n    __typename\n  }\n}"}
-----------------------------280040456729225388761008974791
Content-Disposition: form-data; name="map"

{"1":["variables.file"]}
-----------------------------280040456729225388761008974791
Content-Disposition: form-data; name="1"; filename="filename"
Content-Type: image/png

‰PNG

...
xÇåAzÛÐÿèJãSƒ0%
-----------------------------280040456729225388761008974791--

response

{"errors":[{"message":"POST body missing, invalid Content-Type, or JSON object has no keys.","extensions":{"code":"BAD_REQUEST","stacktrace":["BadRequestError: POST body missing, invalid Content-Type, or JSON object has no keys.","    at new GraphQLErrorWithCode (file:///C:/Users/Javier/webDev/proyectosFinales/puntorol/node_modules/.pnpm/@apollo+server@4.2.2_graphql@16.6.0/node_modules/@apollo/server/dist/esm/internalErrorClasses.js:7:9)","    at new BadRequestError (file:///C:/Users/Javier/webDev/proyectosFinales/puntorol/node_modules/.pnpm/@apollo+server@4.2.2_graphql@16.6.0/node_modules/@apollo/server/dist/esm/internalErrorClasses.js:75:9)","    at runHttpQuery (file:///C:/Users/Javier/webDev/proyectosFinales/puntorol/node_modules/.pnpm/@apollo+server@4.2.2_graphql@16.6.0/node_modules/@apollo/server/dist/esm/runHttpQuery.js:76:23)","    at runPotentiallyBatchedHttpQuery (file:///C:/Users/Javier/webDev/proyectosFinales/puntorol/node_modules/.pnpm/@apollo+server@4.2.2_graphql@16.6.0/node_modules/@apollo/server/dist/esm/httpBatching.js:34:22)","    at ApolloServer.executeHTTPGraphQLRequest (file:///C:/Users/Javier/webDev/proyectosFinales/puntorol/node_modules/.pnpm/@apollo+server@4.2.2_graphql@16.6.0/node_modules/@apollo/server/dist/esm/ApolloServer.js:486:26)"]}}]}

package.json

{
  "name": "puntorol",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prettier": "prettier --write ."
  },
  "dependencies": {
    "@apollo/client": "^3.7.0",
    "@apollo/server": "^4.2.2",
    "@as-integrations/next": "^1.2.0",
    "@dicebear/adventurer": "^4.10.5",
    "@dicebear/avatars": "^4.10.5",
    "@graphql-tools/schema": "^9.0.4",
    "@mantine/core": "^5.7.1",
    "@mantine/dates": "^5.7.1",
    "@mantine/dropzone": "^5.7.1",
    "@mantine/form": "^5.7.1",
    "@mantine/hooks": "^5.7.1",
    "@mantine/modals": "^5.7.1",
    "@mantine/next": "^5.7.1",
    "@mantine/notifications": "^5.7.1",
    "@mantine/nprogress": "^5.7.1",
    "@mantine/rte": "^5.7.1",
    "@mantine/spotlight": "^5.7.1",
    "apollo-upload-client": "^17.0.0",
    "aws-sdk": "^2.1230.0",
    "bcryptjs": "^2.4.3",
    "dayjs": "^1.11.5",
    "google-map-react": "^2.2.0",
    "graphql": "^16.6.0",
    "graphql-upload": "^16.0.2",
    "highlight.js": "^11.6.0",
    "ipapi.co": "^0.3.0",
    "mongoose": "^6.6.5",
    "next": "12.3.1",
    "next-auth": "^4.12.3",
    "react": "18.2.0",
    "react-cropper": "^2.1.8",
    "react-dom": "18.2.0",
    "react-icons": "^4.4.0",
    "sass": "^1.55.0",
    "sharp": "^0.31.1",
    "tabler-icons-react": "^1.55.0",
    "tinify": "^1.7.1"
  },
  "devDependencies": {
    "@types/apollo-upload-client": "^17.0.1",
    "@types/bcryptjs": "^2.4.2",
    "@types/google-map-react": "^2.1.7",
    "@types/node": "18.8.3",
    "@types/react": "18.0.21",
    "@types/react-dom": "18.0.6",
    "eslint": "8.24.0",
    "eslint-config-next": "12.3.1",
    "eslint-config-prettier": "^8.5.0",
    "typescript": "4.8.4"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "maxNodeModuleJsDepth": 10,
    "module": "NodeNext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "@/components/*": ["components/*"],
      "@/gql/*": ["gql/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

I don't know if you need another file to check.

I was using 'apollo-server' v3 with micro and it was working, but as it's now deprecated I wanted to switch to this new version.

Sorry in advanced if the format of this message is way off.

jaydenseric commented 1 year ago

This error message is an important clue:

POST body missing, invalid Content-Type, or JSON object has no keys.

The function processRequest resolves the processed request body that you then need to set somewhere to replace the original request body, but in your code you are not using it.

Here is an example from the Koa middleware:

https://github.com/jaydenseric/graphql-upload/blob/e01b5d5541760d529b06c900883c5fa7febcff00/graphqlUploadKoa.mjs#L55

Hope that helps :)

javiash commented 1 year ago

Hi @jaydenseric ! I did that, but the proccessRequest throws an error before I could asign it to the req.body, or even changing the Content-Type...

wait  - compiling /api/graphql (client and server)...
event - compiled successfully in 52 ms (167 modules)
🚀 ~ file: graphql.ts ~ line 22 ~ context: ~ contentType multipart/form-data; boundary=---------------------------90155217136423324944131082892
🚀 ~ file: graphql.ts:37 ~ context: ~ error Error: Unexpected end of form
    at Multipart._final (C:\Users\Javier\webDev\proyectosFinales\puntorol\node_modules\.pnpm\busboy@1.6.0\node_modules\busboy\lib\types\multipart.js:588:17)
    at callFinal (node:internal/streams/writable:663:25)
    at prefinish (node:internal/streams/writable:714:7)
    at finishMaybe (node:internal/streams/writable:724:5)
    at Multipart.Writable.end (node:internal/streams/writable:631:5)
    at onend (node:internal/streams/readable:690:10)
    at processTicksAndRejections (node:internal/process/task_queues:78:11)

This is why is getting the POST body missing, invalid Content-Type, or JSON object has no keys. in the request.

Sorry if there's something that I'm not seeing, and thanks for your reply.

jaydenseric commented 1 year ago

Perhaps investigate one of these tips from the readme:

Promisify and await file upload streams in resolvers or the server will send a response to the client before uploads are complete, causing a disconnect. — https://github.com/jaydenseric/graphql-upload/tree/v16.0.2#tips