mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Implement circular JSON err.sh link (#6149)
* Implement circular JSON err.sh link * Add test for getInitialProps returning circular json * Make test warn less * Fix tests * Add reference to original tests
This commit is contained in:
parent
125aaf8834
commit
b3045cc7a9
11
errors/circular-structure.md
Normal file
11
errors/circular-structure.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Circular structure in "getInitialProps" result
|
||||
|
||||
#### Why This Error Occurred
|
||||
|
||||
`getInitialProps` is serialized to JSON using `JSON.stringify` and sent to the client side for hydrating the page.
|
||||
|
||||
However, the result returned from `getInitialProps` can't be serialized when it has a circular structure.
|
||||
|
||||
#### Possible Ways to Fix It
|
||||
|
||||
Circular structures are not supported, so the way to fix this error is removing the circular structure from the object that is returned from `getInitialProps`.
|
|
@ -66,7 +66,6 @@
|
|||
"friendly-errors-webpack-plugin": "1.7.0",
|
||||
"glob": "7.1.2",
|
||||
"hoist-non-react-statics": "3.2.0",
|
||||
"htmlescape": "1.1.1",
|
||||
"http-status": "1.0.1",
|
||||
"launch-editor": "2.2.1",
|
||||
"loader-utils": "1.1.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable */
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import htmlescape from 'htmlescape'
|
||||
import {htmlEscapeJsonString} from '../server/htmlescape'
|
||||
import flush from 'styled-jsx/server'
|
||||
|
||||
const Fragment = React.Fragment || function Fragment ({ children }) {
|
||||
|
@ -193,8 +193,16 @@ export class NextScript extends Component {
|
|||
}
|
||||
|
||||
static getInlineScriptSource (documentProps) {
|
||||
const { __NEXT_DATA__ } = documentProps
|
||||
return htmlescape(__NEXT_DATA__)
|
||||
const {__NEXT_DATA__} = documentProps
|
||||
try {
|
||||
const data = JSON.stringify(__NEXT_DATA__)
|
||||
return htmlEscapeJsonString(data)
|
||||
} catch(err) {
|
||||
if(err.message.indexOf('circular structure')) {
|
||||
throw new Error(`Circular structure in "getInitialProps" result of page "${__NEXT_DATA__.page}". https://err.sh/zeit/next.js/circular-structure`)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
16
packages/next/server/htmlescape.ts
Normal file
16
packages/next/server/htmlescape.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
// This utility is based on https://github.com/zertosh/htmlescape
|
||||
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
|
||||
|
||||
const ESCAPE_LOOKUP: {[match: string]: string} = {
|
||||
'&': '\\u0026',
|
||||
'>': '\\u003e',
|
||||
'<': '\\u003c',
|
||||
'\u2028': '\\u2028',
|
||||
'\u2029': '\\u2029',
|
||||
}
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g
|
||||
|
||||
export function htmlEscapeJsonString(str: string) {
|
||||
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match])
|
||||
}
|
|
@ -6,7 +6,7 @@ module.exports = {
|
|||
},
|
||||
webpack (config) {
|
||||
config.module.rules.push({
|
||||
test: /pages[\\/]hmr/,
|
||||
test: /pages[\\/]hmr[\\/]about/,
|
||||
loader: path.join(__dirname, 'warning-loader.js')
|
||||
})
|
||||
|
||||
|
|
17
test/integration/basic/pages/circular-json-error.js
Normal file
17
test/integration/basic/pages/circular-json-error.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
function CircularJSONErrorPage () {
|
||||
return <div>This won't render</div>
|
||||
}
|
||||
|
||||
CircularJSONErrorPage.getInitialProps = () => {
|
||||
// This creates a circular JSON object
|
||||
const object = {}
|
||||
object.arr = [
|
||||
object, object
|
||||
]
|
||||
object.arr.push(object.arr)
|
||||
object.obj = object
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
export default CircularJSONErrorPage
|
|
@ -120,6 +120,12 @@ export default function ({ app }, suiteName, render, fetch) {
|
|||
expect(link.text()).toBe('About')
|
||||
})
|
||||
|
||||
test('getInitialProps circular structure', async () => {
|
||||
const $ = await get$('/circular-json-error')
|
||||
const expectedErrorMessage = 'Circular structure in "getInitialProps" result of page "/circular-json-error".'
|
||||
expect($('pre').text().includes(expectedErrorMessage)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('getInitialProps should be class method', async () => {
|
||||
const $ = await get$('/instance-get-initial-props')
|
||||
const expectedErrorMessage = '"InstanceInitialPropsPage.getInitialProps()" is defined as an instance method - visit https://err.sh/zeit/next.js/get-initial-props-as-an-instance-method for more information.'
|
||||
|
|
43
test/unit/htmlescape.test.js
Normal file
43
test/unit/htmlescape.test.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* eslint-env jest */
|
||||
// These tests are based on https://github.com/zertosh/htmlescape/blob/3e6cf0614dd0f778fd0131e69070b77282150c15/test/htmlescape-test.js
|
||||
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
|
||||
import {htmlEscapeJsonString} from 'next/dist/server/htmlescape'
|
||||
import vm from 'vm'
|
||||
|
||||
describe('htmlescape', () => {
|
||||
test('with angle brackets should escape', () => {
|
||||
const evilObj = {evil: '<script></script>'}
|
||||
expect(htmlEscapeJsonString(JSON.stringify(evilObj))).toBe('{"evil":"\\u003cscript\\u003e\\u003c/script\\u003e"}')
|
||||
})
|
||||
|
||||
test('with angle brackets should parse back', () => {
|
||||
const evilObj = {evil: '<script></script>'}
|
||||
expect(JSON.parse(htmlEscapeJsonString(JSON.stringify(evilObj)))).toMatchObject(evilObj)
|
||||
})
|
||||
|
||||
test('with ampersands should escape', () => {
|
||||
const evilObj = {evil: '&'}
|
||||
expect(htmlEscapeJsonString(JSON.stringify(evilObj))).toBe('{"evil":"\\u0026"}')
|
||||
})
|
||||
|
||||
test('with ampersands should parse back', () => {
|
||||
const evilObj = {evil: '&'}
|
||||
expect(JSON.parse(htmlEscapeJsonString(JSON.stringify(evilObj)))).toMatchObject(evilObj)
|
||||
})
|
||||
|
||||
test('with "LINE SEPARATOR" and "PARAGRAPH SEPARATOR" should escape', () => {
|
||||
const evilObj = {evil: '\u2028\u2029'}
|
||||
expect(htmlEscapeJsonString(JSON.stringify(evilObj))).toBe('{"evil":"\\u2028\\u2029"}')
|
||||
})
|
||||
|
||||
test('with "LINE SEPARATOR" and "PARAGRAPH SEPARATOR" should parse back', () => {
|
||||
const evilObj = {evil: '\u2028\u2029'}
|
||||
expect(JSON.parse(htmlEscapeJsonString(JSON.stringify(evilObj)))).toMatchObject(evilObj)
|
||||
})
|
||||
|
||||
test('escaped line terminators should work', () => {
|
||||
expect(() => {
|
||||
vm.runInNewContext('(' + htmlEscapeJsonString(JSON.stringify({evil: '\u2028\u2029'})) + ')')
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
|
@ -5898,11 +5898,6 @@ html-entities@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
|
||||
integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
|
||||
|
||||
htmlescape@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
|
||||
integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
|
||||
|
||||
htmlparser2@^3.9.1:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
|
||||
|
|
Loading…
Reference in a new issue