nitrojs / nitrojs/nitro

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

Open
#3,916 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

v3
Dominant language
TypeScript
Stars
11.2k
Forks
899
Avg merge
2d 24m
Merged PRs (30d)
40

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the missing-route case with the default Nitro project and inspect nitro/dist/runtime/internal/app.mjs alongside the renderer-template fallback described in the report. Verify that requesting a nonexistent route from a template no longer recurses indefinitely and instead produces a clear bounded error.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, typescript
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.