mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
### What? Switch Turbopack's hash encoding charset from base40 (`0-9 a-z _ - ~ .`) to base38 (`0-9 a-z _ -`), removing the `~` and `.` characters. Pure rename/charset change — no structural changes. ### Why? The `~` and `.` characters in base40-encoded filenames are blocked by standard Nginx hardening rules (`block_common_exploits.conf`) and enterprise WAF configurations, causing **403 Forbidden errors** when applications are deployed behind security-hardened infrastructure. Examples of problematic filenames: - `turbopack-0c3o1svijj_~~.js` — `~~` flagged as directory traversal / injection - `0...f7~att2_2.js` — `...` flagged as path traversal attempt - `0q~2copru0zy0.css` — `~` filtered by some WAF rulesets Previous hex-only filenames (e.g. `turbopack-01ca012029ca2e66.js`) had no such issues. Fixes https://github.com/vercel/next.js/issues/91678 ### How? **Charset change** (`turbo-tasks-hash/src/base38.rs`, renamed from `base40.rs`): - Alphabet reduced from 40 to 38 characters: `0123456789abcdefghijklmnopqrstuvwxyz_-` - All constants and functions renamed: `BASE40_*` → `BASE38_*`, `encode_base40` → `encode_base38` - Hash lengths unchanged: 13 chars for 64-bit, 25 chars for 128-bit (`38^13 > 2^64`, `38^25 > 2^128`) - Content hash length stays at 13 (68.2 bits vs 69.2 bits with base40 — negligible) **`HashAlgorithm` enum variants** (`turbo-tasks-hash/src/lib.rs`): - `Xxh3Hash64Base40` → `Xxh3Hash64Base38` - `Xxh3Hash128Base40` → `Xxh3Hash128Base38` **Bit computation comment** (`turbopack-core/src/ident.rs`): - Updated `7 base38 chars ≈ 37 bits` → `≈ 36 bits` (log2(38) × 7 = 36.7) **Test regex patterns** (15 test files): - Updated `[0-9a-z_.~-]` → `[0-9a-z_-]` to match the new charset --------- Co-authored-by: Tobias Koppers <[email protected]> Co-authored-by: Claude <[email protected]>
115 lines
3.5 KiB
TypeScript
115 lines
3.5 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
|
|
import path from 'path'
|
|
import { promises as fs } from 'fs'
|
|
import { readJson } from 'fs-extra'
|
|
|
|
describe('Edge Compiler can import asset assets', () => {
|
|
const { next, isTurbopack, isNextDeploy } = nextTestSetup({
|
|
files: path.join(__dirname, './app'),
|
|
})
|
|
|
|
it('allows to fetch a remote URL', async () => {
|
|
const response = await fetchViaHTTP(next.url, '/api/edge', {
|
|
handler: 'remote-full',
|
|
})
|
|
expect(await response.text()).toContain('Example Domain')
|
|
})
|
|
|
|
it('allows to fetch a remote URL with a path and basename', async () => {
|
|
const response = await fetchViaHTTP(next.url, '/api/edge', {
|
|
handler: 'remote-with-base',
|
|
})
|
|
expect(await response.text()).toContain('Example Domain')
|
|
})
|
|
|
|
it('allows to fetch text assets', async () => {
|
|
const html = await renderViaHTTP(next.url, '/api/edge', {
|
|
handler: 'text-file',
|
|
})
|
|
expect(html).toContain('Hello, from text-file.txt!')
|
|
})
|
|
|
|
it('allows to fetch image assets', async () => {
|
|
const response = await fetchViaHTTP(next.url, '/api/edge', {
|
|
handler: 'image-file',
|
|
})
|
|
const buffer: Buffer = await response.buffer()
|
|
const image = await fs.readFile(
|
|
path.join(__dirname, './app/src/vercel.png')
|
|
)
|
|
expect(buffer.equals(image)).toBeTrue()
|
|
})
|
|
|
|
it('allows to assets from node_modules', async () => {
|
|
const response = await fetchViaHTTP(next.url, '/api/edge', {
|
|
handler: 'from-node-module',
|
|
})
|
|
const json = await response.json()
|
|
expect(json).toEqual({
|
|
'i am': 'a node dependency',
|
|
})
|
|
})
|
|
|
|
// Cannot read the file on deployment
|
|
if (!isNextDeploy) {
|
|
it('extracts all the assets from the bundle', async () => {
|
|
const manifestPath = path.join(
|
|
next.testDir,
|
|
next.distDir,
|
|
'server/middleware-manifest.json'
|
|
)
|
|
const manifest = await readJson(manifestPath)
|
|
const orderedAssets = manifest.functions['/api/edge'].assets.sort(
|
|
(a, z) => {
|
|
return String(a.name).localeCompare(z.name)
|
|
}
|
|
)
|
|
|
|
if (isTurbopack) {
|
|
expect(orderedAssets).toMatchObject([
|
|
{
|
|
name: expect.stringMatching(
|
|
/server\/edge\/assets\/text-file\.[0-9a-z_-]+\.txt$/
|
|
),
|
|
filePath: expect.stringMatching(/^server\/edge\/assets\/text-file/),
|
|
},
|
|
{
|
|
name: expect.stringMatching(
|
|
/^server\/edge\/assets\/vercel\.[0-9a-z_-]+\.png$/
|
|
),
|
|
filePath: expect.stringMatching(/^server\/edge\/assets\/vercel/),
|
|
},
|
|
{
|
|
name: expect.stringMatching(
|
|
/^server\/edge\/assets\/world\.[0-9a-z_-]+\.json/
|
|
),
|
|
filePath: expect.stringMatching(/^server\/edge\/assets\/world/),
|
|
},
|
|
])
|
|
} else {
|
|
expect(orderedAssets).toMatchObject([
|
|
{
|
|
name: expect.stringMatching(/^text-file\.[0-9a-f]{16}\.txt$/),
|
|
filePath: expect.stringMatching(
|
|
/^server\/edge-chunks\/asset_text-file/
|
|
),
|
|
},
|
|
{
|
|
name: expect.stringMatching(/^vercel\.[0-9a-f]{16}\.png$/),
|
|
filePath: expect.stringMatching(
|
|
/^server\/edge-chunks\/asset_vercel/
|
|
),
|
|
},
|
|
{
|
|
name: expect.stringMatching(/^world\.[0-9a-f]{16}\.json/),
|
|
filePath: expect.stringMatching(
|
|
/^server\/edge-chunks\/asset_world/
|
|
),
|
|
},
|
|
])
|
|
}
|
|
})
|
|
}
|
|
})
|