Skip to content

Commit

Permalink
Fix NextJS 500s (github#20048)
Browse files Browse the repository at this point in the history
* feat: allow server to contextualize request when on a /_next/data path

* add client side routing test, run lint

* enable nextjs client side routing
  • Loading branch information
mikesurowiec authored Jun 22, 2021
1 parent 1b255f1 commit a24d01f
Show file tree
Hide file tree
Showing 21 changed files with 75 additions and 30 deletions.
2 changes: 1 addition & 1 deletion components/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMainContext } from 'components/context/MainContext'

const { NODE_ENV } = process.env

const enableNextLinks = false
const enableNextLinks = true

type Props = { locale?: string } & ComponentProps<'a'>
export function Link(props: Props) {
Expand Down
4 changes: 3 additions & 1 deletion components/Survey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ export const Survey = () => {
</>
)}

{state === ViewState.END && <p className="color-text-secondary f6" data-testid="survey-end">{t`feedback`}</p>}
{state === ViewState.END && (
<p className="color-text-secondary f6" data-testid="survey-end">{t`feedback`}</p>
)}
</form>
)
}
Expand Down
2 changes: 1 addition & 1 deletion components/context/ProductSubLandingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const getProductSubLandingContextFromRequest = (req: any): ProductSubLand
includeGuides: (page.includeGuides || []).map((guide: any) => {
return {
...pick(guide, ['href', 'title', 'intro', 'topics']),
type: guide.type || ''
type: guide.type || '',
}
}),
}
Expand Down
4 changes: 3 additions & 1 deletion components/sublanding/ArticleCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export const ArticleCard = ({ card, typeLabel }: Props) => {
<div data-testid="article-card" className="d-flex col-12 col-md-4 pr-0 pr-md-6 pr-lg-8">
<a className="no-underline d-flex flex-column py-3 border-bottom" href={card.href}>
<h4 className="h4 color-text-primary mb-1">{card.title}</h4>
<div className="h6 text-uppercase" data-testid="article-card-type">{typeLabel}</div>
<div className="h6 text-uppercase" data-testid="article-card-type">
{typeLabel}
</div>
<p className="color-text-secondary my-3">{card.intro}</p>
{card.topics.length > 0 && (
<div>
Expand Down
2 changes: 1 addition & 1 deletion components/sublanding/SubLandingHero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const SubLandingHero = () => {
const { t } = useTranslation('product_sublanding')

const guideItems = featuredTrack?.guides?.map((guide) => (
<li className="px-2 d-flex flex-shrink-0">
<li className="px-2 d-flex flex-shrink-0" key={guide.href}>
<Link
href={`${guide.href}?learn=${featuredTrack.trackName}`}
className="d-inline-block Box p-5 color-bg-primary color-border-primary no-underline"
Expand Down
4 changes: 2 additions & 2 deletions javascripts/wrap-code-terms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export default function () {
const codeTerms = document.querySelectorAll('#article-contents table code')
if (!codeTerms) return

codeTerms.forEach(node => {
codeTerms.forEach((node) => {
// Return early if a child node is an anchor element
const hasChildAnchor = Array.from(node.childNodes).some(child => child.nodeName === 'A')
const hasChildAnchor = Array.from(node.childNodes).some((child) => child.nodeName === 'A')
if (hasChildAnchor) return

// Do the wrapping on the inner text only
Expand Down
10 changes: 5 additions & 5 deletions middleware/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ module.exports = async function contextualize (req, res, next) {
// e.g. searches for "req.context.page" will include results from this file
req.context.currentLanguage = req.language
req.context.userLanguage = req.userLanguage
req.context.currentVersion = getVersionStringFromPath(req.path)
req.context.currentProduct = getProductStringFromPath(req.path)
req.context.currentCategory = getCategoryStringFromPath(req.path)
req.context.currentVersion = getVersionStringFromPath(req.pagePath)
req.context.currentProduct = getProductStringFromPath(req.pagePath)
req.context.currentCategory = getCategoryStringFromPath(req.pagePath)
req.context.productMap = productMap
req.context.activeProducts = activeProducts
req.context.allVersions = allVersions
req.context.currentPathWithoutLanguage = getPathWithoutLanguage(req.path)
req.context.currentPath = req.path
req.context.currentPathWithoutLanguage = getPathWithoutLanguage(req.pagePath)
req.context.currentPath = req.pagePath
req.context.query = req.query
req.context.languages = languages
req.context.productNames = productNames
Expand Down
2 changes: 1 addition & 1 deletion middleware/contextualizers/generic-toc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports = async function genericToc (req, res, next) {
const currentTocType = tocTypes[req.context.page.documentType]

// Find the part of the site tree that corresponds to the current path.
const treePage = findPageInSiteTree(req.context.currentProductTree, req.context.currentEnglishTree, req.path)
const treePage = findPageInSiteTree(req.context.currentProductTree, req.context.currentEnglishTree, req.pagePath)

// Conditionally run getTocItems() recursively.
let isRecursive
Expand Down
2 changes: 1 addition & 1 deletion middleware/contextualizers/graphql.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = function graphqlContext (req, res, next) {
const currentVersionObj = allVersions[req.context.currentVersion]
// ignore requests to non-GraphQL reference paths
// and to versions that don't exist
if (!req.path.includes('/graphql/') || !currentVersionObj) {
if (!req.pagePath.includes('/graphql/') || !currentVersionObj) {
return next()
}
// Get the relevant name of the GraphQL schema files for the current version
Expand Down
4 changes: 2 additions & 2 deletions middleware/contextualizers/release-notes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const supported = all.filter(release => {

module.exports = async function releaseNotesContext (req, res, next) {
// The `/release-notes` sub-path
if (!(req.path.endsWith('/release-notes') || req.path.endsWith('/admin'))) return next()
if (!(req.pagePath.endsWith('/release-notes') || req.pagePath.endsWith('/admin'))) return next()

const [requestedPlan, requestedRelease] = req.context.currentVersion.split('@')
const releaseNotesPerPlan = req.context.site.data['release-notes'][requestedPlan]
Expand All @@ -28,7 +28,7 @@ module.exports = async function releaseNotesContext (req, res, next) {
if (hasNumberedReleases) {
const currentReleaseNotes = releaseNotesPerPlan[`${requestedRelease.replace(/\./g, '-')}`]

if (!currentReleaseNotes && req.path.endsWith('/release-notes')) {
if (!currentReleaseNotes && req.pagePath.endsWith('/release-notes')) {
// If the GHES version doesn't have any release notes, let's be helpful and redirect to `enterprise.github.com`
return requestedPlan === 'enterprise-server'
? res.redirect(`https://enterprise.github.com/releases/${requestedRelease}.0/notes`)
Expand Down
4 changes: 2 additions & 2 deletions middleware/contextualizers/rest.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ module.exports = function restContext (req, res, next) {
))

// ignore requests to non-REST reference paths
if (!req.path.includes('rest/reference')) return next()
if (!req.pagePath.includes('rest/reference')) return next()

// e.g. the `activity` from `/en/rest/reference/activity#events`
const category = req.path
const category = req.pagePath
.split('rest/reference')[1]
.replace(/^\//, '') // remove leading slash
.split('/')[0]
Expand Down
2 changes: 1 addition & 1 deletion middleware/contextualizers/webhooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = function webhooksContext (req, res, next) {
const currentVersionObj = allVersions[req.context.currentVersion]
// ignore requests to non-webhook reference paths
// and to versions that don't exist
if (!req.path.includes('webhook') || !currentVersionObj) {
if (!req.pagePath.includes('webhook') || !currentVersionObj) {
return next()
}

Expand Down
4 changes: 2 additions & 2 deletions middleware/find-page.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// This middleware uses the request path to find a page in the preloaded context.pages object

module.exports = async function findPage (req, res, next) {
let page = req.context.pages[req.path]
let page = req.context.pages[req.pagePath]

// if this is a localized request that can't be found, try finding an English variant
if (!page && req.language !== 'en') {
const englishPath = req.path.replace(new RegExp(`^/${req.language}`), '/en')
const englishPath = req.pagePath.replace(new RegExp(`^/${req.language}`), '/en')
// NOTE the fallback page will have page.languageCode = 'en'
page = req.context.pages[englishPath]
}
Expand Down
19 changes: 19 additions & 0 deletions middleware/handle-next-data-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = async function handleNextDataPath (req, res, next) {
if (req.path.startsWith('/_next/data') && req.path.endsWith('.json')) {
// translate a nextjs data request to a page path that the server can use on context
// this is triggered via client-side route tranistions
// example path:
// /_next/data/development/en/free-pro-team%40latest/github/setting-up-and-managing-your-github-user-account.json
const decodedPath = decodeURIComponent(req.path)
const parts = decodedPath.split('/').slice(4)
// free-pro-team@latest should not be included in the page path
if (parts[1] === 'free-pro-team@latest') {
parts.splice(1,1)
}
req.pagePath = '/' + parts.join('/').replace(/.json+$/, '')
} else {
req.pagePath = req.path
}

next()
}
1 change: 1 addition & 0 deletions middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module.exports = function (app) {
app.set('trust proxy', 1)
app.use(require('./rate-limit'))
app.use(instrument('./handle-invalid-paths'))
app.use(instrument('./handle-next-data-path'))

// *** Security ***
app.use(require('./cors'))
Expand Down
2 changes: 1 addition & 1 deletion middleware/learning-track.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = async function learningTrack (req, res, next) {

const currentLearningTrack = { trackName }

const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.path))
const guidePath = getPathWithoutLanguage(getPathWithoutVersion(req.pagePath))
const guideIndex = track.guides.findIndex((path) => path === guidePath)

if (guideIndex < 0) return noTrack()
Expand Down
2 changes: 1 addition & 1 deletion middleware/next.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if (FEATURE_NEXTJS) {
}

module.exports = function renderPageWithNext (req, res, next) {
if (req.path.startsWith('/_next/')) {
if (req.path.startsWith('/_next') && !req.path.startsWith('/_next/data')) {
return nextHandleRequest(req, res)
}

Expand Down
8 changes: 4 additions & 4 deletions middleware/render-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ module.exports = async function renderPage (req, res, next) {
const context = Object.assign({}, req.context, { page })

// collect URLs for variants of this page in all languages
context.page.languageVariants = Page.getLanguageVariants(req.path)
context.page.languageVariants = Page.getLanguageVariants(req.pagePath)
// Stop processing if the connection was already dropped
if (isConnectionDropped(req, res)) return

Expand All @@ -147,14 +147,14 @@ module.exports = async function renderPage (req, res, next) {
}

// handle special-case prerendered GraphQL objects page
if (req.path.endsWith('graphql/reference/objects')) {
if (req.pagePath.endsWith('graphql/reference/objects')) {
// concat the markdown source miniToc items and the prerendered miniToc items
context.miniTocItems = context.miniTocItems.concat(req.context.graphql.prerenderedObjectsForCurrentVersion.miniToc)
context.renderedPage = context.renderedPage + req.context.graphql.prerenderedObjectsForCurrentVersion.html
}

// handle special-case prerendered GraphQL input objects page
if (req.path.endsWith('graphql/reference/input-objects')) {
if (req.pagePath.endsWith('graphql/reference/input-objects')) {
// concat the markdown source miniToc items and the prerendered miniToc items
context.miniTocItems = context.miniTocItems.concat(req.context.graphql.prerenderedInputObjectsForCurrentVersion.miniToc)
context.renderedPage = context.renderedPage + req.context.graphql.prerenderedInputObjectsForCurrentVersion.html
Expand All @@ -164,7 +164,7 @@ module.exports = async function renderPage (req, res, next) {
context.page.fullTitle = context.page.titlePlainText

// add localized ` - GitHub Docs` suffix to <title> tag (except for the homepage)
if (!patterns.homepagePath.test(req.path)) {
if (!patterns.homepagePath.test(req.pagePath)) {
context.page.fullTitle = context.page.fullTitle + ' - ' + context.site.data.ui.header.github_docs
}

Expand Down
3 changes: 2 additions & 1 deletion nodemon.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"translations",
"dist",
"javascripts",
"stylesheets"
"stylesheets",
"tests"
]
}
2 changes: 0 additions & 2 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ import events from 'javascripts/events'
import experiment from 'javascripts/experiment'
import setNextEnv from 'javascripts/set-next-env'


type MyAppProps = AppProps & { csrfToken: string; themeProps: typeof defaultThemeProps }
const MyApp = ({ Component, pageProps, csrfToken, themeProps }: MyAppProps) => {

useEffect(() => {
events()
experiment()
Expand Down
22 changes: 22 additions & 0 deletions tests/browser/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,25 @@ describe('nextjs query param', () => {
flagVal === true ? expect(IS_NEXTJS_PAGE).toBe(true) : expect(IS_NEXTJS_PAGE).toBe(false)
})
})

describe('next/link client-side navigation', () => {
jest.setTimeout(60 * 1000)

it('should have 200 response to /_next/data when link is clicked', async () => {
const initialViewport = page.viewport()
await page.setViewport({ width: 1024, height: 768 })
await page.goto('http://localhost:4001/en/actions')

const [response] = await Promise.all([
page.waitForResponse(
(response) =>
response.url().startsWith('http://localhost:4001/_next/data')
),
page.waitForNavigation({ waitUntil: 'networkidle2' }),
page.click('.sidebar-categories .sidebar-category:nth-child(2) a'),
])

expect(response.status()).toBe(200)
await page.setViewport(initialViewport)
})
})

0 comments on commit a24d01f

Please sign in to comment.