offer a json response for grabbing short links (#4671)
parent
fff3ae8f35
commit
d5ca95233e
|
@ -11,6 +11,7 @@ export default function (ctx: AppContext, app: Express) {
|
||||||
'/:linkId',
|
'/:linkId',
|
||||||
handler(async (req, res) => {
|
handler(async (req, res) => {
|
||||||
const linkId = req.params.linkId
|
const linkId = req.params.linkId
|
||||||
|
const contentType = req.accepts(['html', 'json'])
|
||||||
assert(
|
assert(
|
||||||
typeof linkId === 'string',
|
typeof linkId === 'string',
|
||||||
'express guarantees id parameter is a string',
|
'express guarantees id parameter is a string',
|
||||||
|
@ -21,9 +22,19 @@ export default function (ctx: AppContext, app: Express) {
|
||||||
.where('id', '=', linkId)
|
.where('id', '=', linkId)
|
||||||
.executeTakeFirst()
|
.executeTakeFirst()
|
||||||
if (!found) {
|
if (!found) {
|
||||||
// potentially broken or mistyped link— send user to the app
|
// potentially broken or mistyped link
|
||||||
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
|
|
||||||
res.setHeader('Cache-Control', 'no-store')
|
res.setHeader('Cache-Control', 'no-store')
|
||||||
|
if (contentType === 'json') {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({
|
||||||
|
error: 'NotFound',
|
||||||
|
message: 'Link not found',
|
||||||
|
})
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
// send the user to the app
|
||||||
|
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
|
||||||
return res.status(302).end()
|
return res.status(302).end()
|
||||||
}
|
}
|
||||||
// build url from original url in order to preserve query params
|
// build url from original url in order to preserve query params
|
||||||
|
@ -32,8 +43,11 @@ export default function (ctx: AppContext, app: Express) {
|
||||||
`https://${ctx.cfg.service.appHostname}`,
|
`https://${ctx.cfg.service.appHostname}`,
|
||||||
)
|
)
|
||||||
url.pathname = found.path
|
url.pathname = found.path
|
||||||
res.setHeader('Location', url.href)
|
|
||||||
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
|
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
|
||||||
|
if (contentType === 'json') {
|
||||||
|
return res.json({url: url.href}).end()
|
||||||
|
}
|
||||||
|
res.setHeader('Location', url.href)
|
||||||
return res.status(301).end()
|
return res.status(301).end()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -56,6 +56,26 @@ describe('link service', async () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('returns json object with url when requested', async () => {
|
||||||
|
const link = await getLink('/start/did:example:carol/zzz/')
|
||||||
|
const [status, json] = await getJsonRedirect(link)
|
||||||
|
assert.strictEqual(status, 200)
|
||||||
|
assert(json.url)
|
||||||
|
const url = new URL(json.url)
|
||||||
|
assert.strictEqual(url.pathname, '/start/did:example:carol/zzz')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 404 for unknown link when requesting json', async () => {
|
||||||
|
const [status, json] = await getJsonRedirect(
|
||||||
|
'https://test.bsky.link/unknown',
|
||||||
|
)
|
||||||
|
assert(json.error)
|
||||||
|
assert(json.message)
|
||||||
|
assert.strictEqual(status, 404)
|
||||||
|
assert.strictEqual(json.error, 'NotFound')
|
||||||
|
assert.strictEqual(json.message, 'Link not found')
|
||||||
|
})
|
||||||
|
|
||||||
async function getRedirect(link: string): Promise<[number, string]> {
|
async function getRedirect(link: string): Promise<[number, string]> {
|
||||||
const url = new URL(link)
|
const url = new URL(link)
|
||||||
const base = new URL(baseUrl)
|
const base = new URL(baseUrl)
|
||||||
|
@ -70,6 +90,25 @@ describe('link service', async () => {
|
||||||
return [res.status, res.headers.get('location') ?? '']
|
return [res.status, res.headers.get('location') ?? '']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getJsonRedirect(
|
||||||
|
link: string,
|
||||||
|
): Promise<[number, {url?: string; error?: string; message?: string}]> {
|
||||||
|
const url = new URL(link)
|
||||||
|
const base = new URL(baseUrl)
|
||||||
|
url.protocol = base.protocol
|
||||||
|
url.host = base.host
|
||||||
|
const res = await fetch(url, {
|
||||||
|
redirect: 'manual',
|
||||||
|
headers: {accept: 'application/json,text/html'},
|
||||||
|
})
|
||||||
|
assert(
|
||||||
|
res.headers.get('content-type')?.startsWith('application/json'),
|
||||||
|
'content type was not json',
|
||||||
|
)
|
||||||
|
const json = await res.json()
|
||||||
|
return [res.status, json]
|
||||||
|
}
|
||||||
|
|
||||||
async function getLink(path: string): Promise<string> {
|
async function getLink(path: string): Promise<string> {
|
||||||
const res = await fetch(new URL('/link', baseUrl), {
|
const res = await fetch(new URL('/link', baseUrl), {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
|
Loading…
Reference in New Issue