Skip to content

withEve() drops Next's segment-prefetch routes from Vercel Build Output, silently disabling PPR soft navigation #768

Description

@patrick-xin

Summary

withEve() writes the root .vercel/output/config.json from ensureEveVercelOutputConfig(), which runs at next.config load time — before Next's builder emits its own routing table into that same file. On Vercel, Next's header-conditional routes (the ones keyed on Next-Router-Segment-Prefetch) are not present in the deployed routing table.

The effect on a Next app using cacheComponents + withEve(): every segment-prefetch request is routed to /404, the router cannot assemble a soft navigation, and every client-side navigation falls back to a full document reload. PPR / App Shells are effectively inert in production.

Nothing errors. The document still serves correctly, so links appear to work — they just cost a full page load each. The only visible trace is 404s in the network tab.

  • eve 0.22.6
  • next 16.3.0-preview.5, with cacheComponents: true, partialPrefetching: true

Reproduction

The write is gated on process.env.VERCEL, so it reproduces locally:

VERCEL=1 next build
cat .vercel/output/config.json
{
  "routes": [
    { "src": "^/eve/v1/(.*)$", "destination": { "service": "eve", "type": "service" } }
  ],
  "version": 3,
  "services": {
    "eve": {
      "buildCommand": "node 'node_modules/eve/bin/eve.js' build",
      "framework": "eve",
      "routes": [
        { "src": "^/eve/v1/(.*)$", "transforms": [{ "args": "/eve/v1/$1", "op": "set", "type": "request.path" }] }
      ],
      "root": "."
    }
  }
}

Because ensureEveVercelOutputConfig() runs while the config is still loading, the file it reads does not exist yet, so it writes a complete v3 config whose routes array contains only eve's own route.

On the deployed app, with a dynamic route (/chat/[id]):

# plain RSC request — served correctly
curl -sI -o /dev/null -w '%{http_code} %header{x-matched-path}\n' \
  -H 'RSC: 1' https://<app>/chat/<id>
# 200 /chat/[id].rsc

# identical request plus the segment-prefetch header
curl -sI -o /dev/null -w '%{http_code} %header{x-matched-path}\n' \
  -H 'RSC: 1' -H 'Next-Router-Segment-Prefetch: /_tree' https://<app>/chat/<id>
# 404 /404

That single header is the difference between 200 and 404.

Evidence it comes from withEve()

Removing the wrapper fixes it. Changing export default withEve(nextConfig) to export default nextConfig, changing nothing else, and deploying: segment prefetch returns 200.

A control app without eve works. next-beats, the cacheComponents reference app, runs the same flags (cacheComponents, partialPrefetching, reactCompiler, typedRoutes) and sets export const prefetch = 'allow-runtime' on every page. Its segment-prefetch requests return 200 on Vercel. Its next.config.ts is a bare export default nextConfig; ours differed only by withEve().

The Next build itself is fine. next build emits the segment artifacts either way (.next/server/app/chat/[id].segments/{_tree,_shell,_full}.segment.rsc), and the app soft-navigates correctly against next start. Only the deployed Build Output routing is affected.

What we tried

Declaring the eve service in vercel.json makes ensureEveVercelOutputConfig() early-return, and VERCEL=1 next build then emits no .vercel/output/config.json at all — confirmed locally. But deployed, the entire app 404s, not just the eve routes.

That suggests the generated config is load-bearing for two things at once: registering the eve service, and keeping the Next app as the deployment's default app. Suppressing the write loses both. If that reading is right, a fix likely needs to keep emitting the default-app registration while no longer racing the framework builder for the routes array — for example by merging into the config after the builder has written it, rather than writing first at config-load time.

So as far as we can tell there is no userland workaround short of dropping withEve(), which means dropping the mounted agent.

Happy to test a patch against this reproduction if that would help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions