Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add resource type header to CDP, extension, and electron #23821

14 changes: 12 additions & 2 deletions packages/extension/app/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ const connect = function (host, path, extraOpts) {
// adds a header to the request to mark it as a request for the AUT frame
// itself, so the proxy can utilize that for injection purposes
browser.webRequest.onBeforeSendHeaders.addListener((details) => {
const requestModifications = {
requestHeaders: [
...(details.requestHeaders || []),
...(details.type === 'xmlhttprequest' ? [{
name: 'X-Cypress-Request',
value: 'true',
}] : []),
],
}

if (
// parentFrameId: 0 means the parent is the top-level, so if it isn't
// 0, it's nested inside the AUT and can't be the AUT itself
Expand All @@ -69,11 +79,11 @@ const connect = function (host, path, extraOpts) {
|| details.type !== 'sub_frame'
// is the spec frame, not the AUT
|| details.url.includes('__cypress')
) return
) return requestModifications

return {
requestHeaders: [
...details.requestHeaders,
...requestModifications.requestHeaders,
{
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
Expand Down
62 changes: 58 additions & 4 deletions packages/extension/test/integration/background_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ describe('app/background', () => {

const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

expect(result).to.be.undefined
expect(result).to.deep.equal({ requestHeaders: [] })
})

it('does not add header if it is a nested frame', async function () {
Expand All @@ -311,7 +311,7 @@ describe('app/background', () => {

const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

expect(result).to.be.undefined
expect(result).to.deep.equal({ requestHeaders: [] })
})

it('does not add header if it is not a sub frame request', async function () {
Expand All @@ -326,7 +326,7 @@ describe('app/background', () => {

const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

expect(result).to.be.undefined
expect(result).to.deep.equal({ requestHeaders: [] })
})

it('does not add header if it is a spec frame request', async function () {
Expand All @@ -341,7 +341,7 @@ describe('app/background', () => {
await this.connect(withExperimentalFlagOn)
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

expect(result).to.be.undefined
expect(result).to.deep.equal({ requestHeaders: [] })
})

it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () {
Expand Down Expand Up @@ -373,6 +373,60 @@ describe('app/background', () => {
})
})

it('appends X-Cypress-Request header to request if the resourceType is "xmlhttprequest"', async function () {
const details = {
parentFrameId: 0,
type: 'xmlhttprequest',
url: 'http://localhost:3000/index.html',
requestHeaders: [
{ name: 'X-Foo', value: 'Bar' },
],
}

sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')

await this.connect(withExperimentalFlagOn)
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

expect(result).to.deep.equal({
requestHeaders: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Request',
value: 'true',
},
],
})
})

it('does not append X-Cypress-Request header to request if the resourceType is not an "xmlhttprequest"', async function () {
const details = {
parentFrameId: 0,
type: 'sub_frame',
url: 'http://localhost:3000/index.html',
requestHeaders: [
{ name: 'X-Foo', value: 'Bar' },
],
}

sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener')

await this.connect(withExperimentalFlagOn)
const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details)

expect(result).to.not.deep.equal({
requestHeaders: [
{
name: 'X-Cypress-Request',
value: 'true',
},
],
})
})

it('does not add before-headers listener if in non-Firefox browser', async function () {
browser.runtime.getBrowserInfo = undefined

Expand Down
9 changes: 7 additions & 2 deletions packages/proxy/lib/http/request-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ const LogRequest: RequestMiddleware = function () {
this.next()
}

const ExtractIsAUTFrameHeader: RequestMiddleware = function () {
const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
this.req.isAUTFrame = !!this.req.headers['x-cypress-is-aut-frame']

if (this.req.headers['x-cypress-is-aut-frame']) {
delete this.req.headers['x-cypress-is-aut-frame']
}

if (this.req.headers['x-cypress-request']) {
this.debug(`found x-cypress-request header. Deleting x-cypress-request header.`)
delete this.req.headers['x-cypress-request']
}

this.next()
}

Expand Down Expand Up @@ -247,7 +252,7 @@ const SendRequestOutgoing: RequestMiddleware = function () {

export default {
LogRequest,
ExtractIsAUTFrameHeader,
ExtractCypressMetadataHeaders,
MaybeSimulateSecHeaders,
MaybeAttachCrossOriginCookies,
MaybeEndRequestWithBufferedResponse,
Expand Down
38 changes: 33 additions & 5 deletions packages/proxy/test/unit/http/request-middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('http/request-middleware', () => {
it('exports the members in the correct order', () => {
expect(_.keys(RequestMiddleware)).to.have.ordered.members([
'LogRequest',
'ExtractIsAUTFrameHeader',
'ExtractCypressMetadataHeaders',
'MaybeSimulateSecHeaders',
'MaybeAttachCrossOriginCookies',
'MaybeEndRequestWithBufferedResponse',
Expand All @@ -26,8 +26,8 @@ describe('http/request-middleware', () => {
])
})

describe('ExtractIsAUTFrameHeader', () => {
const { ExtractIsAUTFrameHeader } = RequestMiddleware
describe('ExtractCypressMetadataHeaders', () => {
const { ExtractCypressMetadataHeaders } = RequestMiddleware

it('removes x-cypress-is-aut-frame header when it exists, sets in on the req', async () => {
const ctx = {
Expand All @@ -38,7 +38,7 @@ describe('http/request-middleware', () => {
} as Partial<CypressIncomingRequest>,
}

await testMiddleware([ExtractIsAUTFrameHeader], ctx)
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-is-aut-frame']).not.to.exist
expect(ctx.req.isAUTFrame).to.be.true
Expand All @@ -52,12 +52,40 @@ describe('http/request-middleware', () => {
} as Partial<CypressIncomingRequest>,
}

await testMiddleware([ExtractIsAUTFrameHeader], ctx)
await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-is-aut-frame']).not.to.exist
expect(ctx.req.isAUTFrame).to.be.false
})
})

it('removes x-cypress-request header when it exists', async () => {
const ctx = {
req: {
headers: {
'x-cypress-request': 'true',
},
} as Partial<CypressIncomingRequest>,
}

await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-request']).not.to.exist
})
})

it('removes x-cypress-request header when it does not exist', async () => {
const ctx = {
req: {
headers: {},
} as Partial<CypressIncomingRequest>,
}

await testMiddleware([ExtractCypressMetadataHeaders], ctx)
.then(() => {
expect(ctx.req.headers['x-cypress-request']).not.to.exist
})
})
})

describe('MaybeSimulateSecHeaders', () => {
Expand Down
26 changes: 20 additions & 6 deletions packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,19 +342,19 @@ const _listenForFrameTreeChanges = (client) => {
client.on('Page.frameDetached', _updateFrameTree(client, 'Page.frameDetached'))
}

const _continueRequest = (client, params, header?) => {
const _continueRequest = (client, params, headers?) => {
const details: Protocol.Fetch.ContinueRequestRequest = {
requestId: params.requestId,
}

if (header) {
if (headers && headers.length) {
// headers are received as an object but need to be an array
// to modify them
const currentHeaders = _.map(params.request.headers, (value, name) => ({ name, value }))

details.headers = [
...currentHeaders,
header,
...headers,
]
}

Expand Down Expand Up @@ -403,20 +403,34 @@ const _handlePausedRequests = async (client) => {
// adds a header to the request to mark it as a request for the AUT frame
// itself, so the proxy can utilize that for injection purposes
client.on('Fetch.requestPaused', async (params: Protocol.Fetch.RequestPausedEvent) => {
const addedHeaders: {
name: string
value: string
}[] = []

if (params.resourceType === 'XHR' || params.resourceType === 'Fetch') {
debug('add X-Cypress-Request header to: %s', params.request.url)
addedHeaders.push({
name: 'X-Cypress-Request',
value: params.resourceType.toLowerCase(),
})
}

if (
// is a script, stylesheet, image, etc
params.resourceType !== 'Document'
|| !(await _isAUTFrame(params.frameId))
) {
return _continueRequest(client, params)
return _continueRequest(client, params, addedHeaders)
}

debug('add X-Cypress-Is-AUT-Frame header to: %s', params.request.url)

_continueRequest(client, params, {
addedHeaders.push({
name: 'X-Cypress-Is-AUT-Frame',
value: 'true',
})

return _continueRequest(client, params, addedHeaders)
})
}

Expand Down
13 changes: 11 additions & 2 deletions packages/server/lib/browsers/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,15 @@ export = {
// adds a header to the request to mark it as a request for the AUT frame
// itself, so the proxy can utilize that for injection purposes
win.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => {
const requestModifications = {
requestHeaders: {
...details.requestHeaders,
...(details.resourceType === 'xhr') ? {
'X-Cypress-Request': 'true',
} : {},
},
}

if (
// isn't an iframe
details.resourceType !== 'subFrame'
Expand All @@ -392,14 +401,14 @@ export = {
// is the spec frame, not the AUT
|| details.url.includes('__cypress')
) {
cb({})
cb(requestModifications)

return
}

cb({
requestHeaders: {
...details.requestHeaders,
...requestModifications.requestHeaders,
'X-Cypress-Is-AUT-Frame': 'true',
},
})
Expand Down
64 changes: 64 additions & 0 deletions packages/server/test/unit/browsers/chrome_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,70 @@ describe('lib/browsers/chrome', () => {
})
})

it('appends X-Cypress-Request header to fetch request', async function () {
await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation)

this.pageCriClient.on.withArgs('Page.frameAttached').yield()

await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({
frameId: 'aut-frame-id',
requestId: '1234',
resourceType: 'Fetch',
request: {
url: 'http://localhost:3000/test-request',
headers: {
'X-Foo': 'Bar',
},
},
})

expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
headers: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Request',
value: 'fetch',
},
],
})
})

it('appends X-Cypress-Request header to xhr request', async function () {
await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation)

this.pageCriClient.on.withArgs('Page.frameAttached').yield()

await this.pageCriClient.on.withArgs('Fetch.requestPaused').args[0][1]({
frameId: 'aut-frame-id',
requestId: '1234',
resourceType: 'XHR',
request: {
url: 'http://localhost:3000/test-request',
headers: {
'X-Foo': 'Bar',
},
},
})

expect(this.pageCriClient.send).to.be.calledWith('Fetch.continueRequest', {
requestId: '1234',
headers: [
{
name: 'X-Foo',
value: 'Bar',
},
{
name: 'X-Cypress-Request',
value: 'xhr',
},
],
})
})

it('gets frame tree on Page.frameAttached', async function () {
await chrome.open('chrome', 'http://', withExperimentalFlagOn, this.automation)

Expand Down
Loading