mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-23 15:39:28 -05:00
Fix "Parse Error" when performing HTTP requests (#5988)
* Detect if NODE_OPTIONS are present in binary; if not, respawn
* Always reset NODE_OPTIONS, even if no ORIGINAL_
Co-authored-by: Andrew Smith <andrew@andrew.codes>
* Exit with correct code # from stub process
* Clean up based on Brian's feedback
* how process.versions is null, i have no idea, but it is
* add repro for invalid header char
* Always pass NODE_OPTIONS with max-http-header-size (#5452)
* cli: set NODE_OPTIONS=--max-http-header-size=1024*1024 on spawn
* electron: remove redundant max-http-header-size
* server: add useCli option to make e2e tests go thru cli
* server: add test for XHR with body > 100kb via CLI
* clean up conditional
* cli: don't pass --max-http-header-size in dev w node < 11.10
* add original_node_options to restore o.g. user node_options
* force no color
* Revert "Use websockets to stub large XHR response bodies instead of hea… (#5525)"
This reverts commit 249db45363.
* fix yarn.lock
* update 4_xhr_spec snapshot
* make 6_visit_spec reproduce invalid header char error
* pass --http-parser=legacy
* still set headers if an ERR_INVALID_CHAR is raised
* add --http-parser=legacy in some more places
* update http_requests_spec
* readd spawn_spec
* improve debug logging
* remove unnecessary changes
* cleanup
* revert yarn.lock to develop
* use cp.spawn, not cp.fork
to work around the Electron patch: https://github.com/electron/electron/blob/39baf6879011c0fe8cc975c7585567c7ed0aeed8/patches/node/refactor_alter_child_process_fork_to_use_execute_script_with.patch
Co-authored-by: Andrew Smith <andrew@andrew.codes>
This commit is contained in:
@@ -1,3 +1,22 @@
|
||||
exports['lib/exec/spawn .start forces colors and streams when supported 1'] = {
|
||||
"FORCE_COLOR": "1",
|
||||
"DEBUG_COLORS": "1",
|
||||
"MOCHA_COLORS": "1",
|
||||
"FORCE_STDIN_TTY": "1",
|
||||
"FORCE_STDOUT_TTY": "1",
|
||||
"FORCE_STDERR_TTY": "1",
|
||||
"NODE_OPTIONS": "--max-http-header-size=1048576 --http-parser=legacy"
|
||||
}
|
||||
|
||||
exports['lib/exec/spawn .start does not force colors and streams when not supported 1'] = {
|
||||
"FORCE_COLOR": "0",
|
||||
"DEBUG_COLORS": "0",
|
||||
"FORCE_STDIN_TTY": "0",
|
||||
"FORCE_STDOUT_TTY": "0",
|
||||
"FORCE_STDERR_TTY": "0",
|
||||
"NODE_OPTIONS": "--max-http-header-size=1048576 --http-parser=legacy"
|
||||
}
|
||||
|
||||
exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = `
|
||||
The Test Runner unexpectedly exited via a [36mexit[39m event with signal [36mSIGKILL[39m
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
const { onStderrData, electronLogging } = overrides
|
||||
const envOverrides = util.getEnvOverrides()
|
||||
const envOverrides = util.getEnvOverrides(options)
|
||||
const electronArgs = _.clone(args)
|
||||
const node11WindowsFix = isPlatform('win32')
|
||||
|
||||
|
||||
+27
-1
@@ -268,7 +268,7 @@ const util = {
|
||||
return isCi
|
||||
},
|
||||
|
||||
getEnvOverrides () {
|
||||
getEnvOverrides (options = {}) {
|
||||
return _
|
||||
.chain({})
|
||||
.extend(util.getEnvColors())
|
||||
@@ -277,9 +277,35 @@ const util = {
|
||||
.mapValues((value) => { // stringify to 1 or 0
|
||||
return value ? '1' : '0'
|
||||
})
|
||||
.extend(util.getNodeOptions(options))
|
||||
.value()
|
||||
},
|
||||
|
||||
getNodeOptions (options, nodeVersion) {
|
||||
if (!nodeVersion) {
|
||||
nodeVersion = Number(process.versions.node.split('.')[0])
|
||||
}
|
||||
|
||||
if (options.dev && nodeVersion < 12) {
|
||||
// `node` is used instead of Electron when --dev is passed, so this won't work if Node is too old
|
||||
debug('NODE_OPTIONS=--max-http-header-size could not be set because we\'re in dev mode and Node is < 12.0.0')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/5431
|
||||
const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2} --http-parser=legacy`
|
||||
|
||||
if (_.isString(process.env.NODE_OPTIONS)) {
|
||||
return {
|
||||
NODE_OPTIONS: `${NODE_OPTIONS} ${process.env.NODE_OPTIONS}`,
|
||||
ORIGINAL_NODE_OPTIONS: process.env.NODE_OPTIONS || '',
|
||||
}
|
||||
}
|
||||
|
||||
return { NODE_OPTIONS }
|
||||
},
|
||||
|
||||
getForceTty () {
|
||||
return {
|
||||
FORCE_STDIN_TTY: util.isTty(process.stdin.fd),
|
||||
|
||||
@@ -329,14 +329,7 @@ describe('lib/exec/spawn', function () {
|
||||
|
||||
return spawn.start([], { env: {} })
|
||||
.then(() => {
|
||||
expect(cp.spawn.firstCall.args[2].env).to.deep.eq({
|
||||
FORCE_COLOR: '1',
|
||||
DEBUG_COLORS: '1',
|
||||
MOCHA_COLORS: '1',
|
||||
FORCE_STDERR_TTY: '1',
|
||||
FORCE_STDIN_TTY: '1',
|
||||
FORCE_STDOUT_TTY: '1',
|
||||
})
|
||||
snapshot(cp.spawn.firstCall.args[2].env)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -368,13 +361,7 @@ describe('lib/exec/spawn', function () {
|
||||
|
||||
return spawn.start([], { env: {} })
|
||||
.then(() => {
|
||||
expect(cp.spawn.firstCall.args[2].env).to.deep.eq({
|
||||
FORCE_COLOR: '0',
|
||||
DEBUG_COLORS: '0',
|
||||
FORCE_STDERR_TTY: '0',
|
||||
FORCE_STDIN_TTY: '0',
|
||||
FORCE_STDOUT_TTY: '0',
|
||||
})
|
||||
snapshot(cp.spawn.firstCall.args[2].env)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ require('../spec_helper')
|
||||
const os = require('os')
|
||||
const tty = require('tty')
|
||||
const snapshot = require('../support/snapshot')
|
||||
const mockedEnv = require('mocked-env')
|
||||
const supportsColor = require('supports-color')
|
||||
const proxyquire = require('proxyquire')
|
||||
const hasha = require('hasha')
|
||||
@@ -11,6 +12,9 @@ const la = require('lazy-ass')
|
||||
const util = require(`${lib}/util`)
|
||||
const logger = require(`${lib}/logger`)
|
||||
|
||||
// https://github.com/cypress-io/cypress/issues/5431
|
||||
const expectedNodeOptions = `--max-http-header-size=${1024 * 1024} --http-parser=legacy`
|
||||
|
||||
describe('util', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(process, 'exit')
|
||||
@@ -213,6 +217,7 @@ describe('util', () => {
|
||||
FORCE_COLOR: '1',
|
||||
DEBUG_COLORS: '1',
|
||||
MOCHA_COLORS: '1',
|
||||
NODE_OPTIONS: expectedNodeOptions,
|
||||
})
|
||||
|
||||
util.supportsColor.returns(false)
|
||||
@@ -224,10 +229,49 @@ describe('util', () => {
|
||||
FORCE_STDERR_TTY: '0',
|
||||
FORCE_COLOR: '0',
|
||||
DEBUG_COLORS: '0',
|
||||
NODE_OPTIONS: expectedNodeOptions,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.getNodeOptions', () => {
|
||||
let restoreEnv
|
||||
|
||||
afterEach(() => {
|
||||
if (restoreEnv) {
|
||||
restoreEnv()
|
||||
restoreEnv = null
|
||||
}
|
||||
})
|
||||
|
||||
it('adds required NODE_OPTIONS', () => {
|
||||
restoreEnv = mockedEnv({
|
||||
NODE_OPTIONS: undefined,
|
||||
})
|
||||
|
||||
expect(util.getNodeOptions({})).to.deep.eq({
|
||||
NODE_OPTIONS: expectedNodeOptions,
|
||||
})
|
||||
})
|
||||
|
||||
it('includes existing NODE_OPTIONS', () => {
|
||||
restoreEnv = mockedEnv({
|
||||
NODE_OPTIONS: '--foo --bar',
|
||||
})
|
||||
|
||||
expect(util.getNodeOptions({})).to.deep.eq({
|
||||
NODE_OPTIONS: `${expectedNodeOptions} --foo --bar`,
|
||||
ORIGINAL_NODE_OPTIONS: '--foo --bar',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not return if dev is set and version < 12', () => {
|
||||
expect(util.getNodeOptions({
|
||||
dev: true,
|
||||
}, 11)).to.be.undefined
|
||||
})
|
||||
})
|
||||
|
||||
context('.getForceTty', () => {
|
||||
it('forces when each stream is a tty', () => {
|
||||
sinon.stub(tty, 'isatty')
|
||||
|
||||
@@ -86,7 +86,7 @@ cannotVisitDifferentOrigin = (origin, previousUrlVisited, remoteUrl, existingUrl
|
||||
previousUrl: previousUrlVisited
|
||||
attemptedUrl: origin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$errUtils.throwErrByPath("visit.cannot_visit_different_origin", errOpts)
|
||||
|
||||
@@ -294,6 +294,10 @@ normalizeTimeoutOptions = (options) ->
|
||||
module.exports = (Commands, Cypress, cy, state, config) ->
|
||||
reset()
|
||||
|
||||
Cypress.on "test:before:run:async", ->
|
||||
## reset any state on the backend
|
||||
Cypress.backend('reset:server:state')
|
||||
|
||||
Cypress.on("test:before:run", reset)
|
||||
|
||||
Cypress.on "stability:changed", (bool, event) ->
|
||||
|
||||
@@ -88,9 +88,6 @@ startXhrServer = (cy, state, config) ->
|
||||
xhrUrl: config("xhrUrl")
|
||||
stripOrigin: stripOrigin
|
||||
|
||||
emitIncoming: (id, route) ->
|
||||
Cypress.backend('incoming:xhr', id, route)
|
||||
|
||||
## shouldnt these stubs be called routes?
|
||||
## rename everything related to stubs => routes
|
||||
onSend: (xhr, stack, route) =>
|
||||
@@ -252,10 +249,6 @@ module.exports = (Commands, Cypress, cy, state, config) ->
|
||||
## correctly
|
||||
Cypress.on("window:unload", cancelPendingXhrs)
|
||||
|
||||
Cypress.on "test:before:run:async", ->
|
||||
## reset any state on the backend
|
||||
Cypress.backend('reset:server:state')
|
||||
|
||||
Cypress.on "test:before:run", ->
|
||||
## reset the existing server
|
||||
reset()
|
||||
@@ -266,7 +259,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
|
||||
## window such as if the last test ended
|
||||
## with a cross origin window
|
||||
try
|
||||
server = startXhrServer(cy, state, config, Cypress)
|
||||
server = startXhrServer(cy, state, config)
|
||||
catch err
|
||||
## in this case, just don't bind to the server
|
||||
server = null
|
||||
|
||||
@@ -11,8 +11,11 @@ props = "onreadystatechange onload onerror".split(" ")
|
||||
|
||||
restoreFn = null
|
||||
|
||||
setHeader = (xhr, key, val) ->
|
||||
setHeader = (xhr, key, val, transformer) ->
|
||||
if val?
|
||||
if transformer
|
||||
val = transformer(val)
|
||||
|
||||
key = "X-Cypress-" + _.capitalize(key)
|
||||
xhr.setRequestHeader(key, encodeURI(val))
|
||||
|
||||
@@ -174,30 +177,18 @@ create = (options = {}) ->
|
||||
hasEnabledStubs and route and route.response?
|
||||
|
||||
applyStubProperties: (xhr, route) ->
|
||||
responseToString = =>
|
||||
if not _.isString(route.response)
|
||||
return JSON.stringify(route.response)
|
||||
responser = if _.isObject(route.response) then JSON.stringify else null
|
||||
|
||||
route.response
|
||||
## add header properties for the xhr's id
|
||||
## and the testId
|
||||
setHeader(xhr, "id", xhr.id)
|
||||
# setHeader(xhr, "testId", options.testId)
|
||||
|
||||
response = responseToString()
|
||||
|
||||
headers = {
|
||||
"id": xhr.id
|
||||
"status": route.status
|
||||
"matched": route.url + ""
|
||||
"delay": route.delay
|
||||
"headers": transformHeaders(route.headers)
|
||||
}
|
||||
|
||||
if response.length > 4096
|
||||
options.emitIncoming(xhr.id, response)
|
||||
headers.responseDeferred = true
|
||||
else
|
||||
headers.response = response
|
||||
|
||||
_.map headers, (v, k) =>
|
||||
setHeader(xhr, k, v)
|
||||
setHeader(xhr, "status", route.status)
|
||||
setHeader(xhr, "response", route.response, responser)
|
||||
setHeader(xhr, "matched", route.url + "")
|
||||
setHeader(xhr, "delay", route.delay)
|
||||
setHeader(xhr, "headers", route.headers, transformHeaders)
|
||||
|
||||
route: (attrs = {}) ->
|
||||
warnOnStubDeprecation(attrs, "route")
|
||||
|
||||
@@ -85,7 +85,7 @@ module.exports = {
|
||||
|
||||
// max HTTP header size 8kb -> 1mb
|
||||
// https://github.com/cypress-io/cypress/issues/76
|
||||
argv.unshift(`--max-http-header-size=${1024 * 1024}`)
|
||||
argv.unshift(`--max-http-header-size=${1024 * 1024} --http-parser=legacy`)
|
||||
|
||||
debug('spawning %s with args', execPath, argv)
|
||||
|
||||
|
||||
@@ -119,16 +119,70 @@ const LogResponse: ResponseMiddleware = function () {
|
||||
}
|
||||
|
||||
const PatchExpressSetHeader: ResponseMiddleware = function () {
|
||||
const { incomingRes } = this
|
||||
const originalSetHeader = this.res.setHeader
|
||||
|
||||
// express.Response.setHeader does all kinds of silly/nasty stuff to the content-type...
|
||||
// but we don't want to change it at all!
|
||||
this.res.setHeader = (k, v) => {
|
||||
if (k === 'content-type') {
|
||||
v = this.incomingRes.headers['content-type'] || v
|
||||
// Node uses their own Symbol object, so use this to get the internal kOutHeaders
|
||||
// symbol - Symbol.for('kOutHeaders') will not work
|
||||
const getKOutHeadersSymbol = () => {
|
||||
const findKOutHeadersSymbol = (): symbol => {
|
||||
return _.find(Object.getOwnPropertySymbols(this.res), (sym) => {
|
||||
return sym.toString() === 'Symbol(kOutHeaders)'
|
||||
})!
|
||||
}
|
||||
|
||||
return originalSetHeader.call(this.res, k, v)
|
||||
let sym = findKOutHeadersSymbol()
|
||||
|
||||
if (sym) {
|
||||
return sym
|
||||
}
|
||||
|
||||
// force creation of a new header field so the kOutHeaders key is available
|
||||
this.res.setHeader('X-Cypress-HTTP-Response', 'X')
|
||||
this.res.removeHeader('X-Cypress-HTTP-Response')
|
||||
|
||||
sym = findKOutHeadersSymbol()
|
||||
|
||||
if (!sym) {
|
||||
throw new Error('unable to find kOutHeaders symbol')
|
||||
}
|
||||
|
||||
return sym
|
||||
}
|
||||
|
||||
let kOutHeaders
|
||||
|
||||
this.res.setHeader = function (name, value) {
|
||||
// express.Response.setHeader does all kinds of silly/nasty stuff to the content-type...
|
||||
// but we don't want to change it at all!
|
||||
if (name === 'content-type') {
|
||||
value = incomingRes.headers['content-type'] || value
|
||||
}
|
||||
|
||||
// run the original function - if an "invalid header char" error is raised,
|
||||
// set the header manually. this way we can retain Node's original error behavior
|
||||
try {
|
||||
return originalSetHeader.call(this, name, value)
|
||||
} catch (err) {
|
||||
if (err.code !== 'ERR_INVALID_CHAR') {
|
||||
throw err
|
||||
}
|
||||
|
||||
debug('setHeader error ignored %o', { name, value, code: err.code, err })
|
||||
|
||||
if (!kOutHeaders) {
|
||||
kOutHeaders = getKOutHeadersSymbol()
|
||||
}
|
||||
|
||||
// https://github.com/nodejs/node/blob/42cce5a9d0fd905bf4ad7a2528c36572dfb8b5ad/lib/_http_outgoing.js#L483-L495
|
||||
let headers = this[kOutHeaders]
|
||||
|
||||
if (!headers) {
|
||||
this[kOutHeaders] = headers = Object.create(null)
|
||||
}
|
||||
|
||||
headers[name.toLowerCase()] = [name, value]
|
||||
}
|
||||
}
|
||||
|
||||
this.next()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
exports['e2e xhr / passes'] = `
|
||||
exports['e2e xhr / passes in global mode'] = `
|
||||
|
||||
====================================================================================================
|
||||
|
||||
@@ -24,20 +24,21 @@ exports['e2e xhr / passes'] = `
|
||||
✓ does not inject into json's contents from file server even requesting text/html
|
||||
✓ works prior to visit
|
||||
✓ can stub a 100kb response
|
||||
✓ spawns tasks with original NODE_OPTIONS
|
||||
server with 1 visit
|
||||
✓ response body
|
||||
✓ request body
|
||||
✓ aborts
|
||||
|
||||
|
||||
9 passing
|
||||
10 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 9 │
|
||||
│ Passing: 9 │
|
||||
│ Tests: 10 │
|
||||
│ Passing: 10 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
@@ -61,9 +62,80 @@ exports['e2e xhr / passes'] = `
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✔ xhr_spec.coffee XX:XX 9 9 - - - │
|
||||
│ ✔ xhr_spec.coffee XX:XX 10 10 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 9 9 - - -
|
||||
✔ All specs passed! XX:XX 10 10 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
exports['e2e xhr / passes through CLI'] = `
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Starting)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Cypress: 1.2.3 │
|
||||
│ Browser: FooBrowser 88 │
|
||||
│ Specs: 1 found (xhr_spec.coffee) │
|
||||
│ Searched: cypress/integration/xhr_spec.coffee │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Running: xhr_spec.coffee (1 of 1)
|
||||
|
||||
|
||||
xhrs
|
||||
✓ can encode + decode headers
|
||||
✓ ensures that request headers + body go out and reach the server unscathed
|
||||
✓ does not inject into json's contents from http server even requesting text/html
|
||||
✓ does not inject into json's contents from file server even requesting text/html
|
||||
✓ works prior to visit
|
||||
✓ can stub a 100kb response
|
||||
✓ spawns tasks with original NODE_OPTIONS
|
||||
server with 1 visit
|
||||
✓ response body
|
||||
✓ request body
|
||||
✓ aborts
|
||||
|
||||
|
||||
10 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 10 │
|
||||
│ Passing: 10 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
│ Screenshots: 0 │
|
||||
│ Video: true │
|
||||
│ Duration: X seconds │
|
||||
│ Spec Ran: xhr_spec.coffee │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
(Video)
|
||||
|
||||
- Started processing: Compressing to 32 CRF
|
||||
- Finished processing: /XXX/XXX/XXX/cypress/videos/xhr_spec.coffee.mp4 (X second)
|
||||
|
||||
|
||||
====================================================================================================
|
||||
|
||||
(Run Finished)
|
||||
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✔ xhr_spec.coffee XX:XX 10 10 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 10 10 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
@@ -21,6 +21,7 @@ exports['e2e visit / low response timeout / passes'] = `
|
||||
✓ scrolls automatically to div with id=foo
|
||||
✓ can load an http page with a huge amount of elements without timing out
|
||||
✓ can load a local file with a huge amount of elements without timing out
|
||||
✓ can load a website which uses invalid HTTP header chars
|
||||
✓ can load a site via TLSv1
|
||||
issue #225: hash urls
|
||||
✓ can visit a hash url and loads
|
||||
@@ -36,14 +37,14 @@ exports['e2e visit / low response timeout / passes'] = `
|
||||
✓ sets accept header to text/html,*/*
|
||||
|
||||
|
||||
12 passing
|
||||
13 passing
|
||||
|
||||
|
||||
(Results)
|
||||
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Tests: 12 │
|
||||
│ Passing: 12 │
|
||||
│ Tests: 13 │
|
||||
│ Passing: 13 │
|
||||
│ Failing: 0 │
|
||||
│ Pending: 0 │
|
||||
│ Skipped: 0 │
|
||||
@@ -67,9 +68,9 @@ exports['e2e visit / low response timeout / passes'] = `
|
||||
|
||||
Spec Tests Passing Failing Pending Skipped
|
||||
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ✔ visit_spec.coffee XX:XX 12 12 - - - │
|
||||
│ ✔ visit_spec.coffee XX:XX 13 13 - - - │
|
||||
└────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
✔ All specs passed! XX:XX 12 12 - - -
|
||||
✔ All specs passed! XX:XX 13 13 - - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
@@ -40,4 +40,17 @@ process.traceDeprecation = true
|
||||
|
||||
require('./lib/util/suppress_unauthorized_warning').suppress()
|
||||
|
||||
module.exports = require('./lib/cypress').start(process.argv)
|
||||
function launchOrFork () {
|
||||
const nodeOptions = require('./lib/util/node_options')
|
||||
|
||||
if (nodeOptions.needsOptions()) {
|
||||
// https://github.com/cypress-io/cypress/pull/5492
|
||||
return nodeOptions.forkWithCorrectOptions()
|
||||
}
|
||||
|
||||
nodeOptions.restoreOriginalOptions()
|
||||
|
||||
module.exports = require('./lib/cypress').start(process.argv)
|
||||
}
|
||||
|
||||
launchOrFork()
|
||||
|
||||
@@ -18,7 +18,7 @@ isValidJSON = (text) ->
|
||||
return false
|
||||
|
||||
module.exports = {
|
||||
handle: (req, res, getDeferredResponse, config, next) ->
|
||||
handle: (req, res, config, next) ->
|
||||
get = (val, def) ->
|
||||
decodeURI(req.get(val) ? def)
|
||||
|
||||
@@ -27,10 +27,6 @@ module.exports = {
|
||||
headers = get("x-cypress-headers", null)
|
||||
response = get("x-cypress-response", "")
|
||||
|
||||
if get("x-cypress-responsedeferred", "")
|
||||
id = get("x-cypress-id")
|
||||
response = getDeferredResponse(id)
|
||||
|
||||
respond = =>
|
||||
## figure out the stream interface and pipe these
|
||||
## chunks to the response
|
||||
@@ -63,10 +59,6 @@ module.exports = {
|
||||
.set(headers)
|
||||
.status(status)
|
||||
.end(chunk)
|
||||
.catch { testEndedBeforeResponseReceived: true }, ->
|
||||
res
|
||||
.socket
|
||||
.destroy()
|
||||
.catch (err) ->
|
||||
res
|
||||
.status(400)
|
||||
@@ -92,15 +84,10 @@ module.exports = {
|
||||
{data: bytes, encoding: encoding}
|
||||
|
||||
getResponse: (resp, config) ->
|
||||
if resp.then
|
||||
return resp
|
||||
.then (data) =>
|
||||
{ data }
|
||||
|
||||
if fixturesRe.test(resp)
|
||||
return @_get(resp, config)
|
||||
|
||||
Promise.resolve({data: resp})
|
||||
@_get(resp, config)
|
||||
else
|
||||
Promise.resolve({data: resp})
|
||||
|
||||
parseContentType: (response) ->
|
||||
ret = (type) ->
|
||||
|
||||
@@ -12,7 +12,7 @@ const client = require('./controllers/client')
|
||||
const files = require('./controllers/files')
|
||||
const staticCtrl = require('./controllers/static')
|
||||
|
||||
module.exports = ({ app, config, getDeferredResponse, getRemoteState, networkProxy, project, onError }) => {
|
||||
module.exports = ({ app, config, getRemoteState, networkProxy, project, onError }) => {
|
||||
// routing for the actual specs which are processed automatically
|
||||
// this could be just a regular .js file or a .coffee file
|
||||
app.get('/__cypress/tests', (req, res, next) => {
|
||||
@@ -49,7 +49,7 @@ module.exports = ({ app, config, getDeferredResponse, getRemoteState, networkPro
|
||||
})
|
||||
|
||||
app.all('/__cypress/xhrs/*', (req, res, next) => {
|
||||
xhrs.handle(req, res, getDeferredResponse, config, next)
|
||||
xhrs.handle(req, res, config, next)
|
||||
})
|
||||
|
||||
app.get('/__root/*', (req, res) => {
|
||||
|
||||
@@ -33,7 +33,6 @@ logger = require("./logger")
|
||||
Socket = require("./socket")
|
||||
Request = require("./request")
|
||||
fileServer = require("./file_server")
|
||||
XhrServer = require("./xhr_ws_server")
|
||||
templateEngine = require("./template_engine")
|
||||
|
||||
DEFAULT_DOMAIN_NAME = "localhost"
|
||||
@@ -177,7 +176,6 @@ class Server
|
||||
## TODO: might not be needed anymore
|
||||
@_request = Request({timeout: config.responseTimeout})
|
||||
@_nodeProxy = httpProxy.createProxyServer()
|
||||
@_xhrServer = XhrServer.create()
|
||||
|
||||
getRemoteState = => @_getRemoteState()
|
||||
|
||||
@@ -190,7 +188,6 @@ class Server
|
||||
@createRoutes({
|
||||
app
|
||||
config
|
||||
getDeferredResponse: @_xhrServer.getDeferredResponse
|
||||
getRemoteState
|
||||
networkProxy: @_networkProxy
|
||||
onError
|
||||
@@ -742,10 +739,8 @@ class Server
|
||||
startWebsockets: (automation, config, options = {}) ->
|
||||
options.onResolveUrl = @_onResolveUrl.bind(@)
|
||||
options.onRequest = @_onRequest.bind(@)
|
||||
options.onIncomingXhr = @_xhrServer.onIncomingXhr
|
||||
|
||||
options.onResetServerState = =>
|
||||
@_xhrServer.reset()
|
||||
@_networkProxy.reset()
|
||||
|
||||
@_socket = new Socket(config)
|
||||
|
||||
@@ -147,7 +147,6 @@ class Socket {
|
||||
|
||||
_.defaults(options, {
|
||||
socketId: null,
|
||||
onIncomingXhr () {},
|
||||
onResetServerState () {},
|
||||
onSetRunnables () {},
|
||||
onMocha () {},
|
||||
@@ -375,8 +374,6 @@ class Socket {
|
||||
return firefoxUtil.log()
|
||||
case 'firefox:force:gc':
|
||||
return firefoxUtil.collectGarbage()
|
||||
case 'incoming:xhr':
|
||||
return options.onIncomingXhr(args[0], args[1])
|
||||
case 'get:fixture':
|
||||
return fixture.get(config.fixturesFolder, args[0], args[1])
|
||||
case 'read:file':
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import cp from 'child_process'
|
||||
import debugModule from 'debug'
|
||||
|
||||
const debug = debugModule('cypress:server:util:node_options')
|
||||
|
||||
const NODE_OPTIONS = `--max-http-header-size=${1024 ** 2} --http-parser=legacy`
|
||||
|
||||
/**
|
||||
* If Cypress was not launched via CLI, it may be missing certain startup
|
||||
* options. This checks that those startup options were applied.
|
||||
*
|
||||
* @returns {boolean} does Cypress have the expected NODE_OPTIONS?
|
||||
*/
|
||||
export function needsOptions (): boolean {
|
||||
if ((process.env.NODE_OPTIONS || '').includes(NODE_OPTIONS)) {
|
||||
debug('NODE_OPTIONS check passed, not forking %o', { NODE_OPTIONS: process.env.NODE_OPTIONS })
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof require.main === 'undefined') {
|
||||
debug('require.main is undefined, this should not happen normally, not forking')
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Fork the current process using the good NODE_OPTIONS and pipe stdio
|
||||
* through the current process. On exit, copy the error code too.
|
||||
*/
|
||||
export function forkWithCorrectOptions (): void {
|
||||
// this should only happen when running from global mode, when the CLI couldn't set the NODE_OPTIONS
|
||||
process.env.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS || ''
|
||||
process.env.NODE_OPTIONS = `${NODE_OPTIONS} ${process.env.ORIGINAL_NODE_OPTIONS}`
|
||||
|
||||
debug('NODE_OPTIONS check failed, forking %o', {
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
ORIGINAL_NODE_OPTIONS: process.env.ORIGINAL_NODE_OPTIONS,
|
||||
})
|
||||
|
||||
cp.spawn(
|
||||
process.execPath,
|
||||
process.argv.slice(1),
|
||||
{ stdio: 'inherit' },
|
||||
)
|
||||
.on('error', () => {})
|
||||
.on('exit', (code) => {
|
||||
process.exit(code)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Once the Electron process is launched, restore the user's original NODE_OPTIONS
|
||||
* environment variables from before the CLI added extra NODE_OPTIONS.
|
||||
*
|
||||
* This way, any `node` processes launched by Cypress will retain the user's
|
||||
* `NODE_OPTIONS` without unexpected modificiations that could cause issues with
|
||||
* user code.
|
||||
*/
|
||||
export function restoreOriginalOptions (): void {
|
||||
// @ts-ignore
|
||||
if (!process.versions || !process.versions.electron) {
|
||||
debug('not restoring NODE_OPTIONS since not yet in Electron')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
debug('restoring NODE_OPTIONS %o', {
|
||||
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
||||
ORIGINAL_NODE_OPTIONS: process.env.ORIGINAL_NODE_OPTIONS,
|
||||
})
|
||||
|
||||
process.env.NODE_OPTIONS = process.env.ORIGINAL_NODE_OPTIONS || ''
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import Bluebird from 'bluebird'
|
||||
import debugModule from 'debug'
|
||||
|
||||
const debug = debugModule('cypress:server:xhr_ws_server')
|
||||
|
||||
function trunc (str) {
|
||||
return _.truncate(str, {
|
||||
length: 100,
|
||||
omission: '... [truncated to 100 chars]',
|
||||
})
|
||||
}
|
||||
|
||||
type DeferredPromise<T> = {
|
||||
resolve: Function
|
||||
reject: Function
|
||||
promise: Bluebird<T>
|
||||
}
|
||||
|
||||
export function create () {
|
||||
let incomingXhrResponses: {
|
||||
[key: string]: string | DeferredPromise<string>
|
||||
} = {}
|
||||
|
||||
function onIncomingXhr (id: string, data: string) {
|
||||
debug('onIncomingXhr %o', { id, res: trunc(data) })
|
||||
const deferred = incomingXhrResponses[id]
|
||||
|
||||
if (deferred && typeof deferred !== 'string') {
|
||||
// request came before response, resolve with it
|
||||
return deferred.resolve(data)
|
||||
}
|
||||
|
||||
// response came before request, cache the data
|
||||
incomingXhrResponses[id] = data
|
||||
}
|
||||
|
||||
function getDeferredResponse (id) {
|
||||
debug('getDeferredResponse %o', { id })
|
||||
// if we already have it, send it
|
||||
const res = incomingXhrResponses[id]
|
||||
|
||||
if (res) {
|
||||
if (typeof res === 'object') {
|
||||
debug('returning existing deferred promise for %o', { id, res })
|
||||
|
||||
return res.promise
|
||||
}
|
||||
|
||||
debug('already have deferred response %o', { id, res: trunc(res) })
|
||||
delete incomingXhrResponses[id]
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
let deferred: Partial<DeferredPromise<string>> = {}
|
||||
|
||||
deferred.promise = new Bluebird((resolve, reject) => {
|
||||
debug('do not have response, waiting %o', { id })
|
||||
deferred.resolve = resolve
|
||||
deferred.reject = reject
|
||||
})
|
||||
.tap((res) => {
|
||||
debug('deferred response found %o', { id, res: trunc(res) })
|
||||
}) as Bluebird<string>
|
||||
|
||||
incomingXhrResponses[id] = deferred as DeferredPromise<string>
|
||||
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
function reset () {
|
||||
debug('resetting incomingXhrs %o', { incomingXhrResponses })
|
||||
|
||||
_.forEach(incomingXhrResponses, (res) => {
|
||||
if (typeof res !== 'string') {
|
||||
const err: any = new Error('This stubbed XHR was pending on a stub response object from the driver, but the test ended before that happened.')
|
||||
|
||||
err.testEndedBeforeResponseReceived = true
|
||||
|
||||
res.reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
incomingXhrResponses = {}
|
||||
}
|
||||
|
||||
return {
|
||||
onIncomingXhr,
|
||||
getDeferredResponse,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,13 @@ describe "e2e xhr", ->
|
||||
}
|
||||
})
|
||||
|
||||
e2e.it "passes", {
|
||||
e2e.it "passes in global mode", {
|
||||
spec: "xhr_spec.coffee"
|
||||
snapshot: true
|
||||
}
|
||||
|
||||
e2e.it "passes through CLI", {
|
||||
spec: "xhr_spec.coffee"
|
||||
snapshot: true
|
||||
useCli: true
|
||||
}
|
||||
|
||||
@@ -69,6 +69,21 @@ onServer = (app) ->
|
||||
## dont ever end this response
|
||||
res.type("html").write("foo\n")
|
||||
|
||||
## https://github.com/cypress-io/cypress/issues/5602
|
||||
app.get "/invalid-header-char", (req, res) ->
|
||||
## express/node may interfere if we just use res.setHeader
|
||||
res.connection.write(
|
||||
"""
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html
|
||||
Set-Cookie: foo=bar-#{String.fromCharCode(1)}-baz
|
||||
|
||||
foo
|
||||
"""
|
||||
)
|
||||
|
||||
res.connection.end()
|
||||
|
||||
describe "e2e visit", ->
|
||||
require("mocha-banner").register()
|
||||
|
||||
|
||||
@@ -693,23 +693,6 @@ describe "Routes", ->
|
||||
expect(res.statusCode).to.eq(200)
|
||||
expect(res.body).to.deep.eq({test: "We’ll"})
|
||||
|
||||
context "deferred", ->
|
||||
it "closes connection if no stub is received before a reset", ->
|
||||
p = @rp({
|
||||
url: "http://localhost:2020/__cypress/xhrs/users/1"
|
||||
json: true
|
||||
headers: {
|
||||
"x-cypress-id": "foo1"
|
||||
"x-cypress-responsedeferred": true
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout =>
|
||||
@server._xhrServer.reset()
|
||||
, 100
|
||||
|
||||
expect(p).to.be.rejectedWith('Error: socket hang up')
|
||||
|
||||
context "fixture", ->
|
||||
beforeEach ->
|
||||
Fixtures.scaffold("todos")
|
||||
@@ -1568,14 +1551,14 @@ describe "Routes", ->
|
||||
"__cypress.initial=true; Domain=localhost; Path=/"
|
||||
]
|
||||
|
||||
it "ignores invalid cookies", ->
|
||||
it "passes invalid cookies", ->
|
||||
nock(@server._remoteOrigin)
|
||||
.get("/invalid")
|
||||
.reply 200, "OK", {
|
||||
"set-cookie": [
|
||||
"foo=bar; Path=/"
|
||||
"___utmvmXluIZsM=fidJKOsDSdm; path=/; Max-Age=900" ## this is okay
|
||||
"___utmvbXluIZsM=bZM\n XtQOGalF: VtR; path=/; Max-Age=900" ## should ignore this
|
||||
"___utmvmXluIZsM=fidJKOsDSdm; path=/; Max-Age=900"
|
||||
"___utmvbXluIZsM=bZM\n XtQOGalF: VtR; path=/; Max-Age=900"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1585,6 +1568,7 @@ describe "Routes", ->
|
||||
expect(res.headers["set-cookie"]).to.deep.eq [
|
||||
"foo=bar; Path=/"
|
||||
"___utmvmXluIZsM=fidJKOsDSdm; path=/; Max-Age=900"
|
||||
"___utmvbXluIZsM=bZM XtQOGalF: VtR; path=/; Max-Age=900"
|
||||
]
|
||||
|
||||
it "forwards other headers from incoming responses", ->
|
||||
|
||||
@@ -87,7 +87,7 @@ if (isGteNode12()) {
|
||||
// max HTTP header size 8kb -> 1mb
|
||||
// https://github.com/cypress-io/cypress/issues/76
|
||||
commandAndArguments.args.push(
|
||||
`--max-http-header-size=${1024 * 1024}`,
|
||||
`--max-http-header-size=${1024 * 1024} --http-parser=legacy`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -10,6 +10,11 @@ describe "visits", ->
|
||||
it "can load a local file with a huge amount of elements without timing out", ->
|
||||
cy.visit("/elements.html", {timeout: 5000})
|
||||
|
||||
## https://github.com/cypress-io/cypress/issues/5602
|
||||
it "can load a website which uses invalid HTTP header chars", ->
|
||||
cy.visit("http://localhost:3434/invalid-header-char")
|
||||
.contains('foo')
|
||||
|
||||
## https://github.com/cypress-io/cypress/issues/5446
|
||||
it "can load a site via TLSv1", ->
|
||||
cy.visit("https://localhost:6776")
|
||||
|
||||
@@ -103,6 +103,9 @@ describe "xhrs", ->
|
||||
xhr.onload = finish
|
||||
xhr.onerror = finish
|
||||
|
||||
it "spawns tasks with original NODE_OPTIONS", ->
|
||||
cy.task('assert:http:max:header:size', 8192)
|
||||
|
||||
describe "server with 1 visit", ->
|
||||
before ->
|
||||
cy.visit("/xhr.html")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
require('@packages/ts/register')
|
||||
|
||||
const _ = require('lodash')
|
||||
const { expect } = require('chai')
|
||||
const http = require('http')
|
||||
const Jimp = require('jimp')
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
@@ -167,5 +169,11 @@ module.exports = (on, config) => {
|
||||
'get:config:value' (key) {
|
||||
return config[key]
|
||||
},
|
||||
|
||||
'assert:http:max:header:size' (expectedBytes) {
|
||||
expect(http.maxHeaderSize).to.eq(expectedBytes)
|
||||
|
||||
return null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import chai, { expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import * as XhrServer from '../../lib/xhr_ws_server'
|
||||
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
describe('lib/xhr_ws_server', function () {
|
||||
context('#create', function () {
|
||||
let xhrServer
|
||||
|
||||
beforeEach(function () {
|
||||
xhrServer = XhrServer.create()
|
||||
})
|
||||
|
||||
it('resolves a response when incomingXhr is received before request', function () {
|
||||
xhrServer.onIncomingXhr('foo', 'bar')
|
||||
expect(xhrServer.getDeferredResponse('foo')).to.eq('bar')
|
||||
})
|
||||
|
||||
it('resolves a response when incomingXhr is received after request', async function () {
|
||||
const p = xhrServer.getDeferredResponse('foo')
|
||||
const q = xhrServer.getDeferredResponse('foo')
|
||||
|
||||
xhrServer.onIncomingXhr('foo', 'bar')
|
||||
|
||||
await expect(p).to.eventually.deep.eq('bar')
|
||||
await expect(q).to.eventually.deep.eq('bar')
|
||||
})
|
||||
|
||||
it('rejects a response when incomingXhr is received and test gets reset', function () {
|
||||
const p = xhrServer.getDeferredResponse('foo')
|
||||
|
||||
xhrServer.reset()
|
||||
|
||||
return expect(p).to.be.rejectedWith('This stubbed XHR was pending')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user