Skip to content

Commit

Permalink
ensure handleUnlock is called even for non-cached responses (#70766)
Browse files Browse the repository at this point in the history
When a response is unsuccessful, we might still have acquired an
incremental cache lock and only realized we couldn't cache it after
receiving a non-success response code. `handleUnlock` is called in the
case of a successful response or a response received during a dynamic
render, but not in the case of an unsuccessful response. This adds the
missing `handleUnlock` for non-cached responses.

This used to be handled appropriately but was lost in some recent
refactors.

No explicit test case was added here as the reproduction in the failing
case was just a hanging build. So if the build passes and does not hang,
the test was successful. Validated it was hanging before implementing
this fix.
  • Loading branch information
ztanner authored Oct 4, 2024
1 parent 4f56a33 commit f8096a1
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 4 deletions.
4 changes: 4 additions & 0 deletions packages/next/src/server/lib/patch-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,10 @@ export function createPatchedFetcher(
}
}

// we had response that we determined shouldn't be cached so we return it
// and don't cache it. This also needs to unlock the cache lock we acquired.
await handleUnlock()

return res
})
}
Expand Down
18 changes: 14 additions & 4 deletions test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { findPort, waitFor } from 'next-test-utils'
import http from 'http'
import url from 'url'
import { outdent } from 'outdent'
import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils'

Expand All @@ -9,12 +10,21 @@ describe('app-fetch-deduping', () => {
const { next } = nextTestSetup({ files: __dirname, skipStart: true })
let externalServerPort: number
let externalServer: http.Server
let requests = []
let successfulRequests = []

beforeAll(async () => {
externalServerPort = await findPort()
externalServer = http.createServer((req, res) => {
requests.push(req.url)
const parsedUrl = url.parse(req.url, true)
const overrideStatus = parsedUrl.query.status

// if the requested url has a "status" search param, override the response status
if (overrideStatus) {
res.statusCode = Number(overrideStatus)
} else {
successfulRequests.push(req.url)
}

res.end(`Request ${req.url} received at ${Date.now()}`)
})

Expand All @@ -30,7 +40,7 @@ describe('app-fetch-deduping', () => {
})

beforeEach(() => {
requests = []
successfulRequests = []
})

afterAll(() => externalServer.close())
Expand All @@ -43,7 +53,7 @@ describe('app-fetch-deduping', () => {
}`
)
await next.build()
expect(requests.length).toBe(1)
expect(successfulRequests.length).toBe(1)
})
})
} else if (isNextDev) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// This page is validating that despite multiple unsuccessful responses to the same, potentially cached URLs,
// the build locks are resolved and the page is still built successfully.
export async function generateStaticParams() {
return [
{
slug: 'slug-0',
},
{
slug: 'slug-1',
},
{
slug: 'slug-2',
},
{
slug: 'slug-3',
},
{
slug: 'slug-4',
},
]
}

export default async function Page({ params }) {
const data = await fetch(
`http://localhost:${process.env.TEST_SERVER_PORT}?status=404`
).then((res) => res.text())
await fetch(
`http://localhost:${process.env.TEST_SERVER_PORT}?status=404`
).then((res) => res.text())

return (
<>
<p>hello world</p>
<p id="data">{data}</p>
</>
)
}

0 comments on commit f8096a1

Please sign in to comment.