node.jsexpressherokugraphqlexpress-graphql

Why my GraphQL subscriptions are working on local server but not when deployed on live server


I am building a GraphQL server and have subscriptions in it. Once I deploy the server on my local machine and check the subscription in Playground it works, i.e. it listens to events and I get the newly added data. It means the subscriptions implementation is correct and there is nothing wrong with them. The subscriptions are working fine when I'm running on local (i.e localhost) but when I deploy the server on live (Heroku) it gives the below error when I listen to subscriptions in Playground:

{
  "error": "Could not connect to websocket endpoint wss://foodesk.herokuapp.com/graphql. Please check if the endpoint url is correct."
}

Just for more information, my queries and mutations are also working on live server, it's just the subscriptions that are not working.

This is my code:

/**
 * third party libraries
 */
const bodyParser = require('body-parser');
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const helmet = require('helmet');
const http = require('http');
const mapRoutes = require('express-routes-mapper');
const mkdirp = require('mkdirp');
const shortid = require('shortid');
/**
 * server configuration
 */
const config = require('../config/');
const auth = require('./policies/auth.policy');
const dbService = require('./services/db.service');
const { schema } = require('./graphql');

// environment: development, testing, production
const environment = process.env.NODE_ENV;


const graphQLServer = new ApolloServer({
  schema,
  uploads: false
});

/**
 * express application
 */
const api = express();
const server = http.createServer(api);

graphQLServer.installSubscriptionHandlers(server)

const mappedRoutes = mapRoutes(config.publicRoutes, 'api/controllers/');
const DB = dbService(environment, config.migrate).start();

// allow cross origin requests
// configure to allow only requests from certain origins
api.use(function (req, res, next) {

  // Website you wish to allow to connect
  res.setHeader('Access-Control-Allow-Origin', '*');

  // Request methods you wish to allow
  res.setHeader('Access-Control-Allow-Methods', '*');

  // Request headers you wish to allow
  res.setHeader('Access-Control-Allow-Headers', '*');

  // Pass to next layer of middleware
  next();
});

// secure express app
api.use(helmet({
  dnsPrefetchControl: false,
  frameguard: false,
  ieNoOpen: false,
}));

// parsing the request bodys
api.use(bodyParser.urlencoded({ extended: false }));
api.use(bodyParser.json());

// public REST API
api.use('/rest', mappedRoutes);

// private GraphQL API
api.post('/graphql', (req, res, next) => auth(req, res, next));

graphQLServer.applyMiddleware({
  app: api,
  cors: {
    origin: true,
    credentials: true,
    methods: ['POST'],
    allowedHeaders: [
      'X-Requested-With',
      'X-HTTP-Method-Override',
      'Content-Type',
      'Accept',
      'Authorization',
      'Access-Control-Allow-Origin',
    ],
  },
  playground: {
    settings: {
      'editor.theme': 'light',
    },
  },
});

server.listen(config.port, () => {
  if (environment !== 'production'
    && environment !== 'development'
    && environment !== 'testing'
  ) {
    console.error(`NODE_ENV is set to ${environment}, but only production and development are valid.`);
    process.exit(1);
  }
  return DB;
});


Solution

  • based on your error;

    {
      "error": "Could not connect to websocket endpoint wss://foodesk.herokuapp.com/graphql. Please check if the endpoint url is correct."
    }
    

    Your app is running on heroku app service i.e "herokuapp.com". as far as this service is concerned, you are not in control of the server configurations.

    in development, you were serving your subscriptions under the "ws://" protocol which is converted to the secure version "wss://" when you deployed to heroku. the solution i have needs you to have access to the server via ssh. this means you upgrade your heroku subscription to have a vm with a dedicated ip address or any other cloud provider for that matter. if you have that in place do the following;

    1. host your app on the vm and serve it, then edit the apache to proxy "yourdomain.com" to serve the app at maybe port 3000 as shown below
    <VirtualHost *:80>
     ServerName yourdomain.com
     ProxyPreserveHost on
     ProxyPass / http://localhost:3000/
     ProxyPassReverse / http://localhost:3000/
    RewriteEngine on
    
    
    </VirtualHost>
    
    1. Go to your domain registrar and add a subdomain through which you will be serving your subscriptions aka web-sockets. this is done by adding a A record with the subdomain name eg "websocket" pointing to your vm ip address. this will make "websocket.yourdomain.com" available for use.

    2. on your server install apache server and include a virtualhost as shown below

    <VirtualHost *:80>
     ServerName websocket.yourdomain.com
     ProxyPreserveHost on
     ProxyPass / ws://localhost:3000/
     ProxyPassReverse / ws://localhost:3000/
    RewriteEngine on
    
    
    </VirtualHost>
    
    1. restart apache server

    at this point, your app is running on "yourdomain.com" which is where all your graphql mutations and queries are also served. but all the subscriptions will be established at "websocket.yourdomain.com" which will proxy all the requests to the "ws://" protocol hence reaching your subscription server without an error.

    N/B Your client side code will use "wss://websocket.yourdomain.com" for subscription connections