Troubles to implement Apollo Server Subscriptions in Next.js

Viewed 64

I've been trying to implement a websocket server with Apollo Server in Next.js for 5 days now and still can't get anything to work. I tried a lot of different implementations and the one i was close to get the job works was with apollo-server-micro package. Close but still not working... I drop you bellow my config of Apollo Server and Apollo Client, and the resolver i'm using for the subscription.

src/pages/api/graphql.ts

import { ApolloServer } from 'apollo-server-micro';
import { useServer } from 'graphql-ws/lib/use/ws';
import { WebSocketServer } from 'ws';
import resolvers from '@/graphql/resolvers/resolvers';
import typeDefs from '@/graphql/schemas/typeDefs';
import { prisma } from '@/server/prismaClient';
import { makeExecutableSchema } from '@graphql-tools/schema';

const schema = makeExecutableSchema({ typeDefs, resolvers });

export const config = {
  api: {
    bodyParser: false,
    externalResolver: true,
  },
};

// Apollo Server --------------------------------------------------------------
const apolloServer = new ApolloServer({
  schema,
  cache: 'bounded',
  context({ req }) {
    const { socket } = req;
    return { pubsub: socket?.server?.pubsub, prisma };
  },
  plugins: [
    {
      async serverWillStart() {
        return {
          async drainServer() {
            console.log(`ApolloServer: drain server :>>>>>>>>>>>>>>>>>>`);
            if (graphqlWSS) {
              console.log(`ApolloServer: disposing WSS :>>>>>>>>>>>>>>>>>>`);
              await graphqlWSS.dispose();
            }
          },
        };
      },
    },
  ],
});

const startServer = apolloServer.start();
let graphqlWSS;
let apolloServerHandler;

const handler = async (req: any, res: any) => {
  if (req.method === 'OPTIONS') {
    return res.end();
  }

  const oldApolloServer = res.socket.server.apolloServer;

  if (oldApolloServer && oldApolloServer !== apolloServer) {
    console.warn('Fixing Apollo Server hot reload');
    oldApolloServer.stop();
    delete res.socket.server.apolloServer;
  }

  if (!res.socket.server.apolloServer) {
    res.socket.server.apolloServer = apolloServer;

    if (!graphqlWSS) {
      /* eslint-disable react-hooks/rules-of-hooks */
      console.log(`Initializing GraphQL WSS :>>>>>>>>>>>>>>>>>>`);
      const wss = new WebSocketServer({
        server: res.socket.server,
        path: '/api/graphql',
      });

      graphqlWSS = useServer(
        {
          schema,
          context() {
            const { socket } = req;
            return { pubsub: socket.server.pubsub };
          },
        },
        wss
      );
    }

    await startServer;

    apolloServerHandler = apolloServer.createHandler({ path: '/api/graphql' });
  }

  await apolloServerHandler(req, res);
};

export default handler;

src/pages/_app.tsx

import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import { SessionProvider } from 'next-auth/react';
import Router from 'next/router';
import NProgress from 'nprogress';
import { Provider } from 'react-redux';
import { ToastContainer } from 'react-toastify';

import { HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

import store from '../redux/store';
import { hmtTheme } from '../styles/theme';

// Styles
import 'nprogress/nprogress.css';
import 'react-toastify/dist/ReactToastify.css';
import 'tippy.js/dist/tippy.css';
import '../styles/globals.css';

const httpLink = new HttpLink({
  uri: '/api/graphql',
});

const wsLink =
  typeof window !== 'undefined'
    ? new GraphQLWsLink(
        createClient({
          url: 'ws://localhost:3000/api/graphql',
        })
      )
    : null;

const link =
  typeof window !== 'undefined' && wsLink != null
    ? split(
        ({ query }) => {
          const def = getMainDefinition(query);
          return (
            def.kind === 'OperationDefinition' &&
            def.operation === 'subscription'
          );
        },
        wsLink,
        httpLink
      )
    : httpLink;

const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      ChatConversation: {
        fields: {
          messages: {
            merge(
              existing: any[],
              incoming: any[],
              { readField, mergeObjects }
            ) {
              const merged: any[] = existing ? existing.slice(0) : [];
              const messageToIndex: Record<string, number> =
                Object.create(null);
              if (existing) {
                existing.forEach((message, index) => {
                  messageToIndex[readField<string>('id', message)] = index;
                });
              }
              incoming.forEach((message) => {
                const id = readField<string>('id', message);
                const index = messageToIndex[id];
                if (typeof index === 'number') {
                  // Merge the new author data with the existing author data.
                  merged[index] = mergeObjects(merged[index], message);
                } else {
                  // First time we've seen this author in this array.
                  messageToIndex[id] = merged.length;
                  merged.push(message);
                }
              });
              return merged;
            },
          },
        },
      },
    },
  }),
  link,
  uri: '/api/graphql',
});

