#64300·TypeScript

[TS7 LSP] Panic on document URI "file://" (empty path): vfs: path "tsconfig.json" is not absolute

Author: chenxin-yanCreated Sep 16, 2026Updated Sep 17, 2026

Search Terms

tsgo lsp panic, vfs: path "tsconfig.json" is not absolute, file:// empty path, computeConfigFileName, RootLength, DidChangeFiles, DidRequestFile

Version & Regression Information

⏯ Playground Link

No response (LSP server crash, not a type-checking issue)

Code

The server exits when a client sends a document with URI file:// (a file URI with an empty path). Client-agnostic repro, no editor involved — run from any directory containing node_modules/.bin/tsc:

#!/usr/bin/env python3
"""Repro: TS 7 LSP panics on document URI "file://" (empty path).
Usage: python3 tsgo-repro.py node_modules/.bin/tsc
"""
import json, os, subprocess, sys, threading

tsc = sys.argv[1] if len(sys.argv) > 1 else "node_modules/.bin/tsc"
p = subprocess.Popen([tsc, "--lsp", "--stdio"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

def send(m):
    b = json.dumps(m).encode()
    p.stdin.write(b"Content-Length: %d\r\n\r\n" % len(b) + b); p.stdin.flush()

def read():
    hdr = b""
    while not hdr.endswith(b"\r\n\r\n"):
        c = p.stdout.read(1)
        if not c: return None
        hdr += c
    n = int(hdr.split(b":")[1]); return json.loads(p.stdout.read(n))

def pump():  # answer server->client requests so it does not cancel on us
    while (m := read()) is not None:
        if "id" in m and "method" in m:
            send({"jsonrpc": "2.0", "id": m["id"], "result": [None] if m["method"] == "workspace/configuration" else None})
        elif m.get("id") == 2:
            print("hover response:", json.dumps(m)[:200])

threading.Thread(target=pump, daemon=True).start()
send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"processId": None, "rootUri": "file://" + os.getcwd(), "capabilities": {}}})
send({"jsonrpc": "2.0", "method": "initialized", "params": {}})
send({"jsonrpc": "2.0", "method": "textDocument/didOpen", "params": {"textDocument": {"uri": "file://", "languageId": "typescript", "version": 1, "text": "const x = 1;\n"}}})
send({"jsonrpc": "2.0", "id": 2, "method": "textDocument/hover", "params": {"textDocument": {"uri": "file://"}, "position": {"line": 0, "character": 6}}})
try:
    print("server exit code:", p.wait(timeout=5))
except subprocess.TimeoutExpired:
    print("server still alive (no crash)"); p.kill()
$ python3 tsgo-repro.py node_modules/.bin/tsc
panic: vfs: path "tsconfig.json" is not absolute [recovered, repanicked]
...
server exit code: 2

How this happens in practice: Neovim's vim.lsp attaches to any buffer whose filetype matches, including unnamed buffers (nvim_buf_get_name is ""), and vim.uri_from_bufnr serializes those as file://. Some plugins create such buffers, so the whole server dies mid-session and every open TypeScript file loses the language server (method "textDocument/definition" is not supported by any server activated for this buffer).

Actual behavior

Cause chain:

  1. lsproto.DocumentUri.FileName()url.Parse("file://").Path is "", so the file name becomes "".
  2. ProjectCollectionBuilder.DidChangeFiles / DidRequestFiletoPath("") resolves to the current directory, fileName stays "".
  3. configFileRegistryBuilder.computeConfigFileName("")tspath.GetDirectoryPath("") is ""tspath.ForEachAncestorDirectory("", ...)CombinePaths("", "tsconfig.json") = "tsconfig.json"FS().FileExists("tsconfig.json")vfs/internal.RootLength panics on a relative path.
  4. The panic is recovered and re-panicked in Snapshot.Clone, the process exits with code 2, and all documents lose the server.
Full stack trace (7.0.2)
panic: vfs: path "tsconfig.json" is not absolute [recovered, repanicked]

goroutine 6 [running]:
github.com/microsoft/typescript-go/internal/project.(*Snapshot).Clone.func1()
	github.com/microsoft/typescript-go/internal/project/snapshot.go:243 +0xb8
