about summary refs log tree commit diff
path: root/bskylink
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-06-27 11:31:24 -0700
committerGitHub <noreply@github.com>2024-06-27 11:31:24 -0700
commitd5ca95233e3f8dd545fddb54a1f182d5a2e354f8 (patch)
tree5412dbdd5997cd88ad6b2ece3659398a208bc4c4 /bskylink
parentfff3ae8f359f496de3165d9d15c7135fc4269916 (diff)
downloadvoidsky-d5ca95233e3f8dd545fddb54a1f182d5a2e354f8.tar.zst
offer a json response for grabbing short links (#4671)
Diffstat (limited to 'bskylink')
-rw-r--r--bskylink/src/routes/redirect.ts20
-rw-r--r--bskylink/tests/index.ts39
2 files changed, 56 insertions, 3 deletions
diff --git a/bskylink/src/routes/redirect.ts b/bskylink/src/routes/redirect.ts
index 7791ea815..276aae1ca 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 51449c21b..c5604c7a1 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<string> {
     const res = await fetch(new URL('/link', baseUrl), {
       method: 'post',