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:
Zach Bloomquist
2020-03-18 17:26:22 -04:00
committed by GitHub
parent ead1d38e47
commit 47410d50e5
27 changed files with 398 additions and 248 deletions
+19
View File
@@ -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 exit event with signal SIGKILL
+1 -1
View File
@@ -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
View File
@@ -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),
+2 -15
View File
@@ -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)
})
})
+44
View File
@@ -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) ->
+1 -8
View File
@@ -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
+14 -23
View File
@@ -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")
+1 -1
View File
@@ -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)
+60 -6
View File
@@ -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 - - -
`
+14 -1
View File
@@ -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()
+4 -17
View File
@@ -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) ->
+2 -2
View File
@@ -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) => {
-5
View File
@@ -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)
-3
View File
@@ -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':
+77
View 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 || ''
}
-93
View File
@@ -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,
}
}
+7 -1
View File
@@ -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: "Well"})
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", ->
+1 -1
View File
@@ -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`,
)
}
@@ -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')
})
})
})