Skip to content

Commit 810a9b4

Browse files
authored
fix(net-stubbing): fix waiting on responses with no-body status codes (#9097)
1 parent a3fd875 commit 810a9b4

File tree

8 files changed

+94
-31
lines changed

8 files changed

+94
-31
lines changed

packages/driver/cypress/integration/commands/net_stubbing_spec.ts

+49-6
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ describe('network stubbing', { retries: 2 }, function () {
475475
context('network handling', function () {
476476
// @see https://github.com/cypress-io/cypress/issues/8497
477477
it('can load transfer-encoding: chunked redirects', function () {
478+
cy.intercept('*')
478479
const url4 = 'http://localhost:3501/fixtures/generic.html'
479480
const url3 = `http://localhost:3501/redirect?href=${encodeURIComponent(url4)}`
480481
const url2 = `https://localhost:3502/redirect?chunked=1&href=${encodeURIComponent(url3)}`
@@ -1209,10 +1210,10 @@ describe('network stubbing', { retries: 2 }, function () {
12091210

12101211
context('correctly determines the content-length of an intercepted request', function () {
12111212
it('when body is empty', function (done) {
1212-
cy.route2('/post-only', function (req) {
1213+
cy.intercept('/post-only', function (req) {
12131214
req.body = ''
12141215
}).then(function () {
1215-
cy.route2('/post-only', function (req) {
1216+
cy.intercept('/post-only', function (req) {
12161217
expect(req.headers['content-length']).to.eq('0')
12171218

12181219
done()
@@ -1224,10 +1225,10 @@ describe('network stubbing', { retries: 2 }, function () {
12241225
})
12251226

12261227
it('when body contains ascii', function (done) {
1227-
cy.route2('/post-only', function (req) {
1228+
cy.intercept('/post-only', function (req) {
12281229
req.body = 'this is only ascii'
12291230
}).then(function () {
1230-
cy.route2('/post-only', function (req) {
1231+
cy.intercept('/post-only', function (req) {
12311232
expect(req.headers['content-length']).to.eq('18')
12321233

12331234
done()
@@ -1239,10 +1240,10 @@ describe('network stubbing', { retries: 2 }, function () {
12391240
})
12401241

12411242
it('when body contains unicode', function (done) {
1242-
cy.route2('/post-only', function (req) {
1243+
cy.intercept('/post-only', function (req) {
12431244
req.body = '🙃🤔'
12441245
}).then(function () {
1245-
cy.route2('/post-only', function (req) {
1246+
cy.intercept('/post-only', function (req) {
12461247
expect(req.headers['content-length']).to.eq('8')
12471248

12481249
done()
@@ -1288,6 +1289,18 @@ describe('network stubbing', { retries: 2 }, function () {
12881289
.wait('@dest')
12891290
})
12901291

1292+
it('can simply wait on redirects without intercepting', function () {
1293+
const href = `/fixtures/generic.html?t=${Date.now()}`
1294+
const url = `/redirect?href=${encodeURIComponent(href)}`
1295+
1296+
cy.intercept('/redirect')
1297+
.as('redirect')
1298+
.intercept('/fixtures/generic.html').as('dest')
1299+
.then(() => fetch(url))
1300+
.wait('@redirect')
1301+
.wait('@dest')
1302+
})
1303+
12911304
// @see https://github.com/cypress-io/cypress/issues/7967
12921305
it('can skip redirects via followRedirect', function () {
12931306
const href = `/fixtures/generic.html?t=${Date.now()}`
@@ -1983,6 +1996,36 @@ describe('network stubbing', { retries: 2 }, function () {
19831996
})
19841997
})
19851998

1999+
// @see https://github.com/cypress-io/cypress/issues/8999
2000+
it('can spy on a 204 no body response', function () {
2001+
cy.intercept('/status-code').as('status')
2002+
.then(() => {
2003+
$.get('/status-code?code=204')
2004+
})
2005+
.wait('@status').its('response.statusCode').should('eq', 204)
2006+
})
2007+
2008+
// @see https://github.com/cypress-io/cypress/issues/8934
2009+
it('can spy on a 304 not modified image response', function () {
2010+
const url = `/fixtures/media/cypress.png?i=${Date.now()}`
2011+
2012+
cy.intercept(url).as('image')
2013+
.then(() => {
2014+
$.get({ url, cache: true })
2015+
})
2016+
.then(() => {
2017+
if (Cypress.isBrowser('firefox')) {
2018+
// strangely, Firefox requires some time to be waited before the first image response will be cached
2019+
cy.wait(1000)
2020+
}
2021+
})
2022+
.then(() => {
2023+
$.get({ url, cache: true })
2024+
})
2025+
.wait('@image').its('response.statusCode').should('eq', 200)
2026+
.wait('@image').its('response.statusCode').should('eq', 304)
2027+
})
2028+
19862029
context('with an intercepted request', function () {
19872030
it('can dynamically alias the request', function () {
19882031
cy.intercept('/foo', (req) => {

packages/driver/cypress/plugins/server.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,17 @@ const createApp = (port) => {
4141
.send('<html><body>hello there</body></html>')
4242
})
4343

44-
app.get('/redirect', (req, res) => {
44+
app.get('/status-code', (req, res) => {
45+
res.sendStatus(req.query.code || 200)
46+
})
47+
48+
app.all('/redirect', (req, res) => {
4549
if (req.query.chunked) {
4650
res.setHeader('transfer-encoding', 'chunked')
4751
res.removeHeader('content-length')
4852
}
4953

50-
res.statusCode = 301
54+
res.statusCode = Number(req.query.code || 301)
5155
res.setHeader('Location', req.query.href)
5256
res.end()
5357
})

packages/net-stubbing/lib/server/intercept-request.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import _ from 'lodash'
2-
import concatStream from 'concat-stream'
2+
import { concatStream } from '@packages/network'
33
import Debug from 'debug'
44
import url from 'url'
55

packages/net-stubbing/lib/server/intercept-response.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import _ from 'lodash'
2-
import concatStream from 'concat-stream'
2+
import { concatStream, httpUtils } from '@packages/network'
33
import Debug from 'debug'
4-
import { Readable } from 'stream'
4+
import { Readable, PassThrough } from 'stream'
55

66
import {
77
ResponseMiddleware,
@@ -65,17 +65,30 @@ export const InterceptResponse: ResponseMiddleware = function () {
6565

6666
this.makeResStreamPlainText()
6767

68-
this.incomingResStream.pipe(concatStream((resBody) => {
69-
res.body = resBody.toString()
68+
new Promise((resolve) => {
69+
if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) {
70+
resolve('')
71+
} else {
72+
this.incomingResStream.pipe(concatStream((resBody) => {
73+
resolve(resBody)
74+
}))
75+
}
76+
})
77+
.then((body) => {
78+
const pt = this.incomingResStream = new PassThrough()
79+
80+
pt.end(body)
81+
82+
res.body = String(body)
7083
emitReceived()
71-
}))
7284

73-
if (!backendRequest.waitForResponseContinue) {
74-
this.next()
75-
}
85+
if (!backendRequest.waitForResponseContinue) {
86+
this.next()
87+
}
7688

77-
// this may get set back to `true` by another route
78-
backendRequest.waitForResponseContinue = false
89+
// this may get set back to `true` by another route
90+
backendRequest.waitForResponseContinue = false
91+
})
7992
}
8093

8194
export async function onResponseContinue (state: NetStubbingState, frame: NetEventFrames.HttpResponseContinue) {

packages/net-stubbing/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
},
1111
"dependencies": {
1212
"@types/mime-types": "2.1.0",
13-
"concat-stream": "^2.0.0",
1413
"is-html": "^2.0.0",
1514
"lodash": "4.17.15",
1615
"mime-types": "2.1.27",

packages/network/lib/http-utils.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import _ from 'lodash'
2+
import { IncomingMessage } from 'http'
3+
4+
// https://github.com/cypress-io/cypress/issues/4298
5+
// https://tools.ietf.org/html/rfc7230#section-3.3.3
6+
// HEAD, 1xx, 204, and 304 responses should never contain anything after headers
7+
const NO_BODY_STATUS_CODES = [204, 304]
8+
9+
export function responseMustHaveEmptyBody (req: IncomingMessage, res: IncomingMessage) {
10+
return _.includes(NO_BODY_STATUS_CODES, res.statusCode) || (req.method && req.method.toLowerCase() === 'head')
11+
}

packages/network/lib/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import agent from './agent'
22
import * as blocked from './blocked'
33
import * as connect from './connect'
44
import * as cors from './cors'
5+
import * as httpUtils from './http-utils'
56
import * as uri from './uri'
67

78
export {
89
agent,
910
blocked,
1011
connect,
1112
cors,
13+
httpUtils,
1214
uri,
1315
}
1416

packages/proxy/lib/http/response-middleware.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import _ from 'lodash'
22
import charset from 'charset'
33
import { CookieOptions } from 'express'
4-
import { cors, concatStream } from '@packages/network'
4+
import { cors, concatStream, httpUtils } from '@packages/network'
55
import { CypressIncomingRequest, CypressOutgoingResponse } from '@packages/proxy'
66
import debugModule from 'debug'
77
import { HttpMiddleware } from '.'
@@ -92,15 +92,6 @@ function resIsGzipped (res: IncomingMessage) {
9292
return (res.headers['content-encoding'] || '').includes('gzip')
9393
}
9494

95-
// https://github.com/cypress-io/cypress/issues/4298
96-
// https://tools.ietf.org/html/rfc7230#section-3.3.3
97-
// HEAD, 1xx, 204, and 304 responses should never contain anything after headers
98-
const NO_BODY_STATUS_CODES = [204, 304]
99-
100-
function responseMustHaveEmptyBody (req: CypressIncomingRequest, res: IncomingMessage) {
101-
return _.some([_.includes(NO_BODY_STATUS_CODES, res.statusCode), _.invoke(req.method, 'toLowerCase') === 'head'])
102-
}
103-
10495
function setCookie (res: CypressOutgoingResponse, k: string, v: string, domain: string) {
10596
let opts: CookieOptions = { domain }
10697

@@ -367,7 +358,7 @@ const ClearCyInitialCookie: ResponseMiddleware = function () {
367358
}
368359

369360
const MaybeEndWithEmptyBody: ResponseMiddleware = function () {
370-
if (responseMustHaveEmptyBody(this.req, this.incomingRes)) {
361+
if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) {
371362
this.res.end()
372363

373364
return this.end()

0 commit comments

Comments
 (0)