Skip to content

Commit 749efd7

Browse files
authored
Rest redirect endpoint (github#27027)
1 parent 434f49a commit 749efd7

File tree

7 files changed

+126
-26
lines changed

7 files changed

+126
-26
lines changed

components/article/ClientsideRedirectExceptions.tsx

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,67 @@
11
import { useEffect } from 'react'
22
import { useRouter } from 'next/router'
33

4-
import restApiOverrides from '../../lib/redirects/static/client-side-rest-api-redirects.json'
5-
import productOverrides from '../../lib/redirects/static/client-side-product-redirects.json'
6-
const overrideRedirects: Record<string, string> = { ...restApiOverrides, ...productOverrides }
7-
4+
// We recently moved several rest api operations around
5+
// in the docs. That means that we are out of sync with
6+
// the urls defined in the OpenAPI. We will eventually
7+
// update those urls but for now we want to ensure that
8+
// we have client-side redirects in place for any urls
9+
// in the product that link to the rest docs (e.g., error
10+
// code urls from the apis).
11+
// The client-side redirects can consist of operation urls
12+
// being redirected to the new operation url or headings
13+
// on a page that need to be redirected to the new page (e.g.,
14+
// /rest/reference/repos#statuses to
15+
// /rest/reference/commits#commit-statuses)
816
export default function ClientSideRedirectExceptions() {
917
const router = useRouter()
1018
useEffect(() => {
11-
// We have some one-off redirects for rest api docs
12-
// currently those are limited to the repos page, but
13-
// that will grow soon as we restructure the rest api docs.
14-
// This is a workaround to updating the hardcoded links
15-
// directly in the REST API code in a separate repo, which
16-
// requires many file changes and teams to sign off.
17-
// While the organization is turbulent, we can do this.
18-
// Once it's more settled, we can refactor the rest api code
19-
// to leverage the OpenAPI urls rather than hardcoded urls.
20-
const { hash, pathname } = window.location
19+
// Because we have an async call to fetch, it's possible that this
20+
// component unmounts before we perform the redirect, however, React
21+
// will still try to perform the redirect even after the component
22+
// is unmounted. To prevent this, we can use the AbortController signal
23+
// to abort the Web request when the component unmounts.
24+
const controller = new AbortController()
25+
const signal = controller.signal
2126

22-
// The `hash` will start with a `#` but all the keys in
23-
// `overrideRedirects` do not. Hence, this slice.
24-
const combined = pathname + hash
25-
const overrideKey = combined
27+
const { hash, pathname } = window.location
28+
// path without a version or language
29+
const barePath = pathname
2630
.replace(`/${router.locale}`, '')
2731
.replace(`/${router.query.versionId || ''}`, '')
28-
const redirectToName = overrideRedirects[overrideKey]
29-
if (redirectToName) {
30-
const newPathname = combined.replace(overrideKey, redirectToName)
31-
router.replace(newPathname)
32+
33+
async function getRedirect() {
34+
try {
35+
const sp = new URLSearchParams()
36+
sp.set('path', barePath)
37+
sp.set('hash', hash.replace(/^#/, ''))
38+
39+
// call the anchor-redirect endpoint to get the redirect url
40+
const response = await fetch(`/anchor-redirect?${sp.toString()}`, {
41+
signal,
42+
})
43+
44+
// the response status will always be 200 unless there
45+
// was a problem with the fetch request. When the
46+
// redirect doesn't exist the json response will be empty
47+
if (response.ok) {
48+
const { to } = await response.json()
49+
if (to) {
50+
// we want to redirect with the language and version in tact
51+
// so we'll replace the full url's path and hash
52+
const fromUrl = pathname + hash
53+
const bareUrl = barePath + hash
54+
const toUrl = fromUrl.replace(bareUrl, to)
55+
router.replace(toUrl)
56+
}
57+
}
58+
} catch (error) {
59+
console.warn('Unable to fetch client-side redirect:', error)
60+
}
3261
}
62+
getRedirect()
63+
64+
return () => controller.abort()
3365
}, [])
3466

3567
return null

lib/redirects/static/client-side-product-redirects.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

lib/redirects/static/client-side-rest-api-redirects.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
"/rest/orgs#list-custom-repository-roles-in-an-organization": "/rest/orgs/custom-roles#list-custom-repository-roles-in-an-organization",
156156
"/rest/repos#deploy-keys": "/rest/deploy-keys",
157157
"/rest/deployments#deploy-keys": "/rest/deploy-keys",
158+
"/rest/repos#statuses": "/rest/commits/statuses",
158159
"/rest/apps#get-the-authenticated-app": "/rest/apps/apps#get-the-authenticated-app",
159160
"/rest/apps#create-a-github-app-from-a-manifest": "/rest/apps/apps#create-a-github-app-from-a-manifest",
160161
"/rest/apps#get-a-webhook-configuration-for-an-app": "/rest/apps/webhooks#get-a-webhook-configuration-for-an-app",

middleware/anchor-redirect.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import express from 'express'
2+
import { readCompressedJsonFileFallbackLazily } from '../lib/read-json-file.js'
3+
4+
const clientSideRestAPIRedirects = readCompressedJsonFileFallbackLazily(
5+
'./lib/redirects/static/client-side-rest-api-redirects.json'
6+
)
7+
console.log(clientSideRestAPIRedirects)
8+
9+
const router = express.Router()
10+
11+
// Returns a client side redirect if one exists for the given path.
12+
router.get('/', function redirects(req, res, next) {
13+
if (!req.query.path) {
14+
return res.status(400).send("Missing 'path' query string")
15+
}
16+
if (!req.query.hash) {
17+
return res.status(400).send("Missing 'hash' query string")
18+
}
19+
const redirectFrom = `${req.query.path}#${req.query.hash}`
20+
res.status(200).send({ to: clientSideRestAPIRedirects()[redirectFrom] } || null)
21+
})
22+
23+
export default router

middleware/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import archivedEnterpriseVersionsAssets from './archived-enterprise-versions-ass
3535
import events from './events.js'
3636
import search from './search.js'
3737
import healthz from './healthz.js'
38+
import anchorRedirect from './anchor-redirect.js'
3839
import remoteIP from './remote-ip.js'
3940
import archivedEnterpriseVersions from './archived-enterprise-versions.js'
4041
import robots from './robots.js'
@@ -263,6 +264,7 @@ export default function (app) {
263264
app.use('/events', asyncMiddleware(instrument(events, './events')))
264265
app.use('/search', asyncMiddleware(instrument(search, './search')))
265266
app.use('/healthz', asyncMiddleware(instrument(healthz, './healthz')))
267+
app.use('/anchor-redirect', asyncMiddleware(instrument(anchorRedirect, './anchor-redirect')))
266268
app.get('/_ip', asyncMiddleware(instrument(remoteIP, './remoteIP')))
267269

268270
// Check for a dropped connection before proceeding (again)

script/rest/utils/rest-api-overrides.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@
778778
},
779779
"sectionUrls": {
780780
"/rest/repos#deploy-keys": "/rest/deploy-keys",
781-
"/rest/deployments#deploy-keys": "/rest/deploy-keys"
781+
"/rest/deployments#deploy-keys": "/rest/deploy-keys",
782+
"/rest/repos#statuses": "/rest/commits/statuses"
782783
}
783784
}

tests/unit/anchor-redirect.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect } from '@jest/globals'
2+
import { get } from '../helpers/e2etest.js'
3+
import clientSideRedirects from '../../lib/redirects/static/client-side-rest-api-redirects.json'
4+
5+
describe('anchor-redirect middleware', () => {
6+
test('returns correct redirect to url', async () => {
7+
// test the first entry
8+
const [key, value] = Object.entries(clientSideRedirects)[0]
9+
const [path, hash] = key.split('#')
10+
const sp = new URLSearchParams()
11+
sp.set('path', path)
12+
sp.set('hash', hash)
13+
const res = await get('/anchor-redirect?' + sp)
14+
expect(res.statusCode).toBe(200)
15+
const { to } = JSON.parse(res.text)
16+
expect(to).toBe(value)
17+
})
18+
test('errors when path is not passed', async () => {
19+
// test the first entry
20+
const key = Object.keys(clientSideRedirects)[0]
21+
const hash = key.split('#')[1]
22+
const sp = new URLSearchParams()
23+
sp.set('hash', hash)
24+
const res = await get('/anchor-redirect?' + sp)
25+
expect(res.statusCode).toBe(400)
26+
})
27+
test('errors when path is not passed', async () => {
28+
// test the first entry
29+
const key = Object.keys(clientSideRedirects)[0]
30+
const path = key.split('#')[0]
31+
const sp = new URLSearchParams()
32+
sp.set('path', path)
33+
const res = await get('/anchor-redirect?' + sp)
34+
expect(res.statusCode).toBe(400)
35+
})
36+
test('unfound redirect returns undefined', async () => {
37+
const sp = new URLSearchParams()
38+
sp.set('path', 'foo')
39+
sp.set('hash', 'bar')
40+
const res = await get('/anchor-redirect?' + sp)
41+
const { to } = JSON.parse(res.text)
42+
expect(to).toBe(undefined)
43+
})
44+
})

0 commit comments

Comments
 (0)