Apollo-server: `apollo-server`'s stop() hangs when running in jest jsdom browser-emulation mode due to lack of timer.unref

Created on 16 Feb 2021  路  4Comments  路  Source: apollographql/apollo-server

  • I write tests uning Jest + react testing library + jsdom 15. In one of the tests I use ApolloServer - I start it beforeAll and stop it after all.
    When I used apollo-server version 2.19.0 everything worked as expected. When upgrading to apollo-server version 2.21.0,
    server.stop() hangs forever. I try using stopGracePeriodMillis and that doesn't work as well.

  • [ apollo-server version 2.21.0 ] The package name and version of Apollo showing the problem.

  • [ apollo-server version 2.19.0 ] The last version of Apollo where the problem did _not_ occur.
  • [ server.stop() should work and stop the server ] The expected behavior.
  • [ instead It's stuck and never reaches the following code ] The actual behavior.
  • A simple, runnable reproduction!
import React from 'react';
import { gql } from '@apollo/client';
import { ApolloServer } from 'apollo-server';

// Just a random schema
const typeDefs = gql`  
  type Query {
    books: [Book]!
  }
  type Book {
    title: String
    author: Author
  }
  type Author {
    name: String
    books: [Book]
  }
`;

let mockServer;

beforeAll(async () => {
  mockServer = new ApolloServer({typeDefs});
  try {
    const { url } = await mockServer.listen({
      port: 0
    });
    console.log(`馃殌 Server ready at ${url}`);
    return url;
  } catch (error) {
    console.error('Error running server', error);
    throw error;
  }
});

afterAll(async () => {
  await mockServer?.stop();
  console.log('stopped server'); // This line is never reached
});

describe('VesselName', () => {
  it('Does nothing', async () => {
  });
});

My guess is that this change broke the stop functionality, maybe because the code runs from jsdom browser + jest, and stoppable might have issues with it.

Update:
I think my guess is correct, I found this in the jsdom browser console:

  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:290
    TypeError: setTimeout(...).unref is not a function
        at Immediate.<anonymous> (/Users/nogashimoni/Documents/projects/javascript/marint-ui/node_modules/stoppable/lib/stoppable.js:43:39)
        at processImmediate (internal/timers.js:461:21)

Thank you!

Most helpful comment

Update - it seems that the stoppable code can't run from jsdom.

As a temp solution, I added a polyfill for setTimeout(...).unref, like so:

const setTimeoutProto = window.setTimeout().__proto__; if (!setTimeoutProto.unref) { setTimeoutProto.unref = function () {}; }

It's not a good solution IMO, but it makes server.stop() work and that's good enough for me for now.

All 4 comments

When I run the code not from jest, everything works fine and the server stops, so I assume it's related to the fact that I run this from Jest + react testing library, and that the code runs from jsdom .

(This works perfectly fine:

const React = require('react')
const { gql }  = require( '@apollo/client')
const { ApolloServer } = require('apollo-server');

// Just a random schema
const typeDefs = gql`  
  type Query {
    books: [Book]!
  }
  type Book {
    title: String
    author: Author
  }
  type Author {
    name: String
    books: [Book]
  }
`;

async function demo() {
  mockServer = new ApolloServer({typeDefs});
  try {
    const { url } = await mockServer.listen({
      port: 0
    });
    console.log(`馃殌 Server ready at ${url}`);
  } catch (error) {
    console.error('Error running server', error);
    throw error;
  }
  await mockServer?.stop();
  console.log('stopped server'); // This line is reached when not running from Jest
}

demo();

)

Update - it seems that the stoppable code can't run from jsdom.

As a temp solution, I added a polyfill for setTimeout(...).unref, like so:

const setTimeoutProto = window.setTimeout().__proto__; if (!setTimeoutProto.unref) { setTimeoutProto.unref = function () {}; }

It's not a good solution IMO, but it makes server.stop() work and that's good enough for me for now.

Hi, I appreciate the test file but since it sounds like this issue is very specific to the particular way you run jest with jsdom, is it possible to give a bit more context on how to run it? eg maybe as a git repo I can clone, or something like codesandbox.io?

If the issue is unref, then one workaround would be to pass stopGracePeriodMillis: Infinity to new ApolloServer. This means that the new stoppable functionality will still kill idle HTTP connections but just won't kill ones that are actively running an HTTP request. For a test, this actually seems like a reasonable approach since your tests probably won't end with active HTTP requests (and if they do, Jest's timeouts will kick in?).

I'm definitely also open to forking stoppable (either into @apollographql/stoppable or just copying the single file into this repo). I think we could replace the use of unref by calling clearTimeout from the server.close callback.

Let's see if upstream takes https://github.com/hunterloftis/stoppable/pull/33
In the meantime, let me know if stopGracePeriodMillis: Infinity is a reasonable workaround.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

leinue picture leinue  路  3Comments

manuelfink picture manuelfink  路  3Comments

disyam picture disyam  路  3Comments

attdona picture attdona  路  3Comments

jpcbarros picture jpcbarros  路  3Comments