From 820e495d2d106136475113feb41a9739e6eac987 Mon Sep 17 00:00:00 2001 From: ThatGuySam Date: Mon, 6 Apr 2026 10:31:57 -0500 Subject: [PATCH] Keep redirect lookups from crashing SSR pages Dynamic Astro routes were reading Netlify redirect config through a cwd-relative path, which is fragile inside a serverless runtime and was taking detail pages down with 500s before render. Resolve netlify.toml by searching from the module directory and current working directory, and fail open in request-time redirect lookup so a config read problem does not block page rendering. Constraint: Netlify serverless cwd is not guaranteed to be the repo root Rejected: Inline redirects into route modules | would duplicate platform config and drift from source of truth Rejected: Leave redirect lookup hard-failing | one config read failure should not take down unrelated pages Confidence: medium Scope-risk: narrow Reversibility: clean Directive: Keep redirect config lookup independent of process cwd anywhere server code reads deploy config files Tested: vitest ./test/prebuild/config-node.test.js; pnpm run netlify-build Not-tested: live Netlify production deploy before push --- helpers/astro/request.js | 9 +++++-- helpers/config-node.js | 38 +++++++++++++++++++++++++++- test/prebuild/config-node.test.js | 41 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 test/prebuild/config-node.test.js diff --git a/helpers/astro/request.js b/helpers/astro/request.js index 5c9efb9..959d446 100644 --- a/helpers/astro/request.js +++ b/helpers/astro/request.js @@ -56,7 +56,13 @@ export async function applyResponseDefaults ( Astro ) { export async function catchRedirectResponse ( Astro ) { const requestUrl = new URL( Astro.request.url ) - const netlifyRedirectUrl = await getNetlifyRedirect( requestUrl.pathname ) + let netlifyRedirectUrl = null + + try { + netlifyRedirectUrl = await getNetlifyRedirect( requestUrl.pathname ) + } catch ( error ) { + console.warn( `Skipping redirect lookup for ${ requestUrl.pathname }`, error ) + } // console.log('netlifyRedirectUrl', netlifyRedirectUrl) @@ -67,4 +73,3 @@ export async function catchRedirectResponse ( Astro ) { return null } - diff --git a/helpers/config-node.js b/helpers/config-node.js index c24acef..75dec1a 100644 --- a/helpers/config-node.js +++ b/helpers/config-node.js @@ -1,5 +1,7 @@ import TOML from '@iarna/toml' import fs from 'fs-extra' +import path from 'path' +import { fileURLToPath } from 'url' import pkg from '~/package.json' import { publicRuntimeConfig } from '~/helpers/public-runtime-config.mjs' @@ -8,6 +10,7 @@ import { getRouteType } from '~/helpers/app-derived.js' export const siteUrl = getSiteUrl() +const currentModuleDirectory = path.dirname( fileURLToPath( import.meta.url ) ) export const nuxtHead = { // this htmlAttrs you need @@ -113,8 +116,41 @@ export const nuxtHead = { +export async function getNetlifyConfigPath () { + const searchDirectories = new Set() + + // Local dev usually runs from repo root, but deployed serverless + // functions may execute from a nested working directory. + for ( const baseDirectory of [ + process.cwd(), + currentModuleDirectory, + ] ) { + let directory = baseDirectory + + while ( true ) { + searchDirectories.add( directory ) + + const parentDirectory = path.dirname( directory ) + + if ( parentDirectory === directory ) break + + directory = parentDirectory + } + } + + for ( const directory of searchDirectories ) { + const configPath = path.join( directory, 'netlify.toml' ) + + if ( await fs.pathExists( configPath ) ) { + return configPath + } + } + + throw new Error( 'Could not find netlify.toml' ) +} + export async function getNetlifyConfig () { - const configPath = './netlify.toml' + const configPath = await getNetlifyConfigPath() const tomlContent = await fs.readFile(configPath, 'utf-8') const netlifyConfig = TOML.parse(tomlContent) diff --git a/test/prebuild/config-node.test.js b/test/prebuild/config-node.test.js new file mode 100644 index 0000000..0a16617 --- /dev/null +++ b/test/prebuild/config-node.test.js @@ -0,0 +1,41 @@ +import fs from 'fs-extra' +import os from 'os' +import path from 'path' +import { afterEach, describe, expect, it } from 'vitest' + +import { + getNetlifyConfigPath, + getNetlifyRedirect +} from '~/helpers/config-node.js' + +const originalCwd = process.cwd() + +afterEach(() => { + process.chdir( originalCwd ) +}) + +describe( 'netlify config helpers', () => { + it( 'resolves netlify.toml even when cwd is outside the repo root', async () => { + const tempDirectory = await fs.mkdtemp( path.join( os.tmpdir(), 'doesitarm-netlify-' ) ) + + process.chdir( tempDirectory ) + + const configPath = await getNetlifyConfigPath() + + expect( configPath ).toBe( path.join( originalCwd, 'netlify.toml' ) ) + }) + + it( 'loads redirects when cwd is outside the repo root', async () => { + const tempDirectory = await fs.mkdtemp( path.join( os.tmpdir(), 'doesitarm-netlify-' ) ) + + process.chdir( tempDirectory ) + + const redirect = await getNetlifyRedirect( '/app/electron' ) + + expect( redirect ).toMatchObject({ + from: '/app/electron', + to: '/app/electron-framework', + status: 301 + }) + }) +})