NProgress.configure({ showSpinner: false });

Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());

function App({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <ApolloProvider client={client}>
      <SessionProvider session={session}>
        <Provider store={store}>
          <ThemeProvider theme={hmtTheme}>
            <ToastContainer
              containerId="app"
              draggable={false}
              position="bottom-right"
              toastStyle={{ backgroundColor: '#252525', color: '#ecf0f1' }}
            />
            <CssBaseline />
            <Component {...pageProps} />
          </ThemeProvider>
        </Provider>
      </SessionProvider>
    </ApolloProvider>
  );
}

export default App;

src/server/graphql/resolvers/chatMessageResolver.ts

import { pubsub } from '@/server/pubSubInstanciate';
import { CreateChatMessageInput } from '../types';
import { ChatMessageRepository } from './../../repositories/chatMessageRepository';

const chatMessageRep = new ChatMessageRepository();

const chatMessageResolver = {
  Query: {
    chatMessages: async (
      _: undefined,
      _args: undefined,
      _parent: undefined
    ) => {
      return await chatMessageRep.findAll();
    },
    chatMessagesByUserId: async (
      _: undefined,
      { userId }: { userId: string },
      _parent: undefined
    ) => {
      return await chatMessageRep.findByUserId(userId);
    },
  },
  Mutation: {
    createChatMessage: async (
      _: undefined,
      {
        userId,
        createChatMessageInput,
      }: { userId: string; createChatMessageInput: CreateChatMessageInput },
      ctx: any
    ) => {
      const chatMessage = await chatMessageRep.create(
        userId,
        createChatMessageInput
      );
      pubsub.publish('CHAT_MESSAGE_CREATED', {
        chatMessageCreated: chatMessage,
      });
      return chatMessage;
    },
  },
  Subscription: {
    chatMessageCreated: {
      subscribe: (_: undefined, _args: undefined, ctx: any) =>
        pubsub.asyncIterator(['CHAT_MESSAGE_CREATED']),
      resolve: (payload) => {
        return payload && payload !== null;
      },
    },
  },
};

export default chatMessageResolver;

I also tried this following command to get some informations about my endpoint and this is what it returns

npx diagnose-endpoint@1.1.0 --endpoint=http://localhost:3000/api/graphql

Diagnosing http://localhost:3000/api/graphql
⚠️  OPTIONS response is missing header 'access-control-allow-methods: POST'
⚠️  POST response missing 'access-control-allow-origin' header.
If using cookie-based authentication, the following headers are required from your endpoint: 
    access-control-allow-origin: https://studio.apollographql.com
    access-control-allow-credentials: true
Otherwise, a wildcard value would work:
    access-control-allow-origin: *
( Interested in previewing a local tunnel to bypass CORS requirements? Please let us know at https://docs.google.com/forms/d/e/1FAIpQLScUCi3PdMerraiy6GpD-QiC_9KEKVHr4oDL5Vef5fIvzqqQWg/viewform )

Thank you a lot in advance to people can help me. I'm really breaking my head right now...

1 Answers

try this

res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
if (req.method === 'OPTIONS') {
  return res.end();
}
Related