mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-15 06:35:20 -07:00
fix(build): retry transient sitemap endpoint fetches
The TypeScript helper refactor exposed a separate production risk: builds can fail when the sitemap-endpoints API returns a transient 5xx during Pagefind index generation. Move that fetch into a small helper with retry logic and add a focused prebuild test so this failure mode is caught without waiting on a full deploy. Constraint: Must keep Pagefind index generation behavior the same when the endpoint is healthy Rejected: Ignore the failure as external-only | transient 5xx responses can still block deploys and CI repeatedly Confidence: high Scope-risk: narrow Reversibility: clean Directive: When external build inputs can fail transiently, add a small retryable helper plus a direct test before relying on full deploy verification Tested: pnpm exec vitest run test/prebuild/load-sitemap-endpoints.test.ts; pnpm run typecheck; pnpm run test-prebuild Not-tested: Full production redeploy completion before push
This commit is contained in:
parent
cd41143f0d
commit
f1cb66c477
3 changed files with 159 additions and 21 deletions
63
helpers/pagefind/load-sitemap-endpoints.ts
Normal file
63
helpers/pagefind/load-sitemap-endpoints.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import fs from 'fs-extra'
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
sitemapEndpointsPath
|
||||
} from '~/helpers/pagefind/config.js'
|
||||
|
||||
function shouldRetryError ( error: unknown ) {
|
||||
const status = ( error as { response?: { status?: number } } )?.response?.status
|
||||
|
||||
return typeof status === 'number' && status >= 500
|
||||
}
|
||||
|
||||
async function fetchJsonWithRetries (
|
||||
url: string,
|
||||
{
|
||||
attempts = 3,
|
||||
delayMs = 1000
|
||||
}: {
|
||||
attempts?: number
|
||||
delayMs?: number
|
||||
} = {}
|
||||
) {
|
||||
let lastError: unknown
|
||||
|
||||
for ( let attempt = 1; attempt <= attempts; attempt += 1 ) {
|
||||
try {
|
||||
const response = await axios.get( url )
|
||||
|
||||
return response.data
|
||||
} catch ( error ) {
|
||||
lastError = error
|
||||
|
||||
if ( attempt >= attempts || !shouldRetryError( error ) ) {
|
||||
throw error
|
||||
}
|
||||
|
||||
await new Promise( resolve => setTimeout( resolve, delayMs ) )
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
export async function loadSitemapEndpoints () {
|
||||
if ( await fs.pathExists( sitemapEndpointsPath ) ) {
|
||||
return await fs.readJson( sitemapEndpointsPath )
|
||||
}
|
||||
|
||||
if ( !process.env.PUBLIC_API_DOMAIN ) {
|
||||
throw new Error(`Missing ${ sitemapEndpointsPath } and PUBLIC_API_DOMAIN is not set`)
|
||||
}
|
||||
|
||||
const apiUrl = new URL( process.env.PUBLIC_API_DOMAIN )
|
||||
apiUrl.pathname = sitemapEndpointsPath.replace(/^\.?\/?static\//, '/')
|
||||
|
||||
return await fetchJsonWithRetries( apiUrl.toString() )
|
||||
}
|
||||
|
||||
export {
|
||||
fetchJsonWithRetries,
|
||||
shouldRetryError
|
||||
}
|
||||
|
|
@ -1,31 +1,12 @@
|
|||
import fs from 'fs-extra'
|
||||
import axios from 'axios'
|
||||
import 'dotenv/config.js'
|
||||
|
||||
import {
|
||||
sitemapEndpointsPath
|
||||
} from '~/helpers/pagefind/config.js'
|
||||
loadSitemapEndpoints
|
||||
} from '~/helpers/pagefind/load-sitemap-endpoints'
|
||||
import {
|
||||
writePagefindIndex
|
||||
} from '~/helpers/pagefind/index.js'
|
||||
|
||||
async function loadSitemapEndpoints () {
|
||||
if ( await fs.pathExists( sitemapEndpointsPath ) ) {
|
||||
return await fs.readJson( sitemapEndpointsPath )
|
||||
}
|
||||
|
||||
if ( !process.env.PUBLIC_API_DOMAIN ) {
|
||||
throw new Error(`Missing ${ sitemapEndpointsPath } and PUBLIC_API_DOMAIN is not set`)
|
||||
}
|
||||
|
||||
const apiUrl = new URL( process.env.PUBLIC_API_DOMAIN )
|
||||
apiUrl.pathname = sitemapEndpointsPath.replace(/^\.?\/?static\//, '/')
|
||||
|
||||
const response = await axios.get( apiUrl.toString() )
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const sitemapEndpoints = await loadSitemapEndpoints()
|
||||
const {
|
||||
|
|
|
|||
94
test/prebuild/load-sitemap-endpoints.test.ts
Normal file
94
test/prebuild/load-sitemap-endpoints.test.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
import {
|
||||
fetchJsonWithRetries,
|
||||
shouldRetryError
|
||||
} from '~/helpers/pagefind/load-sitemap-endpoints'
|
||||
|
||||
vi.mock( 'axios', () => {
|
||||
return {
|
||||
default: {
|
||||
get: vi.fn()
|
||||
}
|
||||
}
|
||||
} )
|
||||
|
||||
describe( 'load sitemap endpoints helper', () => {
|
||||
beforeEach( () => {
|
||||
vi.mocked( axios.get ).mockReset()
|
||||
} )
|
||||
|
||||
it( 'retries transient 5xx errors and eventually resolves', async () => {
|
||||
const axiosGet = vi.mocked( axios.get )
|
||||
|
||||
axiosGet
|
||||
.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 502
|
||||
}
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
ok: true
|
||||
}
|
||||
} )
|
||||
|
||||
const data = await fetchJsonWithRetries( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
attempts: 2,
|
||||
delayMs: 0
|
||||
} )
|
||||
|
||||
expect( data ).toEqual({
|
||||
ok: true
|
||||
} )
|
||||
expect( axiosGet ).toHaveBeenCalledTimes( 2 )
|
||||
} )
|
||||
|
||||
it( 'does not retry non-5xx errors', async () => {
|
||||
const axiosGet = vi.mocked( axios.get )
|
||||
|
||||
axiosGet.mockRejectedValueOnce({
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
})
|
||||
|
||||
await expect( fetchJsonWithRetries( 'https://api.doesitarm.com/sitemap-endpoints.json', {
|
||||
attempts: 3,
|
||||
delayMs: 0
|
||||
} ) ).rejects.toEqual({
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
})
|
||||
|
||||
expect( axiosGet ).toHaveBeenCalledTimes( 1 )
|
||||
} )
|
||||
|
||||
it( 'classifies retryable server errors', () => {
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 502
|
||||
}
|
||||
} ) ).toBe( true )
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 503
|
||||
}
|
||||
} ) ).toBe( true )
|
||||
expect( shouldRetryError( {
|
||||
response: {
|
||||
status: 404
|
||||
}
|
||||
} ) ).toBe( false )
|
||||
expect( shouldRetryError( new Error( 'network' ) ) ).toBe( false )
|
||||
} )
|
||||
} )
|
||||
Loading…
Add table
Add a link
Reference in a new issue