Skip to content
Open
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
4 changes: 0 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions packages/headers-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@
"license": "MIT",
"dependencies": {
"smol-toml": "^1.5.2",
"escape-string-regexp": "^5.0.0",
"fast-safe-stringify": "^2.0.7",
"is-plain-obj": "^4.0.0",
"map-obj": "^5.0.0",
"path-exists": "^5.0.0"
"escape-string-regexp": "^5.0.0"
},
"devDependencies": {
"@types/node": "^22.12.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/headers-parser/src/line_parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import fs from 'fs/promises'

import { pathExists } from 'path-exists'

import { splitResults } from './results.js'
import type { MinimalHeader } from './types.js'

Expand All @@ -23,7 +21,9 @@ export const parseFileHeaders = async function (headersFile: string): Promise<Pa
}

const parseHeaders = async function (headersFile: string): Promise<(Error | RawHeaderFileLine)[]> {
if (!(await pathExists(headersFile))) {
try {
await fs.access(headersFile)
} catch {
Comment on lines +24 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
fd -p 'packages/headers-parser/src/(line_parser|netlify_config_parser)\.ts$' . || true

echo "== relevant line_parser.ts =="
if [ -f packages/headers-parser/src/line_parser.ts ]; then
  nl -ba packages/headers-parser/src/line_parser.ts | sed -n '1,80p'
fi

echo "== relevant netlify_config_parser.ts =="
if [ -f packages/headers-parser/src/netlify_config_parser.ts ]; then
  nl -ba packages/headers-parser/src/netlify_config_parser.ts | sed -n '1,120p'
fi

echo "== search fs.access and parseConfig usages =="
rg -n "fs\.access|parseConfig|splitResults|parseHeadersFile|read text|readFileSync" packages/headers-parser/src packages/headers-parser -S || true

Repository: netlify/build

Length of output: 340


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package files mentioning headers-parser =="
rg -n "headers-parser|line_parser|netlify_config_parser|parseHeadersFile" package.json pnpm-workspace.yaml packages -S || true

echo "== TypeScript fs access definitions availability =="
pnpm list fs-extra 2>/dev/null | head -20 || true
rg -n "from '(fs-extra|fs)'|require\('fs'|require\('fs-extra'\)" packages/headers-parser/src/tsconfig.json -S || true

Repository: netlify/build

Length of output: 15650


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== line_parser.ts relevant sections =="
awk 'NR>=1 && NR<=120 { printf "%4d  %s\n", NR, $0 }' packages/headers-parser/src/line_parser.ts

echo "== netlify_config_parser.ts relevant sections =="
awk 'NR>=1 && NR<=160 { printf "%4d  %s\n", NR, $0 }' packages/headers-parser/src/netlify_config_parser.ts

echo "== all fs.access/access/read usages in headers-parser source =="
python3 - <<'PY'
from pathlib import Path
for path in sorted(Path("packages/headers-parser/src").rglob("*.ts")):
    text = path.read_text()
    lines = text.splitlines()
    for i, line in enumerate(lines, 1):
        if "fs.access" in line or "access(" in line or "readTextFile" in line or "promises.readFile" in line:
            start=max(1,i-3); end=min(len(lines),i+4)
            print(f"\n--- {path}:{start}-{end} ---")
            for n in range(start,end+1):
                print(f"{n:4d}  {lines[n-1]}")
PY

Repository: netlify/build

Length of output: 6586


🌐 Web query:

Node.js fs.access default flags F_OK read permissions R_OK file permission error documentation

💡 Result:

In Node.js, the fs.access function is used to test a user's permissions for a file or directory [1][2]. The mode argument is an optional integer that specifies the accessibility checks to be performed, with a default value of fs.constants.F_OK [3][4]. Key details regarding fs.access constants and usage: Default Behavior: The default value for the mode argument is fs.constants.F_OK, which indicates that the file is visible to the calling process (i.e., it exists) [3][4]. Permission Constants: The mode argument can also be a bitwise OR of any of the following constants: fs.constants.F_OK: Visible to the calling process [3][4]. fs.constants.R_OK: Readable by the calling process [1][5]. fs.constants.W_OK: Writable by the calling process [1][5]. fs.constants.X_OK: Executable by the calling process [1][5]. Error Handling: If the accessibility check fails, the function returns a rejected promise (when using fsPromises.access) or invokes the callback with an error object (when using fs.access) [1][2]. Important Recommendation: Node.js documentation explicitly advises against using fs.access to check for accessibility before performing operations like fs.open, fs.readFile, or fs.writeFile [1][2]. Doing so creates a race condition, as the file's state could change between the access check and the actual operation [1][6]. Instead, it is best practice to attempt the operation directly and handle any errors that occur [1][4]. Additionally, users should be aware that on Windows, fs.access does not check Access Control Lists (ACLs), so it may return success even if ACLs would otherwise restrict the user from reading or writing to the file [7][8].

Citations:


Replace the fs.access preflight with direct-read error handling.

These checks default to F_OK, so they don’t reliably detect unreadable files and still race with the later fs.readFile/TOML parse calls. Remove the preflight and let the actual read/parse path handle the intended empty-result behavior consistently.

📍 Affects 2 files
  • packages/headers-parser/src/line_parser.ts#L24-L26 (this comment)
  • packages/headers-parser/src/netlify_config_parser.ts#L12-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/headers-parser/src/line_parser.ts` around lines 24 - 26, Replace the
fs.access preflight in the header parsing flow at
packages/headers-parser/src/line_parser.ts:24-26 with direct-read error
handling, allowing the existing read/TOML parse path to determine empty-result
behavior. Apply the same change to the corresponding preflight in
packages/headers-parser/src/netlify_config_parser.ts:12-16; remove both access
checks without changing successful parsing behavior.

return []
}

Expand Down
16 changes: 7 additions & 9 deletions packages/headers-parser/src/merge.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import stringify from 'fast-safe-stringify'

import { splitResults } from './results.js'
import type { Header, MinimalHeader } from './types.js'

Expand Down Expand Up @@ -47,12 +45,12 @@ const removeDuplicates = function (headers: MinimalHeader[] | Header[]) {
return result.reverse()
}

// We generate a unique header key based on JSON stringify. However, because some
// properties can be regexes, we need to replace those by their toString representation
// given the default will be and empty object
// We generate a unique header key based on JSON stringify. The `values` keys
// are sorted so that key order does not affect the result. `forRegExp` is
// derived from `for`, so it does not need to be part of the key.
const generateHeaderKey = function (header: MinimalHeader | Header): string {
return stringify.default.stableStringify(header, (_, value) => {
if (value instanceof RegExp) return value.toString()
return value
})
return JSON.stringify([
header.for,
Object.entries(header.values).sort(([keyA], [keyB]) => (keyA < keyB ? -1 : keyA > keyB ? 1 : 0)),
])
}
5 changes: 3 additions & 2 deletions packages/headers-parser/src/netlify_config_parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { promises as fs } from 'fs'

import { parse as loadToml } from 'smol-toml'
import { pathExists } from 'path-exists'

import { splitResults } from './results.js'
import type { MinimalHeader } from './types.js'
Expand All @@ -10,7 +9,9 @@ import type { MinimalHeader } from './types.js'
// This field is already an array of objects, so it only validates and
// normalizes it.
export const parseConfigHeaders = async function (netlifyConfigPath: string) {
if (!(await pathExists(netlifyConfigPath))) {
try {
await fs.access(netlifyConfigPath)
} catch {
return splitResults([])
}

Expand Down
15 changes: 7 additions & 8 deletions packages/headers-parser/src/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import isPlainObj from 'is-plain-obj'
import mapObj from 'map-obj'
import type { Mapper } from 'map-obj'

import { getForRegExp } from './for_regexp.js'
import { splitResults } from './results.js'
import type { Header, MinimalHeader } from './types.js'

const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value)

export interface MinimalNormalizedHeaders {
headers: MinimalHeader[]
errors: Error[]
Expand Down Expand Up @@ -50,7 +49,7 @@ function parseHeader(
index: number,
minimal: boolean,
): undefined | Error | MinimalHeader | Header {
if (!isPlainObj(header)) {
if (!isObject(header)) {
return new TypeError(`Header must be an object not: ${header}`)
}

Expand Down Expand Up @@ -112,15 +111,15 @@ const normalizePath = function (rawPath?: string): string {

// Normalize and validate the `values` field
const normalizeValues = function (rawValues: Record<string, string | string[]>): Record<string, string> {
if (!isPlainObj(rawValues)) {
if (!isObject(rawValues)) {
throw new TypeError(`"values" must be an object not: ${rawValues}`)
}

return mapObj(rawValues, normalizeValue)
return Object.fromEntries(Object.entries(rawValues).map(([rawKey, rawValue]) => normalizeValue(rawKey, rawValue)))
}

// Normalize and validate each header `values`
const normalizeValue: Mapper<Record<string, string | string[]>, string, string> = function (rawKey, rawValue) {
const normalizeValue = function (rawKey: string, rawValue: string | string[]): [string, string] {
const key: string = rawKey.trim()
if (key === '' || key === 'undefined') {
throw new Error('Empty header name')
Expand Down
Loading