feat(proxy): allow manual injection of bootstrap script to prevent hydration mismatches (#33295)

* fix(proxy): allow manual injection of bootstrap script to prevent hydration mismatches

Injects Cypress bootstrap code into a developer-provided <script data-cy-bootstrap> tag if present. This allows developers to add attributes like suppressHydrationWarning to the script tag to prevent React hydration mismatches.

* docs: add changelog entry for manual bootstrap injection

* docs: move changelog entry to Bugfixes section

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
Co-authored-by: Cacie Prins <cacieprins@users.noreply.github.com>
This commit is contained in:
Marc Agbanchenou
2026-02-17 20:17:31 +01:00
committed by GitHub
parent 484ce1a877
commit 1246ad577f
3 changed files with 80 additions and 0 deletions
+1
View File
@@ -5,6 +5,7 @@ _Released 02/17/2026 (PENDING)_
**Features:**
- Introduces manual bootstrap script injection via a `<script data-cy-bootstrap>` tag. This is a workaround to fix React SSR hydration mismatches, and enables React apps to use `suppressHydrationWarning` to ignore the mismatch. Addresses [#27204](https://github.com/cypress-io/cypress/issues/27204). Addressed in [#33295](https://github.com/cypress-io/cypress/pull/33295).
- Added Brotli compression support to the proxy. Addresses [#6197](https://github.com/cypress-io/cypress/issues/6197).
**Bugfixes:**
+15
View File
@@ -26,6 +26,7 @@ const doctypeRe = /<\!doctype.*?>/i
const headRe = /<head(?!er).*?>/i
const bodyRe = /<body.*?>/i
const htmlRe = /<html.*?>/i
const bootstrapScriptRe = /(<script[^>]*\bdata-cy-bootstrap\b[^>]*>)([\s\S]*?)(<\/script>)/i
function getRewriter (useAstSourceRewriting: boolean) {
return useAstSourceRewriting ? astRewriter : regexRewriter
@@ -91,6 +92,20 @@ export async function html (html: string, opts: SecurityOpts & InjectionOpts) {
return html
}
const bootstrapMatch = html.match(bootstrapScriptRe)
if (bootstrapMatch) {
const contentToInject = htmlToInject.replace(/^<script[^>]*>|<\/script>$/g, '')
let openTag = bootstrapMatch[1]
// Ensure nonce is present if provided in options
if (opts.cspNonce && !openTag.includes('nonce=')) {
openTag = openTag.replace(/>$/, ` nonce="${opts.cspNonce}">`)
}
return html.replace(bootstrapScriptRe, `${openTag}${contentToInject}${bootstrapMatch[3]}`)
}
// TODO: move this into regex-rewriting and have ast-rewriting handle this in its own way
const headMatch = html.match(headRe)
@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest'
import * as rewriter from '../../../../lib/http/util/rewriter'
describe('http/util/rewriter', () => {
describe('html', () => {
it('injects into head by default', async () => {
const html = '<html><head></head><body></body></html>'
const opts = {
domainName: 'localhost',
wantsInjection: 'full',
shouldInjectDocumentDomain: true,
} as any
const result = await rewriter.html(html, opts)
expect(result).toContain('document.domain')
expect(result).toContain('<script')
expect(result).toContain('<head> <script')
})
it('injects into developer-provided script tag and adds nonce if missing', async () => {
const html = '<html><head><script data-cy-bootstrap></script></head><body></body></html>'
const opts = {
domainName: 'localhost',
wantsInjection: 'full',
shouldInjectDocumentDomain: true,
cspNonce: 'test-nonce-123',
} as any
const result = await rewriter.html(html, opts)
// Should NOT inject a new head script
expect(result).not.toContain('<head> <script')
// Should preserve the marker
expect(result).toContain('data-cy-bootstrap')
// Should automatically add the nonce
expect(result).toContain('nonce="test-nonce-123"')
// Should contain the code
expect(result).toContain('document.domain')
})
it('preserves existing attributes on developer-provided script tag', async () => {
const html = '<html><head><script data-cy-bootstrap id="cy-bootstrap" nonce="existing"></script></head><body></body></html>'
const opts = {
domainName: 'localhost',
wantsInjection: 'full',
shouldInjectDocumentDomain: true,
cspNonce: 'new-nonce',
} as any
const result = await rewriter.html(html, opts)
// Should check that it uses the existing nonce and doesn't double up
// Current implementation checks !includes('nonce='), so it respects existing
expect(result).toContain('nonce="existing"')
expect(result).not.toContain('nonce="new-nonce"')
expect(result).toContain('id="cy-bootstrap"')
})
})
})