Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

fix: if the purge api call fails, include the api response body in the thrown error's message #571

Merged
merged 2 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
extends: '@netlify/eslint-config-node',
rules: {
'max-statements': 'off',
'max-lines': 'off',
},
overrides: [
...overrides,
Expand Down
50 changes: 46 additions & 4 deletions src/lib/purge_cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ test('Calls the purge API endpoint and returns `undefined` if the operation was
expect(mockAPI.fulfilled).toBeTruthy()
})

test('Throws if the API response does not have a successful status code', async () => {
test('Throws an error if the API response does not have a successful status code, using the response body as part of the error message', async () => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return
}

const mockSiteID = '123456789'
Expand All @@ -77,7 +79,7 @@ test('Throws if the API response does not have a successful status code', async
},
headers: { Authorization: `Bearer ${mockToken}` },
method: 'post',
response: new Response(null, { status: 500 }),
response: new Response('site not found', { status: 404 }),
url: `https://api.netlify.com/api/v1/purge`,
})
// eslint-disable-next-line unicorn/consistent-function-scoping
Expand All @@ -90,14 +92,54 @@ test('Throws if the API response does not have a successful status code', async
try {
await invokeLambda(myFunction)

throw new Error('Invocation should have failed')
expect.fail('Invocation should have failed')
} catch (error) {
expect((error as NodeJS.ErrnoException).message).toBe(
'Cache purge API call returned an unexpected status code: 500',
'Cache purge API call was unsuccessful.\nStatus: 404\nBody: site not found',
)
}
})

test('Throws if the API response does not have a successful status code, does not include the response body if it is not text', async () => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')

return
}

const mockSiteID = '123456789'
const mockToken = '1q2w3e4r5t6y7u8i9o0p'

process.env.NETLIFY_PURGE_API_TOKEN = mockToken
process.env.SITE_ID = mockSiteID

const mockAPI = new MockFetch().post({
body: (payload: string) => {
const data = JSON.parse(payload)

expect(data.site_id).toBe(mockSiteID)
},
headers: { Authorization: `Bearer ${mockToken}` },
method: 'post',
response: new Response(null, { status: 500 }),
url: `https://api.netlify.com/api/v1/purge`,
})
// eslint-disable-next-line unicorn/consistent-function-scoping
const myFunction = async () => {
await purgeCache()
}

globalThis.fetch = mockAPI.fetcher

try {
await invokeLambda(myFunction)

throw new Error('Invocation should have failed')
} catch (error) {
expect((error as NodeJS.ErrnoException).message).toBe('Cache purge API call was unsuccessful.\nStatus: 500')
}
})

test('Ignores purgeCache if in local dev with no token or site', async () => {
if (!hasFetchAPI) {
console.warn('Skipping test requires the fetch API')
Expand Down
31 changes: 22 additions & 9 deletions src/lib/purge_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export const purgeCache = async (options: PurgeCacheOptions = {}) => {
)
}

const { siteID } = options as PurgeCacheOptionsWithSiteID
const { siteSlug } = options as PurgeCacheOptionsWithSiteSlug
const { domain } = options as PurgeCacheOptionsWithDomain

if ((siteID && siteSlug) || (siteID && domain) || (siteSlug && domain)) {
throw new Error('Can only pass one of either "siteID", "siteSlug", or "domain"')
}

const payload: PurgeAPIPayload = {
cache_tags: options.tags,
deploy_alias: options.deployAlias,
Expand All @@ -50,22 +58,20 @@ export const purgeCache = async (options: PurgeCacheOptions = {}) => {
return
}

if ('siteSlug' in options) {
payload.site_slug = options.siteSlug
} else if ('domain' in options) {
payload.domain = options.domain
if (siteSlug) {
payload.site_slug = siteSlug
} else if (domain) {
payload.domain = domain
} else {
// The `siteID` from `options` takes precedence over the one from the
// environment.
const siteID = options.siteID || env.SITE_ID
payload.site_id = siteID || env.SITE_ID

if (!siteID) {
if (!payload.site_id) {
throw new Error(
'The Netlify site ID was not found in the execution environment. Please supply it manually using the `siteID` property.',
)
}

payload.site_id = siteID
}

if (!token) {
Expand All @@ -91,6 +97,13 @@ export const purgeCache = async (options: PurgeCacheOptions = {}) => {
})

if (!response.ok) {
throw new Error(`Cache purge API call returned an unexpected status code: ${response.status}`)
let text
try {
text = await response.text()
} catch {}
if (text) {
throw new Error(`Cache purge API call was unsuccessful.\nStatus: ${response.status}\nBody: ${text}`)
}
throw new Error(`Cache purge API call was unsuccessful.\nStatus: ${response.status}`)
}
}