panic({0x1061a7d60?, 0x5b2c9e122100?})
	runtime/panic.go:860 +0x12c
github.com/microsoft/typescript-go/internal/vfs/internal.RootLength({0x5b2c9e1041c0, 0xd})
	github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:23 +0x94
github.com/microsoft/typescript-go/internal/vfs/internal.SplitPath({0x5b2c9e1041c0?, 0x104fe0fe0?})
	github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:32 +0x2c
github.com/microsoft/typescript-go/internal/vfs/internal.(*Common).RootAndPath(0x1064e4c40, {0x5b2c9e1041c0?, 0x5b2c9e134048?})
	github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:39 +0x2c
github.com/microsoft/typescript-go/internal/vfs/internal.(*Common).Stat(0x5b2c9df120e0?, {0x5b2c9e1041c0?, 0x5b2c9e13c601?})
	github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:47 +0x20
github.com/microsoft/typescript-go/internal/vfs/internal.(*Common).FileExists(0x5b2c9e134048?, {0x5b2c9e1041c0?, 0x1061a7d60?})
	github.com/microsoft/typescript-go/internal/vfs/internal/internal.go:59 +0x20
github.com/microsoft/typescript-go/internal/vfs/osvfs.(*osFS).FileExists(0x1064e4c40, {0x5b2c9e1041c0, 0xd})
	github.com/microsoft/typescript-go/internal/vfs/osvfs/os.go:105 +0x74
github.com/microsoft/typescript-go/internal/vfs/cachedvfs.(*FS).FileExists(0x5b2c9e134000, {0x5b2c9e1041c0, 0xd})
	github.com/microsoft/typescript-go/internal/vfs/cachedvfs/cachedvfs.go:71 +0x6c
github.com/microsoft/typescript-go/internal/project.(*snapshotFSBuilder).FileExists(0x5b2c9e120080, {0x5b2c9e1041c0, 0xd}, {0x5b2c9e13e060, 0x56})
	github.com/microsoft/typescript-go/internal/project/snapshotfs.go:313 +0xc4
github.com/microsoft/typescript-go/internal/project.(*sourceFS).FileExists(0x5b2c9e11e5d0, {0x5b2c9e1041c0, 0xd})
	github.com/microsoft/typescript-go/internal/project/snapshotfs.go:738 +0x70
github.com/microsoft/typescript-go/internal/project.(*configFileRegistryBuilder).computeConfigFileName.func2({0x0, 0x0})
	github.com/microsoft/typescript-go/internal/project/configfileregistrybuilder.go:613 +0x7c
github.com/microsoft/typescript-go/internal/tspath.ForEachAncestorDirectory[...]({0x0?, 0x5b2c9e13c898?}, 0x5b2c9e13c8c0?)
	github.com/microsoft/typescript-go/internal/tspath/path.go:1068 +0x4c
github.com/microsoft/typescript-go/internal/project.(*configFileRegistryBuilder).computeConfigFileName(0x5b2c9e1300c0, {0x0, 0x0}, 0x0, 0x5b2c9e10a0f0)
	github.com/microsoft/typescript-go/internal/project/configfileregistrybuilder.go:610 +0x124
github.com/microsoft/typescript-go/internal/project.(*configFileRegistryBuilder).getConfigFileNameForFile(0x5b2c9e1300c0, {0x0, 0x0}, {0x5b2c9e11c1e0, 0x48}, 0x5b2c9e10a0f0)
	github.com/microsoft/typescript-go/internal/project/configfileregistrybuilder.go:643 +0x170
github.com/microsoft/typescript-go/internal/project.(*ProjectCollectionBuilder).findOrCreateDefaultConfiguredProjectForFile(0x5b2c9e12e1e0, {0x0, 0x0}, {0x5b2c9e11c1e0, 0x48}, 0x1, 0x5b2c9e10a0f0)
	github.com/microsoft/typescript-go/internal/project/projectcollectionbuilder.go:999 +0xe4
github.com/microsoft/typescript-go/internal/project.(*ProjectCollectionBuilder).ensureConfiguredProjectAndAncestorsForFile(0x5b2c9e12e1e0, {0x0, 0x0}, {0x5b2c9e11c1e0, 0x48}, 0x5b2c9e10a0f0)
	github.com/microsoft/typescript-go/internal/project/projectcollectionbuilder.go:765 +0x3c
github.com/microsoft/typescript-go/internal/project.(*ProjectCollectionBuilder).DidChangeFiles(0x5b2c9e12e1e0, {{0x5b2c9e104150, 0x7}, {0x0, 0x0}, {0x0}, {0x0}, {0x5b2c9e11e5a0}, {0x0}, 0x0, ...}, ...)
	github.com/microsoft/typescript-go/internal/project/projectcollectionbuilder.go:327 +0x324
github.com/microsoft/typescript-go/internal/project.(*Snapshot).Clone(0x5b2c9e0de000, {0x106451ef8, 0x5b2c9e11e150}, {{{0x5b2c9e122080, 0x1, 0x1}, {0x0, 0x0, 0x0}, {0x0, ...}, ...}, ...}, ...)
	github.com/microsoft/typescript-go/internal/project/snapshot.go:348 +0x98c
github.com/microsoft/typescript-go/internal/project.(*Session).updateSnapshot(0x5b2c9e0e0008, {0x106451ef8, 0x5b2c9e11e150}, _, {{{0x5b2c9e122080, 0x1, 0x1}, {0x0, 0x0, 0x0}, ...}, ...}, ...)
	github.com/microsoft/typescript-go/internal/project/session.go:1205 +0xfc
github.com/microsoft/typescript-go/internal/project.(*Session).UpdateSnapshot(...)
	github.com/microsoft/typescript-go/internal/project/session.go:1191
github.com/microsoft/typescript-go/internal/project.(*Session).DidOpenFile(0x5b2c9e0e0008, {0x106451ef8, 0x5b2c9e11e150}, {0x5b2c9e104150, 0x7}, 0x1, {0x5b2c9e1041a0, 0xd}, {0x5b2c9e104180, 0xa})
	github.com/microsoft/typescript-go/internal/project/session.go:311 +0x3e4
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleDidOpen(0x10644d700?, {0x106451ef8?, 0x5b2c9e11e150?}, 0x104ff95cc?)
	github.com/microsoft/typescript-go/internal/lsp/server.go:1325 +0x3c
github.com/microsoft/typescript-go/internal/lsp.init.func1.registerNotificationHandler[...].6({0x106451ef8, 0x5b2c9e11e150}, 0x14)
	github.com/microsoft/typescript-go/internal/lsp/server.go:827 +0xc4
github.com/microsoft/typescript-go/internal/lsp.(*Server).handleRequestOrNotification(0x5b2c9de0e708, {0x106451ef8?, 0x5b2c9e11e120?}, 0x5b2c9e0b6060)
	github.com/microsoft/typescript-go/internal/lsp/server.go:703 +0xc8
github.com/microsoft/typescript-go/internal/lsp.(*Server).dispatchLoop(0x5b2c9de0e708, {0x106451f30?, 0x5b2c9deba000?})
	github.com/microsoft/typescript-go/internal/lsp/server.go:577 +0x3b8
github.com/microsoft/typescript-go/internal/lsp.(*Server).Run.func1()
	github.com/microsoft/typescript-go/internal/lsp/server.go:426 +0x24
golang.org/x/sync/errgroup.(*Group).Go.func1()
	golang.org/x/[email protected]/errgroup/errgroup.go:93 +0x4c
created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1
	golang.org/x/[email protected]/errgroup/errgroup.go:78 +0x90
server exit code: 2

Expected behavior

The server should reject the document (e.g. respond with InvalidParams for requests targeting it, or ignore the didOpen) and keep running. Other servers handle the same URI gracefully — e.g. tailwindcss-language-server responds InvalidParams: "workspace URI is not a valid file path: file://." and stays alive.

Possible fix: validate the result of DocumentUri.FileName() (empty or non-absolute path) at the request/notification boundary before it reaches ProjectCollectionBuilder, rather than letting it hit the VFS assertion.

Additional information about the issue

Possibly related: #63834 (Fatal error during resolution through RootLength) hits the same VFS assertion via module resolution with no repro; this one has a different entry point (DidChangeFiles / DidRequestFilecomputeConfigFileName) and a deterministic repro.