Skip to content

feat: make CDN SWR background revalidation discard stale cache content in order to produce fresh responses #2765

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
test: add e2e case testing storing fresh responses in cdn when handli…
…ng background SWR requests
  • Loading branch information
pieh committed Feb 28, 2025
commit 87fe9b4bd1691c4a6a0f2d40f620bd72fa1e0d8f
57 changes: 57 additions & 0 deletions tests/e2e/page-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,63 @@ test.describe('Simple Page Router (no basePath, no i18n)', () => {
expect(beforeFetch.localeCompare(date2)).toBeLessThan(0)
})

test('Background SWR invocations can store fresh responses in CDN cache', async ({
page,
pageRouter,
}) => {
const slug = Date.now()
const pathname = `/revalidate-60/${slug}`

const beforeFirstFetch = new Date().toISOString()

const response1 = await page.goto(new URL(pathname, pageRouter.url).href)
expect(response1?.status()).toBe(200)
expect(response1?.headers()['cache-status']).toMatch(
/"Netlify (Edge|Durable)"; fwd=(uri-miss(; stored)?|miss)/m,
)
expect(response1?.headers()['netlify-cdn-cache-control']).toMatch(
/s-maxage=60, stale-while-revalidate=[0-9]+, durable/,
)

// ensure response was NOT produced before invocation
const date1 = (await page.textContent('[data-testid="date-now"]')) ?? ''
expect(date1.localeCompare(beforeFirstFetch)).toBeGreaterThan(0)

// allow page to get stale
await page.waitForTimeout(60_000)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( need for Durable so reduce potential flakiness if different edge nodes would be used - tested page has 60s revalidate time because of that as well


const response2 = await page.goto(new URL(pathname, pageRouter.url).href)
expect(response2?.status()).toBe(200)
expect(response2?.headers()['cache-status']).toMatch(
/"Netlify (Edge|Durable)"; hit; fwd=stale/m,
)
expect(response2?.headers()['netlify-cdn-cache-control']).toMatch(
/s-maxage=60, stale-while-revalidate=[0-9]+, durable/,
)

const date2 = (await page.textContent('[data-testid="date-now"]')) ?? ''
expect(date2).toBe(date1)

// wait a bit to ensure background work has a chance to finish
// (it should take at least 5 seconds to regenerate, so we should wait at least that much to get fresh response)
await page.waitForTimeout(10_000)

// subsequent request should be served with fresh response from cdn cache, as previous request
// should result in background SWR invocation that serves fresh response that was stored in CDN cache
const response3 = await page.goto(new URL(pathname, pageRouter.url).href)
expect(response3?.status()).toBe(200)
expect(response3?.headers()['cache-status']).toMatch(
// hit, without being followed by ';fwd=stale'
/"Netlify (Edge|Durable)"; hit(?!; fwd=stale)/m,
)
expect(response3?.headers()['netlify-cdn-cache-control']).toMatch(
/s-maxage=60, stale-while-revalidate=[0-9]+, durable/,
)

const date3 = (await page.textContent('[data-testid="date-now"]')) ?? ''
expect(date3.localeCompare(date2)).toBeGreaterThan(0)
})

test('should serve 404 page when requesting non existing page (no matching route)', async ({
page,
pageRouter,
Expand Down
41 changes: 41 additions & 0 deletions tests/fixtures/page-router/netlify/functions/purge-cdn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { purgeCache, Config } from '@netlify/functions'

export default async function handler(request: Request) {
const url = new URL(request.url)
const pathToPurge = url.searchParams.get('path')

if (!pathToPurge) {
return Response.json(
{
status: 'error',
error: 'missing "path" query parameter',
},
{ status: 400 },
)
}
try {
await purgeCache({ tags: [`_N_T_${encodeURI(pathToPurge)}`] })
return Response.json(
{
status: 'ok',
},
{
status: 200,
},
)
} catch (error) {
return Response.json(
{
status: 'error',
error: error.toString(),
},
{
status: 500,
},
)
}
}

export const config: Config = {
path: '/api/purge-cdn',
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had some troubles with this function being in Next.js api handler - in the end I don't rely on it for added test, but overall it doesn't really make sense to use Next.js API for it, if it can use just vanilla Netlify function, as it only purge cdn cache, and not any Next.js caches in blobs

25 changes: 0 additions & 25 deletions tests/fixtures/page-router/pages/api/purge-cdn.js

This file was deleted.

35 changes: 35 additions & 0 deletions tests/fixtures/page-router/pages/revalidate-60/[slug].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const Show = ({ time, easyTimeToCompare, slug }) => (
<div>
<p>
This page uses getStaticProps() at
<span data-testid="date-now">{time}</span>
</p>
<p>
Time string: <span data-testid="date-easy-time">{easyTimeToCompare}</span>
</p>
<p>Slug {slug}</p>
</div>
)

/** @type {import('next').getStaticPaths} */
export const getStaticPaths = () => {
return {
paths: [],
fallback: 'blocking',
}
}

/** @type {import('next').GetStaticProps} */
export async function getStaticProps({ params }) {
const date = new Date()
return {
props: {
slug: params.slug,
time: date.toISOString(),
easyTimeToCompare: date.toTimeString(),
},
revalidate: 60,
}
}

export default Show
Loading