Skip to content

Test runner hangs with RangeError: Maximum call stack size exceeded #4346

@souphan-adsk

Description

@souphan-adsk

Current behavior:

The following can be experienced when running cypress headlessly on a particular error.

RangeError: Maximum call stack size exceeded
    at _hasBinary (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/socket/node_modules/has-binary/index.js:25:22)
    at _hasBinary (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/socket/node_modules/has-binary/index.js:49:63)
    at _hasBinary (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/socket/node_modules/has-binary/index.js:49:63)
    ...
    at hasBinary (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/socket/node_modules/has-binary/index.js:58:10)
    at /root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/socket/node_modules/socket.io/lib/socket.js:373:16
    at /root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/lib/socket.js:312:22
    at tryCatcher (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/promise.js:510:31)
    at Promise._settlePromise (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/promise.js:567:18)
    at Promise._settlePromise0 (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/promise.js:612:10)
    at Promise._settlePromises (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/promise.js:687:18)
    at Async._drainQueue (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/async.js:133:16)
    at Async._drainQueues (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/async.js:143:10)
    at Immediate.Async.drainQueues (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/bluebird/js/release/async.js:17:14)
    at runCallback (timers.js:789:20)
    at tryOnImmediate (timers.js:751:5)
    at processImmediate [as _immediateCallback] (timers.js:722:5)

This effectively hangs the process.

Desired behavior:

This should not happen.

Steps to reproduce: (app code and test code)

It is consistent in our test suite, but it seems that giving a minimally reproducible case is not easy, as merely changing the order of tests will not yield this error.

That said, tracing back the issue, here is what has been found:

The library request-promise-core on error throws the following Error object on a particular error (in our case ECONNREFUSED).

    { 
       name: 'RequestError',
       stack: 'RequestError: Error: connect ECONNREFUSED 0.0.0.0:443\n\
           at new RequestError (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/request-promise-core/lib/errors.js:14:15)\n\
           at Request.plumbing.callback (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/request-promise-core/lib/plumbing.js:87:29)\n\
           at Request.RP$callback [as _callback] (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/request-promise-core/lib/plumbing.js:46:31)\n\
           at self.callback (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/request/request.js:185:22)\n\
           at emitOne (events.js:116:13)\n\
           at Request.emit (events.js:211:7)\n\
           at Request.onRequestError (/root/.cache/Cypress/3.3.1/Cypress/resources/app/packages/server/node_modules/request/request.js:884:8)\n\
           at emitOne (events.js:121:20)\n\
           at ClientRequest.emit (events.js:211:7)\n\
           at TLSSocket.socketErrorListener (_http_client.js:387:9)\n\
           at emitOne (events.js:116:13)\n\
           at TLSSocket.emit (events.js:211:7)\n\
           at emitErrorNT (internal/streams/destroy.js:64:8)\n\
           at _combinedTickCallback (internal/process/next_tick.js:138:11)\n\
           at process._tickCallback (internal/process/next_tick.js:180:9)\n\
           ',
       message: 'Error: connect ECONNREFUSED 0.0.0.0:443',
       cause:
        { Error: connect ECONNREFUSED 0.0.0.0:443
    at Object._errnoException (util.js:1024:11)
    at _exceptionWithHostPort (util.js:1046:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1182:14)

          code: 'ECONNREFUSED',
          errno: 'ECONNREFUSED',
          syscall: 'connect',
          address: '0.0.0.0',
          port: 443 },
       error:
        { Error: connect ECONNREFUSED 0.0.0.0:443
    at Object._errnoException (util.js:1024:11)
    at _exceptionWithHostPort (util.js:1046:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1182:14)

          code: 'ECONNREFUSED',
          errno: 'ECONNREFUSED',
          syscall: 'connect',
          address: '0.0.0.0',
          port: 443 },
       options:
        { timeout: 90000,
          agent:
           CombinedAgent {
             familyCache: { 'localhost': 4 },
             httpAgent:
              HttpAgent {
                domain: null,
                _events: { free: [Function] },
                _eventsCount: 1,
                _maxListeners: undefined,
                defaultPort: 80,
                protocol: 'http:',
                options: { keepAlive: true, path: null },
                requests: {},
                sockets: {},
                freeSockets: {},
                keepAliveMsecs: 1000,
                keepAlive: true,
                maxSockets: Infinity,
                maxFreeSockets: 256,
                httpsAgent:
                 Agent {
                   domain: null,
                   _events: { free: [Function] },
                   _eventsCount: 1,
                   _maxListeners: undefined,
                   defaultPort: 443,
                   protocol: 'https:',
                   options: { keepAlive: true, path: null },
                   requests: {},
                   sockets: {},
                   freeSockets: {},
                   keepAliveMsecs: 1000,
                   keepAlive: true,
                   maxSockets: Infinity,
                   maxFreeSockets: 256,
                   maxCachedSessions: 100,
                   _sessionCache: { map: {}, list: [] } } },
             httpsAgent:
              HttpsAgent {
                domain: null,
                _events: { free: [Function] },
                _eventsCount: 1,
                _maxListeners: undefined,
                defaultPort: 443,
                protocol: 'https:',
                options: { keepAlive: true, path: null },
                requests: {},
                sockets: {},
                freeSockets:
                 { 'localhost:443::4::::::false::':
                    [ TLSSocket {
                        _tlsOptions:
                         { pipe: false,
                           secureContext: SecureContext { context: SecureContext {} },
                           isServer: false,
                           requestCert: true,
                           rejectUnauthorized: false,
                           session: undefined,
                           NPNProtocols: undefined,
                           ALPNProtocols: undefined,
                           requestOCSP: undefined },
                        _secureEstablished: true,
                        _securePending: false,
                        _newSessionPending: false,
                        _controlReleased: true,
                        _SNICallback: null,
                        servername: null,
                        npnProtocol: false,
                        alpnProtocol: false,
                        authorized: true,
                        authorizationError: null,
                        encrypted: true,
                        _events:
                         { close:
                            [ [Function],
                              { [Function: bound onceWrapper] listener: [Function] },
                              [Function: onClose] ],
                           end: { [Function: bound onceWrapper] listener: [Function: onend] },
                           finish: [Function: onSocketFinish],
                           _socketEnd: [Function: onSocketEnd],
                           secure: [Function],
                           free: [Function: onFree],
                           agentRemove: [Function: onRemove],
                           drain: [Function: ondrain],
                           error: { [Function: bound onceWrapper] listener: [Function: freeSocketErrorListener] } },
                        _eventsCount: 9,
                        connecting: false,
                        _hadError: false,
                        _handle:
                         TLSWrap {
                           _parent:
                            TCP {
                              reading: [Getter/Setter],
                              owner: [Circular],
                              onread: null,
                              onconnection: null,
                              writeQueueSize: 0 },
                           _parentWrap: undefined,
                           _secureContext: SecureContext { context: SecureContext {} },
                           reading: true,
                           owner: [Circular],
                           onread: [Function: onread],
                           writeQueueSize: 0,
                           onhandshakestart: [Function],
                           onhandshakedone: [Function],
                           onocspresponse: [Function],
                           onerror: [Function] },
                        _parent: null,
                        _host: 'localhost',
                        _readableState:
                         ReadableState {
                           objectMode: false,
                           highWaterMark: 16384,
                           buffer: BufferList { head: null, tail: null, length: 0 },
                           length: 0,
                           pipes: null,
                           pipesCount: 0,
                           flowing: true,
                           ended: false,
                           endEmitted: false,
                           reading: true,
                           sync: false,
                           needReadable: true,
                           emittedReadable: false,
                           readableListening: false,
                           resumeScheduled: false,
                           destroyed: false,
                           defaultEncoding: 'utf8',
                           awaitDrain: 0,
                           readingMore: false,
                           decoder: null,
                           encoding: null },
                        readable: true,
                        domain: null,
                        _maxListeners: undefined,
                        _writableState:
                         WritableState {
                           objectMode: false,
                           highWaterMark: 16384,
                           finalCalled: false,
                           needDrain: false,
                           ending: false,
                           ended: false,
                           finished: false,
                           destroyed: false,
                           decodeStrings: false,
                           defaultEncoding: 'utf8',
                           length: 0,
                           writing: false,
                           corked: 0,
                           sync: false,
                           bufferProcessing: false,
                           onwrite: [Function: bound onwrite],
                           writecb: null,
                           writelen: 0,
                           bufferedRequest: null,
                           lastBufferedRequest: null,
                           pendingcb: 0,
                           prefinished: false,
                           errorEmitted: false,
                           bufferedRequestCount: 0,
                           corkedRequestsFree:
                            { next: null,
                              entry: null,
                              finish: [Function: bound onCorkedFinish] } },
                        writable: true,
                        allowHalfOpen: false,
                        _bytesDispatched: 348,
                        _sockname: null,
                        _pendingData: null,
                        _pendingEncoding: '',
                        server: undefined,
                        _server: null,
                        ssl:
                         TLSWrap {
                           _parent:
                            TCP {
                              reading: [Getter/Setter],
                              owner: [Circular],
                              onread: null,
                              onconnection: null,
                              writeQueueSize: 0 },
                           _parentWrap: undefined,
                           _secureContext: SecureContext { context: SecureContext {} },
                           reading: true,
                           owner: [Circular],
                           onread: [Function: onread],
                           writeQueueSize: 0,
                           onhandshakestart: [Function],
                           onhandshakedone: [Function],
                           onocspresponse: [Function],
                           onerror: [Function] },
                        _requestCert: true,
                        _rejectUnauthorized: false,
                        parser: null,
                        _httpMessage: null,
                        read: [Function],
                        _consuming: true,
                        _idleTimeout: -1,
                        _idleNext: null,
                        _idlePrev: null,
                        _idleStart: 41856,
                        _destroyed: false,
                        [Symbol(asyncId)]: -1,
                        [Symbol(bytesRead)]: 0,
                        [Symbol(asyncId)]: 3173,
                        [Symbol(triggerAsyncId)]: 3163 } ] },
                keepAliveMsecs: 1000,
                keepAlive: true,
                maxSockets: Infinity,
                maxFreeSockets: 256,
                maxCachedSessions: 100,
                _sessionCache:
                 { map:
                    { 'localhost:443::4::::::false::': <Buffer 30 82 08 3c 02 01 01 02 02 03 03 04 02 c0 2f 04 20 e7 f6 ff ce 61 67 f3 90 b7 4f 56 d1 a5 0f 5f 09 65 50 61 20 74 54 db d8 bb c9 59 f6 d7 c4 e4 46 04 ... > },
                   list:
                    [ 'localhost:443::4::::::false::' ] } } },
          headers:
           { Connection: 'keep-alive',
             'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/3.3.1 Chrome/61.0.3163.100 Electron/2.0.18 Safari/537.36',
             accept: '*/*' },
          proxy: null,
          url: 'https://localhost/',
          method: 'GET',
          gzip: true,
          followRedirect: [Function],
          failOnStatusCode: false,
          retryOnNetworkFailure: true,
          retryOnStatusCodeFailure: false,
          jar:
           { _jar: CookieJar { enableLooseMode: true, store: { idx: {} } },
             toJSON: [Function: toJSON],
             setCookie: [Function: setCookie],
             getCookieString: [Function: getCookieString],
             getCookies: [Function: getCookies] },
          cookies: true,
          strictSSL: false,
          simple: false,
          resolveWithFullResponse: true,
          followAllRedirects: true,
          requestId: 'request3',
          retryIntervals: [ 0, 1000, 2000, 2000 ],
          delaysRemaining: [],
          callback: [Function: RP$callback],
          transform: undefined,
          transform2xxOnly: false },
       response: undefined
    }

The important part is that this object contains circular references:

    owner: [Circular],

On request error, this object is sent to socket.io's callback function
https://github.com/cypress-io/cypress/blob/develop/packages/server/lib/socket.coffee#L315-L319

This is a problem because Socket.io scans arguments for binary data that doesn't deal with circular reference.
https://github.com/socketio/socket.io/blob/master/lib/socket.js#L374-L391
https://github.com/expanse-org/socketio-app/blob/master/node_modules/has-binary2/index.js

There is already some filtering done to errors captured and raised back to the execution flow.
https://github.com/cypress-io/cypress/blob/develop/packages/server/lib/errors.coffee#L808-L828

An additional operation to would be to make sure that it doesn't contain circular reference.
I'd suggest using a library like fclone for this.
https://github.com/soyuka/fclone

Versions

  • Cypress 3.3.1
  • Electron 61 (headless)
  • CentOS 7

Note: OSX with electron or chrome did not reproduce the issue.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions