Skip to content

res.render()/app.render() throws opaque TypeError for a view name ending in "." #7350

Description

@webdevelopersrinu

A view name ending in "." causes Express to call require("") in the View constructor, throwing an opaque error instead of resolving the view or reporting a clean lookup error.

Reproduction:

const express = require('express')
const app = express()
app.set('view engine', 'ejs')

// callback is never invoked — throws synchronously:
app.render('index.', (err, html) => {})
// TypeError [ERR_INVALID_ARG_VALUE]: The argument 'id' must be a non-empty string. Received ''

Via res.render('index.') inside a route the same error surfaces as an opaque 500 in the error handler, instead of the usual Failed to lookup view "index." error.

Root cause: path.extname('index.') returns '.', which is truthy, so the "no extension -> use default engine" fallback in lib/view.js is skipped. this.ext stays '.', and this.ext.slice(1) becomes '', so require('') is called.

Expected: a view name ending in "." should be handled like any unresolved view — the callback should receive a normal "Failed to lookup view" error, not an opaque require("") TypeError, and app.render() must never throw past its callback.

I have a fix + regression test ready and will open a PR.

Activity

  1. added a commit that references this issue on Jul 16, 2026
    e24299b
  2. Errr0rr404 commented on Jul 23, 2026

    @Errr0rr404

    Opened a fix in #7393 — skips require('') when path.extname is only '.' so lookup fails with the usual helpful error.

  3. haoku123 commented on Aug 14, 2026

    @haoku123

    Opened #7420 to fix this — view names ending in '.' now report a clean lookup error via the callback.

  4. krzysdz commented on Aug 21, 2026

    @krzysdz
    Contributor

    Multiple LLM users and bots have opened PRs "solving" this problem and this made me think what should be the correct behaviour.

    • . in most conventions1 is a valid suffix/extension name, so IMO Express should not append the default extension.
    • "Failed to lookup view %s in views directory" error suggests that there is no such file, so it won't be a good choice if the file (ending with a dot) really exists.

    While I agree that the error thrown by Node.js is not very helpful, because it's Express that passes the empty string as an argument to require(), debugging it should not be hard if one looks at the stack trace. It may be a good idea to make sure that Express does not pass invalid values to Node.js APIs - something like throwing a new error just before the require(mod). On the other hand, you can hit this only with a typo that can be easliy found if you look at the stack trace (the last line not coming fron Express) - there is approximately 0% chance that someone would write "name." intentionally and expect it to work (garbage in, garbage out).

    This being said, I don't intend to review any PRs (@haoku123 asked for this in #7351 (comment)), because this is just my opinion and I don't like reading LLMs, especially since all relevant PRs in a typical LLM fashion try to "solve the problem" without thinking about its nature.

    Footnotes

    1. Node.js, Python, C++, .NET, Go, but not Rust ↩

  5. webdevelopersrinu commented on Aug 22, 2026

    @webdevelopersrinu
    Author

    I see the point about . being a valid suffix. But right now Express calls require("") by accident, and that is the part I fixed in #7351. Would you rather it throw its own error before require(mod), or is the lookup error fine for now?

  6. haoku123 commented on Aug 22, 2026

    @haoku123

    Thanks for asking — my preference is the lookup error, not a new dedicated error thrown before require(mod). My #7420 used the same approach (now closed; happy for #7351 to carry it forward):

    1. Consistency. Failed to lookup view "..." is the standard error every unresolvable view already produces. Apps already handle it; introducing a brand-new error type for one degenerate input means maintainers have to pick a code/name, document it, and treat it as public API — a lot of surface area for a corner case.

    2. Correct under both readings of .. Even if . counts as a valid suffix (so index. is "just a filename", per the point above), when that file doesn't exist the correct outcome is still a lookup error, not a crash. Treating a bare . as no-extension simply routes through the default-engine path and yields exactly that.

    3. Minimal diff. The real bug is require(""); normalizing . → no-extension is the smallest change that removes the crash while keeping every existing error path intact.

    So yes — the lookup error is fine, and #7351's approach matches what I'd propose. If maintainers ever want to support real files literally named x., that's a separate enhancement and shouldn't ride on this fix.

  7. krzysdz commented on Aug 24, 2026

    @krzysdz
    Contributor
    1. Consistency. Failed to lookup view "..." is the standard error every unresolvable view already produces. Apps already handle it; introducing a brand-new error type for one degenerate input means maintainers have to pick a code/name, document it, and treat it as public API — a lot of surface area for a corner case.

    Not every error is a runtime error - a nonexistent template file is something that the programmer is responsible for. Handling the lookup error in a specific way would mean that someone expects it, so they probably pass user input as the view name, which is a vulnerability (probably RCE, I don't have time now to write a PoC).

    The errors returned by Express are not documented yet (see #6951). Adding a new error for a case that currently results in a different error (thrown by Node.js) and that should not happen without a developer's mistake probably wouldn't inconvenience anybody.

    1. Correct under both readings of .. Even if . counts as a valid suffix (so index. is "just a filename", per the point above), when that file doesn't exist the correct outcome is still a lookup error, not a crash. Treating a bare . as no-extension simply routes through the default-engine path and yields exactly that.

    Maybe it's correct to LLMs, but certainly would confuse brain-powered humans. Under what reading "Failed to lookup view 'name.' in views directory 'dirname'" when there is a file dirname/name. makes sense?

    1. Minimal diff. The real bug is require(""); normalizing . → no-extension is the smallest change that removes the crash while keeping every existing error path intact.

    Adding a new error is more or less the same numer of lines added (3).

    There's one more potential problem with this specific approach - it replaces the extension instead of adding it. If we follow the Node.js convention, the . is an extension, so adding an extension should change name. to name..ext. That's why I want to know what people would expect the behaviour to be.

  8. usualdesigner commented on Sep 12, 2026

    @usualdesigner

    Nice root-cause dig. As a stopgap until the fix ships: if the view name is ever coming from user input (slugs, params) rather than a hardcoded string, just reject/strip a trailing dot before it reaches render() - cheap guard, and it means you get your normal error handling path instead of an uncaught TypeError taking down the request.

  9. krzysdz commented on Sep 12, 2026

    @krzysdz
    Contributor

    if the view name is ever coming from user input (slugs, params)

    I know that I'm replying to a bot, but if anyone is reading this DO NOT EVER PASS USER INPUT AS THE VIEW NAME!1

    Footnotes

    1. Unless you're writing a CTF challenge. ↩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions