Skip to content

[BUG] load-manifest glob reads App Router *-manifest.json route directories and throws EISDIR #1360

Description

@BakedBotAi

Environment

  • @opennextjs/cloudflare: 1.20.2
  • @opennextjs/aws: 4.1.0 (content-updater / aggregate-on-load)
  • Next.js: 16.2.11
  • Node: 22
  • glob: 13.0.6
  • esbuild: 0.25.0

Description

getLoadManifestRule() in packages/cloudflare/src/cli/build/patches/plugins/load-manifest.ts globs for manifest files:

const manifests = await glob(
  join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"),
  { windowsPathsNoEscape: true }
);

Without nodir: true, this matches App Router route directories whose path segments end in *-manifest.json. For example, a Next.js API route at src/app/mail-manifest.json/route.ts creates:

.next/server/app/mail-manifest.json/
├── route.js
└── route.js.nft.json

This directory matches *-manifest...json and is included in the manifests array. The subsequent await readFile(manifest, "utf-8") throws:

EISDIR: illegal operation on a directory, read

The same issue affects the getEvalManifestRule() glob call (which uses the same { windowsPathsNoEscape: true } options without nodir).

Stack trace

[ERROR] EISDIR: illegal operation on a directory, read [plugin aggregate-on-load]

    at async readFileHandle (node:internal/fs/promises:555:24)
    at async .../plugins/load-manifest.js:39:12
    at async getLoadManifestRule (.../plugins/load-manifest.js:37:30)
    at async callback (.../plugins/load-manifest.js:23:54)
    at async .../content-updater.js:57:30
    at setup (.../content-updater.js:40:23)

Reproduction

  1. Create an App Router route whose path ends in *-manifest.json:
    // src/app/mail-manifest.json/route.ts
    import { NextResponse } from 'next/server';
    export function GET() {
      return NextResponse.json({ name: 'My App' });
    }
    
  2. Run opennextjs-cloudflare build
  3. Build fails with EISDIR

Any route whose directory name matches {*-manifest,required-server-files,prefetch-hints}.json will trigger this. The application route is legitimate — the bug is in the glob, not the route.

Proposed fix

Add nodir: true to both glob calls in load-manifest.ts:

const manifests = await glob(
  join(dotNextDir, "**/{*-manifest,required-server-files,prefetch-hints}.json"),
- { windowsPathsNoEscape: true }
+ { windowsPathsNoEscape: true, nodir: true }
);

This excludes directories from glob results while retaining all genuine JSON manifest files. It is the smallest possible fix — no error handling, no path filtering, no route renaming.

Regression test

A test could create a .next-like tree with both:

  • server/app/some-manifest.json/route.js (directory — should be excluded)
  • app-paths-manifest.json (file — should be retained)

And assert the glob with nodir: true excludes the directory and includes the file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions