From d5ca95233e3f8dd545fddb54a1f182d5a2e354f8 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 27 Jun 2024 11:31:24 -0700 Subject: [PATCH] offer a json response for grabbing short links (#4671) --- bskylink/src/routes/redirect.ts | 20 ++++++++++++++--- bskylink/tests/index.ts | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts index 7791ea81..276aae1c 100644 --- a/bskylink/src/routes/redirect.ts +++ b/bskylink/src/routes/redirect.ts @@ -11,6 +11,7 @@ export default function (ctx: AppContext, app: Express) { '/:linkId', handler(async (req, res) => { const linkId = req.params.linkId + const contentType = req.accepts(['html', 'json']) assert( typeof linkId === 'string', 'express guarantees id parameter is a string', @@ -21,9 +22,19 @@ export default function (ctx: AppContext, app: Express) { .where('id', '=', linkId) .executeTakeFirst() if (!found) { - // potentially broken or mistyped link— send user to the app - res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`) + // potentially broken or mistyped link 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() } // 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}`, ) url.pathname = found.path - res.setHeader('Location', url.href) 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() }), ) diff --git a/bskylink/tests/index.ts b/bskylink/tests/index.ts index 51449c21..c5604c7a 100644 --- a/bskylink/tests/index.ts +++ b/bskylink/tests/index.ts @@ -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]> { const url = new URL(link) const base = new URL(baseUrl) @@ -70,6 +90,25 @@ describe('link service', async () => { 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 { const res = await fetch(new URL('/link', baseUrl), { method: 'post',