Skip to content

serverFetch causes infinite loop (Maximum call stack size exceeded) when route doesn't exist #3916

@productdevbook

Description

@productdevbook

Description

When using serverFetch() inside an HTML template (e.g., index.html) to fetch a route that doesn't exist, it causes an infinite recursion loop resulting in RangeError: Maximum call stack size exceeded.

Reproduction

  1. Create a new Nitro project with the default starter template
  2. The index.html contains: {{{ serverFetch("/api") }}}
  3. But there's no /api route defined (only routes/index.ts which maps to /)
  4. Run bun run build && bun run preview
  5. Visit http://localhost:3000/api

What happens

/api request
    ↓
No route found → fallback /** → renderer-template
    ↓
Template calls serverFetch("/api")
    ↓
/api request (again)
    ↓
No route found → fallback /** → renderer-template
    ↓
... infinite loop → Maximum call stack size exceeded

Error output

[request error] [unhandled] [GET] http://localhost/api
 RangeError: Maximum call stack size exceeded
    at hasTrailingSlash (file:///.output/server/chunks/nitro/app.mjs:713:26)
    at withoutTrailingSlash$1 (file:///.output/server/chunks/nitro/app.mjs:718:40)
    at serverFetch (file:///.output/server/chunks/nitro/app.mjs:1574:26)
    at template (file:///.output/server/chunks/nitro/renderer-template.mjs:187:7)
    ...

Root cause

The serverFetch() function in nitro/dist/runtime/internal/app.mjs has no recursion protection:

export function serverFetch(resource, init, context) {
    const req = toRequest(resource, init);
    req.context = {
        ...req.context,
        ...context
    };
    const appHandler = useNitroApp().fetch;
    try {
        return Promise.resolve(appHandler(req));
    } catch (error) {
        return Promise.reject(error);
    }
}

Suggested fix

Add recursion depth tracking to prevent infinite loops:

export function serverFetch(resource, init, context) {
    const req = toRequest(resource, init);
    
    // Recursion guard
    const depth = (context?._serverFetchDepth || 0) + 1;
    if (depth > 10) {
        throw new Error(`serverFetch: maximum recursion depth exceeded for "${resource}". This usually means the requested route doesn't exist and falls back to a template that calls serverFetch again.`);
    }
    
    req.context = {
        ...req.context,
        ...context,
        _serverFetchDepth: depth
    };
    
    const appHandler = useNitroApp().fetch;
    try {
        return Promise.resolve(appHandler(req));
    } catch (error) {
        return Promise.reject(error);
    }
}

Environment

  • Nitro version: v3 (latest)
  • Runtime: Bun
  • OS: macOS

Additional context

This is a defensive programming issue. While the immediate workaround is to ensure all routes called by serverFetch() exist, the framework should protect against infinite recursion with a clear error message.

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