Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apollo Server with subscriptions used inside next.js api routes: websockets trouble

i try to setup GraphQL Subscriptions inside a next.js 9.x app. The app is totally fake, it is just for trying Apollo Server subscriptions. The "database" is just an array, where I push new users to it.

This is the code I got so far.

import { ApolloServer, gql, makeExecutableSchema } from "apollo-server-micro"
import { PubSub } from "apollo-server"

const typeDefs = gql`
  type User {
    id: ID!
    name: String
    status: String
  }


  type Query {
    users: [User!]!
    user(id: ID!): User
  }

  type Mutation {
    addUser(id: String, name: String, status: String): User
  }

  type Subscription {
    newUser: User!
  }
`

const fakedb = [
  {
    id: "1",
    name: "myname",
    status: "active",
  },
]

const NEW_USER = "NEW_USER"

const resolvers = {
  Subscription: {
    newUser: {
      subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(NEW_USER),
    },
  },

  Query: {
    users: (parent, args, context) => {
      console.log(context)

      return fakedb
    },
    user: (_, { id }) => {
      console.log(id)
      console.log(fakedb)

      return fakedb.find((user) => user.id == id)
    },
  },
  Mutation: {
    addUser(_, { id, name, status }, { pubsub }) {
      console.log(pubsub)

      const newUser = {
        id,
        name,
        status,
      }

      pubsub.publish(NEW_USER, { newUser: newUser })

      fakedb.push(newUser)
      return newUser
    },
  },
}

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

const pubsub = new PubSub()
const apolloServer = new ApolloServer({
  // typeDefs,
  // resolvers,
  schema,
  context: ({ req, res }) => {
    return { req, res, pubsub }
  },
  introspection: true,
  subscriptions: {
    path: "/api/graphql",
    // keepAlive: 15000,
    onConnect: () => console.log("connected"),
    onDisconnect: () => console.log("disconnected"),
  },
})

export const config = {
  api: {
    bodyParser: false,
  },
}

export default apolloServer.createHandler({ path: "/api/graphql" })

I run this subscription in localhost:3000/api/graphql:

subscription { newUser { id name } }

I get this error. I am not sure, where and how to fix this, as I can not find any documentation about this.

{ "error": "Could not connect to websocket endpoint ws://localhost:3000/api/graphql. Please check if the endpoint url is correct." }

I found out how to add the subscriptions path, as it complained about that before (was /graphql before). But still not working.

like image 901
Vorname Nachname Avatar asked Dec 17 '22 13:12

Vorname Nachname


2 Answers

This is how I made it work.

import { ApolloServer } from 'apollo-server-micro';
import schema from './src/schema';

const apolloServer = new ApolloServer({
  schema,
  context: async ({ req, connection }) => {
    if (connection) {
      // check connection for metadata
      return connection.context;
    }
    // get the user from the request
    return {
      user: req.user,
      useragent: req.useragent,
    };
  },

  subscriptions: {
    path: '/api/graphqlSubscriptions',
    keepAlive: 9000,
    onConnect: console.log('connected'),
    onDisconnect: () => console.log('disconnected'),
  },
  playground: {
    subscriptionEndpoint: '/api/graphqlSubscriptions',

    settings: {
      'request.credentials': 'same-origin',
    },
  },
});

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

const graphqlWithSubscriptionHandler = (req, res, next) => {
  if (!res.socket.server.apolloServer) {
    console.log(`* apolloServer first use *`);

    apolloServer.installSubscriptionHandlers(res.socket.server);
    const handler = apolloServer.createHandler({ path: '/api/graphql' });
    res.socket.server.apolloServer = handler;
  }

  return res.socket.server.apolloServer(req, res, next);
};

export default graphqlWithSubscriptionHandler;

Just make sure that the websocket path works. https://www.websocket.org/echo.html

like image 171
ordepim Avatar answered Dec 28 '22 15:12

ordepim


I have inspired by @ordepim's answer and I fixed Hot-reload problem this way (I also added typings):

import { ApolloServer } from 'apollo-server-micro'
import { NextApiRequest, NextApiResponse } from 'next'
import { schema } from '../../lib/schema'

//note: this log occurs on every hot-reload
console.log('CREATING APOLLOSERVER ')

const apolloServer = new ApolloServer({
  schema,
  context: async ({ req, connection }) => {
    if (connection) {
      // check connection for metadata
      return connection.context
    }
    // get the user from the request
    return {
      user: req.user,
      useragent: req.useragent,
    }
  },

  subscriptions: {
    path: '/api/graphqlSubscriptions',
    keepAlive: 9000,
    onConnect: () => console.log('connected'),
    onDisconnect: () => console.log('disconnected'),
  },
  playground: {
    subscriptionEndpoint: '/api/graphqlSubscriptions',

    settings: {
      'request.credentials': 'same-origin',
    },
  },
})
export const config = {
  api: {
    bodyParser: false,
  },
}

type CustomSocket = Exclude<NextApiResponse<any>['socket'], null> & {
  server: Parameters<ApolloServer['installSubscriptionHandlers']>[0] & {
    apolloServer?: ApolloServer
    apolloServerHandler?: any
  }
}

type CustomNextApiResponse<T = any> = NextApiResponse<T> & {
  socket: CustomSocket
}

const graphqlWithSubscriptionHandler = (
  req: NextApiRequest,
  res: CustomNextApiResponse
) => {
  const oldOne = res.socket.server.apolloServer
  if (
    //we need compare old apolloServer with newOne, becasue after hot-reload are not equals
    oldOne &&
    oldOne !== apolloServer
  ) {
    console.warn('FIXING HOT RELOAD !!!!!!!!!!!!!!! ')
    delete res.socket.server.apolloServer
  }

  if (!res.socket.server.apolloServer) {
    console.log(`* apolloServer (re)initialization *`)

    apolloServer.installSubscriptionHandlers(res.socket.server)
    res.socket.server.apolloServer = apolloServer
    const handler = apolloServer.createHandler({ path: '/api/graphql' })
    res.socket.server.apolloServerHandler = handler
    //clients losts old connections, but clients are able to reconnect
    oldOne?.stop()
  }

  return res.socket.server.apolloServerHandler(req, res)
}

export default graphqlWithSubscriptionHandler

like image 38
user2958194 Avatar answered Dec 28 '22 14:12

user2958194