Skip to content

Commit

Permalink
Use websockets to stub large XHR response bodies instead of headers
Browse files Browse the repository at this point in the history
  • Loading branch information
flotwig committed Oct 29, 2019
1 parent 1a74623 commit d425fdb
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 24 deletions.
5 changes: 4 additions & 1 deletion packages/driver/src/cy/commands/xhr.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ 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) =>
Expand Down Expand Up @@ -256,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)
server = startXhrServer(cy, state, config, Cypress)
catch err
## in this case, just don't bind to the server
server = null
Expand Down
41 changes: 25 additions & 16 deletions packages/driver/src/cypress/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ props = "onreadystatechange onload onerror".split(" ")

restoreFn = null

setHeader = (xhr, key, val, transformer) ->
setHeader = (xhr, key, val) ->
if val?
if transformer
val = transformer(val)

key = "X-Cypress-" + _.capitalize(key)
xhr.setRequestHeader(key, encodeURI(val))

Expand Down Expand Up @@ -176,18 +173,30 @@ create = (options = {}) ->
hasEnabledStubs and route and route.response?

applyStubProperties: (xhr, route) ->
responser = if _.isObject(route.response) then JSON.stringify else null

## add header properties for the xhr's id
## and the testId
setHeader(xhr, "id", xhr.id)
# setHeader(xhr, "testId", options.testId)

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)
responseToString = =>
if not _.isString(route.response)
return JSON.stringify(route.response)

route.response

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)

route: (attrs = {}) ->
warnOnStubDeprecation(attrs, "route")
Expand Down
15 changes: 11 additions & 4 deletions packages/server/lib/controllers/xhrs.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ isValidJSON = (text) ->
return false

module.exports = {
handle: (req, res, config, next) ->
handle: (req, res, getDeferredResponse, config, next) ->
get = (val, def) ->
decodeURI(req.get(val) ? def)

Expand All @@ -27,6 +27,10 @@ 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
Expand Down Expand Up @@ -79,10 +83,13 @@ module.exports = {
{data: bytes, encoding: encoding}

getResponse: (resp, config) ->
if resp.then
return resp

if fixturesRe.test(resp)
@_get(resp, config)
else
Promise.resolve({data: resp})
return @_get(resp, config)

Promise.resolve({data: resp})

parseContentType: (response) ->
ret = (type) ->
Expand Down
4 changes: 2 additions & 2 deletions packages/server/lib/routes.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ files = require("./controllers/files")
proxy = require("./controllers/proxy")
staticCtrl = require("./controllers/static")

module.exports = (app, config, request, getRemoteState, project, nodeProxy) ->
module.exports = (app, config, request, getRemoteState, getDeferredResponse, project, nodeProxy) ->
## 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) ->
Expand Down Expand Up @@ -45,7 +45,7 @@ module.exports = (app, config, request, getRemoteState, project, nodeProxy) ->
files.handleIframe(req, res, config, getRemoteState)

app.all "/__cypress/xhrs/*", (req, res, next) ->
xhrs.handle(req, res, config, next)
xhrs.handle(req, res, getDeferredResponse, config, next)

app.get "/__root/*", (req, res, next) ->
file = path.join(config.projectRoot, req.params[0])
Expand Down
7 changes: 6 additions & 1 deletion packages/server/lib/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ logger = require("./logger")
Socket = require("./socket")
Request = require("./request")
fileServer = require("./file_server")
XhrServer = require("./xhr_ws_server")

DEFAULT_DOMAIN_NAME = "localhost"
fullyQualifiedRe = /^https?:\/\//
Expand Down Expand Up @@ -145,12 +146,15 @@ class Server
## and set the responseTimeout
@_request = Request({timeout: config.responseTimeout})
@_nodeProxy = httpProxy.createProxyServer()
@_xhrServer = XhrServer.create()

getRemoteState = => @_getRemoteState()

getDeferredResponse = @_xhrServer.getDeferredResponse.bind(@_xhrServer)

@createHosts(config.hosts)

@createRoutes(app, config, @_request, getRemoteState, project, @_nodeProxy)
@createRoutes(app, config, @_request, getRemoteState, getDeferredResponse, project, @_nodeProxy)

@createServer(app, config, project, @_request, onWarning)

Expand Down Expand Up @@ -702,6 +706,7 @@ class Server
startWebsockets: (automation, config, options = {}) ->
options.onResolveUrl = @_onResolveUrl.bind(@)
options.onRequest = @_onRequest.bind(@)
options.onIncomingXhr = @_xhrServer.onIncomingXhr.bind(@_xhrServer)

@_socket = Socket(config)
@_socket.startListening(@_server, automation, config, options)
Expand Down
3 changes: 3 additions & 0 deletions packages/server/lib/socket.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class Socket

_.defaults options,
socketId: null
onIncomingXhr: ->
onSetRunnables: ->
onMocha: ->
onConnect: ->
Expand Down Expand Up @@ -298,6 +299,8 @@ class Socket
options.onResolveUrl(url, headers, automationRequest, resolveOpts)
when "http:request"
options.onRequest(headers, automationRequest, args[0])
when "incoming:xhr"
options.onIncomingXhr(args[0], args[1])
when "get:fixture"
fixture.get(config.fixturesFolder, args[0], args[1])
when "read:file"
Expand Down
64 changes: 64 additions & 0 deletions packages/server/lib/xhr_ws_server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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]',
})
}

export function create () {
let incomingXhrs: any[] = []

function onIncomingXhr (id, data) {
debug('onIncomingXhr %o', { id, res: trunc(data) })
const resolve = incomingXhrs[id]

if (resolve) {
return resolve({
data,
})
}

incomingXhrs[id] = data
}

function getDeferredResponse (id) {
debug('getDeferredResponse %o', { id })
// if we already have it, send it
const res = incomingXhrs[id]

if (res) {
if (res.then) {
debug('returning existing deferred promise for %o', { id })
}

debug('already have deferred response %o', { id, res: trunc(res) })
delete incomingXhrs[id]

return res
}

return new Bluebird((resolve) => {
debug('do not have response, waiting %o', { id })
incomingXhrs[id] = resolve
})
.tap((res) => {
debug('deferred response found %o', { id, res: trunc(res) })
})
}

function onBeforeTestRun () {

}

return {
onIncomingXhr,
getDeferredResponse,
onBeforeTestRun,
}
}

0 comments on commit d425fdb

Please sign in to comment.