apollo-server: Subscriptions using built-in SubscriptionServer error: client gets "Cannot read property 'headers' of undefined"

The server sees client connecting successfully, but the client gets the error, even if nothing is published from server.

Meteor’s WebApp is the HTTP server.

  import { ApolloServer } from 'apollo-server-express'
  import { typeDefs } from './schema.js'
  import { resolvers } from './resolvers.js'
  import { DSBooks } from "./DSBooks.js"
  import { getUser } from 'meteor/apollo'

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    dataSources: () => ({
      dsBooks: new DSBooks()
    }),
    context: async ({ req }) => ({ user: await getUser(req.headers.authorization) }),
    uploads: false,
    subscriptions: {
      path: "/subscriptions",
      onConnect: async (connectionParams, webSocket, context) => {
        console.log(`Subscription client connected using Apollo server's built-in SubscriptionServer.`)
      },
      onDisconnect: async (webSocket, context) => {
        console.log(`Subscription client disconnected.`)
      }
     }
  })


  import { WebApp } from 'meteor/webapp'
  server.applyMiddleware({ app: WebApp.connectHandlers }) //path option defaults to /graphql
  WebApp.connectHandlers.use('/graphql', (req, res) => { if (req.method === 'GET') res.end() }) // To prevent server-side exception "Can't set headers after they are sent" because GraphQL Playground (accessible in browser via /graphql) sets headers and WebApp also sets headers

  server.installSubscriptionHandlers(WebApp.httpServer)

If I start a new SubscriptionServer instead, there’s no problem, and the client receives subscription data.

  import { ApolloServer } from 'apollo-server-express'
  import { typeDefs } from './schema.js'
  import { resolvers } from './resolvers.js'
  import { DSBooks } from "./DSBooks.js"
  import { getUser } from 'meteor/apollo'

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    dataSources: () => ({
      dsBooks: new DSBooks()
    }),
    context: async ({ req }) => ({ user: await getUser(req.headers.authorization) }),
    uploads: false,
  })


  import { WebApp } from 'meteor/webapp'
  server.applyMiddleware({ app: WebApp.connectHandlers }) //path option defaults to /graphql
  WebApp.connectHandlers.use('/graphql', (req, res) => { if (req.method === 'GET') res.end() }) // To prevent server-side exception "Can't set headers after they are sent" because GraphQL Playground (accessible in browser via /graphql) sets headers and WebApp also sets headers


  import { SubscriptionServer } from 'subscriptions-transport-ws'
  import { execute, subscribe } from 'graphql'
  import { makeExecutableSchema } from 'graphql-tools'

  SubscriptionServer.create(
    {
      schema: makeExecutableSchema({ typeDefs, resolvers }),
      execute,
      subscribe,
      onConnect: async (connectionParams, webSocket) => {
        console.log(`Subscription client connected using new SubscriptionServer.`)
      },
      onDisconnect: async (webSocket, context) => {
        console.log(`Subscription client disconnected.`)
      }
  },
    {
      server: WebApp.httpServer,
      path: "/subscriptions",
    },
  )

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 13
  • Comments: 22 (7 by maintainers)

Most upvoted comments

@almostprogrammer is correct. I structured my ApolloServer code below and it works. for subscription, you don’t use req. u need to use connection details https://github.com/apollographql/graphql-subscriptions/blob/master/.designs/authorization.md

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req, connection }) => {
    if (connection) {
      return {
        ...connection.context,
        pubsub
      };
    } else {
      const token = req.headers["authorization"] || "";
      return {
        User,
        Tweet,
        Comment,
        pubsub,
        userId: await getUser(token)
      };
    }
  }
});

This is still an issue for most people who setup auth with subscriptions. It’s possible to get it working but really not as easy as it should be. Please see #2315 for proposed api change. Other related issue: #1597

This is a running sandbox showing the problem: https://codesandbox.io/s/apollo-server-qthfh?fontsize=14

Is this dead? I’m having issues with receiving the connection param from context. I’m using websockets for auth If that’s relevent.

I still get this problem. Has anyone tried to fix this?

Should you use req when using subscriptions? Docs point out that you should use connection to create context from a subscription https://www.apollographql.com/docs/apollo-server/v2/features/subscriptions.html#Context-with-Subscriptions

This problem still exists. Please fix it.

@StefanFeederle that seems like the wrong issue number, mind checking it?

@helfer to make matters worse, https://github.com/apollographql/apollo-server/blob/master/packages/apollo-server-core/src/ApolloServer.ts#L480 overwrites the context returned by subscriptions: { onConnect: async (connectionParams: any) => { ... } } that is recommended in the docs on the website.

This is a major issue. It seems like the developers working on the ApolloServer 2 refactor weren’t all on the same page?

It looks like returning ...connection.context fixed the problem, so thank you @myhendry for the code.

Though I still needed to add

subscriptions: {
  path: "/subscriptions"

in the ApolloServer arguments object, otherwise I think the built-in subscription server wouldn’t start.

So my code is now:

  import { ApolloServer } from 'apollo-server-express'
  import { typeDefs } from './schema.js'
  import { resolvers } from './resolvers.js'
  import { DSBooks } from "./DSBooks.js"
  import { getUser } from 'meteor/apollo'

  const server = new ApolloServer({
    typeDefs,
    resolvers,
    dataSources: () => ({
      dsBooks: new DSBooks()
    }),
    context: async ({ req, connection }) => {
    if (connection) {
      return {
        ...connection.context
      };
    }
    uploads: false,
    subscriptions: {
      path: "/subscriptions",
      onConnect: async (connectionParams, webSocket, context) => {
        console.log(`Subscription client connected using Apollo server's built-in SubscriptionServer.`)
      },
      onDisconnect: async (webSocket, context) => {
        console.log(`Subscription client disconnected.`)
      }
     }
  })

  import { WebApp } from 'meteor/webapp'
  server.applyMiddleware({ app: WebApp.connectHandlers }) //path option defaults to /graphql
  WebApp.connectHandlers.use('/graphql', (req, res) => { if (req.method === 'GET') res.end() }) // To prevent server-side exception "Can't set headers after they are sent" because GraphQL Playground (accessible in browser via /graphql) sets headers and WebApp also sets headers

  server.installSubscriptionHandlers(WebApp.httpServer)

Code showing ApolloClient creation:

    import ApolloClient from 'apollo-client'
    import { ApolloLink } from 'apollo-link'
    import { MeteorAccountsLink } from 'meteor/apollo'
    import { HttpLink } from 'apollo-link-http'
    import { WebSocketLink } from 'apollo-link-ws'
    import { InMemoryCache } from 'apollo-cache-inmemory'
    import { Meteor } from "meteor/meteor"

    const apolloClient = new ApolloClient({
      link: ApolloLink.split( // split based on operation type
        ({ query }) => {
          import { getMainDefinition } from 'apollo-utilities'
          const { kind, operation } = getMainDefinition(query)
          return kind === 'OperationDefinition' && operation === 'subscription'
        },
        new WebSocketLink({
          uri: `${Meteor.absoluteUrl("/subscriptions").replace("http", "ws")}`,
          options: { reconnect: true }
        }),
        ApolloLink.from([
          new MeteorAccountsLink(),
          new HttpLink({ uri: '/graphql' })
        ]),
      ),
      cache: new InMemoryCache()
    })