From d06de4d8ea64d9ca1173a4a5ae9e8df61edfef29 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 26 Feb 2024 11:53:34 -0600 Subject: [PATCH] Add PPR error screening to admin pages --- src/admin/AdminPhotoMenu.tsx | 4 ++-- src/auth/cache.ts | 4 +++- src/services/storage/index.ts | 10 +++++++--- src/services/vercel-postgres.ts | 8 +++----- src/site/CommandK.tsx | 4 ++-- src/site/Footer.tsx | 5 ++--- src/site/Nav.tsx | 5 ++--- src/utility/ppr.ts | 18 ++++++++++++++++++ 8 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 src/utility/ppr.ts diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx index 84f7259b..441520ab 100644 --- a/src/admin/AdminPhotoMenu.tsx +++ b/src/admin/AdminPhotoMenu.tsx @@ -1,11 +1,11 @@ -import { authCached } from '@/auth/cache'; +import { authCachedSafe } from '@/auth/cache'; import AdminPhotoMenuClient from './AdminPhotoMenuClient'; import { ComponentProps } from 'react'; export default async function AdminPhotoMenu( props: ComponentProps, ) { - const session = await authCached(); + const session = await authCachedSafe(); return Boolean(session?.user?.email) ? : null; diff --git a/src/auth/cache.ts b/src/auth/cache.ts index c5ebeb47..84ac9f5d 100644 --- a/src/auth/cache.ts +++ b/src/auth/cache.ts @@ -1,4 +1,6 @@ import { cache } from 'react'; import { auth } from '@/auth'; +import { screenForPPR } from '@/utility/ppr'; -export const authCached = cache(auth); +export const authCachedSafe = cache(() => auth() + .catch(e => screenForPPR(e, null, 'auth'))); diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index b0428d69..2a37576c 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -27,6 +27,7 @@ import { isUrlFromCloudflareR2, } from './cloudflare-r2'; import { PATH_API_PRESIGNED_URL } from '@/site/paths'; +import { screenForPPR } from '@/utility/ppr'; export const generateStorageId = () => generateNanoid(16); @@ -191,13 +192,16 @@ const getStorageUrlsForPrefix = async (prefix = '') => { const urls: StorageListResponse = []; if (HAS_VERCEL_BLOB_STORAGE) { - urls.push(...await vercelBlobList(prefix)); + urls.push(...await vercelBlobList(prefix) + .catch(e => screenForPPR(e, [], 'vercel blob'))); } if (HAS_AWS_S3_STORAGE) { - urls.push(...await awsS3List(prefix)); + urls.push(...await awsS3List(prefix) + .catch(e => screenForPPR(e, [], 'aws blob'))); } if (HAS_CLOUDFLARE_R2_STORAGE) { - urls.push(...await cloudflareR2List(prefix)); + urls.push(...await cloudflareR2List(prefix) + .catch(e => screenForPPR(e, [], 'cloudflare blob'))); } return urls diff --git a/src/services/vercel-postgres.ts b/src/services/vercel-postgres.ts index c7ffa059..7544de0d 100644 --- a/src/services/vercel-postgres.ts +++ b/src/services/vercel-postgres.ts @@ -12,6 +12,7 @@ import { parameterize } from '@/utility/string'; import { Tags } from '@/tag'; import { FilmSimulation, FilmSimulations } from '@/simulation'; import { PRIORITY_ORDER_ENABLED } from '@/site/config'; +import { screenForPPR } from '@/utility/ppr'; const PHOTO_DEFAULT_LIMIT = 100; @@ -283,11 +284,8 @@ const safelyQueryPhotos = async (callback: () => Promise): Promise => { try { result = await callback(); } catch (e: any) { - if (/ppr-caught-error/.test(e.message) && e.sourceError) { - // PPR errors, if caught, must be re-thrown in order to - // postpone rendering - throw e.sourceError; - } else if (/relation "photos" does not exist/i.test(e.message)) { + screenForPPR(e, undefined, 'neon postgres'); + if (/relation "photos" does not exist/i.test(e.message)) { console.log('Creating table "photos" because it did not exist'); await sqlCreatePhotosTable(); result = await callback(); diff --git a/src/site/CommandK.tsx b/src/site/CommandK.tsx index 68ef8fe4..66ba3924 100644 --- a/src/site/CommandK.tsx +++ b/src/site/CommandK.tsx @@ -17,7 +17,7 @@ import { pathForTag, } from './paths'; import { formatCameraText } from '@/camera'; -import { authCached } from '@/auth/cache'; +import { authCachedSafe } from '@/auth/cache'; import { getPhotos } from '@/services/vercel-postgres'; import { photoQuantityText, titleForPhoto } from '@/photo'; import PhotoTiny from '@/photo/PhotoTiny'; @@ -45,7 +45,7 @@ export default async function CommandK() { getUniqueFilmSimulationsCached().catch(() => []), ]); - const session = await authCached().catch(() => null); + const session = await authCachedSafe(); const isAdminLoggedIn = Boolean(session?.user?.email); diff --git a/src/site/Footer.tsx b/src/site/Footer.tsx index bd91dfc1..9effbb91 100644 --- a/src/site/Footer.tsx +++ b/src/site/Footer.tsx @@ -1,9 +1,8 @@ -import { authCached } from '@/auth/cache'; +import { authCachedSafe } from '@/auth/cache'; import FooterClient from './FooterClient'; export default async function Footer() { - // Make footer auth resilient to error on first time setup - const session = await authCached().catch(() => null); + const session = await authCachedSafe(); return ( ); diff --git a/src/site/Nav.tsx b/src/site/Nav.tsx index c090f1ea..0526b141 100644 --- a/src/site/Nav.tsx +++ b/src/site/Nav.tsx @@ -1,9 +1,8 @@ -import { authCached } from '@/auth/cache'; +import { authCachedSafe } from '@/auth/cache'; import NavClient from './NavClient'; export default async function Nav() { - // Make nav auth resilient to error on first time setup - const session = await authCached().catch(() => null); + const session = await authCachedSafe(); return ( ); diff --git a/src/utility/ppr.ts b/src/utility/ppr.ts new file mode 100644 index 00000000..1194583c --- /dev/null +++ b/src/utility/ppr.ts @@ -0,0 +1,18 @@ +export const screenForPPR = ( + error: any, + fallback: T, + sourceToLog?: string, +): T => { + if (/ppr-caught-error/.test(error.message) && error.sourceError) { + // PPR errors, if caught, must be re-thrown in order to + // postpone rendering + console.log( + sourceToLog ? `${sourceToLog}: PPR error caught` : 'PPR error caught', + error.sourceError, + ); + throw error.sourceError; + } else if (sourceToLog) { + console.error(sourceToLog, error.sourceError); + } + return fallback; +};