Skip to content

LSP: workspace/configuration response returns [] instead of [null], crashing ty server #2094

@adithyabsk

Description

@adithyabsk

Copilot CLI returns an empty array [] in response to workspace/configuration requests, violating the LSP 3.17 spec. This causes language servers like ty to crash.

Steps to reproduce

  1. Copilot CLI starts ty server as an LSP subprocess
  2. During initialize, the client advertises capabilities.workspace.configuration: true
  3. After initialized, ty sends a workspace/configuration request with 1 item
  4. Copilot CLI responds with [] instead of [null]
  5. ty panics with: Mismatch in number of workspace URLs (1) and configuration results (0)

Expected vs actual

Response Spec-compliant
Expected [null] ✅ One entry per item; null for unknown settings
Actual [] ❌ Empty array regardless of items requested

Versions

  • Copilot CLI: 1.0.6
  • ty: 0.0.22, 0.0.23
LSP 3.17 spec reference

https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration

"The order of the returned configuration settings correspond to the order of the passed ConfigurationItems."

"If the client can't provide a configuration setting for a given scope then null needs to be present in the returned array."

The response array must have the same length as the request's items array.

Repro script
#!/usr/bin/env -S uv run --script
"""
Minimal repro: ty 0.0.23 panics when workspace/configuration response
has fewer items than the request's items array.

Usage:
    python3 repro.py          # Demonstrates the crash  (result: [])
    python3 repro.py --fix    # Shows correct behavior  (result: [null])
"""
import json
import subprocess
import sys
import time


def build_msg(msg_dict: dict) -> bytes:
    body = json.dumps(msg_dict).encode()
    return f"Content-Length: {len(body)}\r\n\r\n".encode() + body


def read_lsp_message(stream):
    headers = {}
    while True:
        line = stream.readline()
        if not line:
            return None
        line = line.decode("utf-8", errors="replace").strip()
        if not line:
            break
        if ":" in line:
            key, _, val = line.partition(":")
            headers[key.strip().lower()] = val.strip()
    length = int(headers.get("content-length", 0))
    if length == 0:
        return None
    body = stream.read(length)
    return json.loads(body)


def main():
    use_fix = "--fix" in sys.argv

    import tempfile, os

    workspace = tempfile.mkdtemp(prefix="ty_repro_")
    with open(os.path.join(workspace, "example.py"), "w") as f:
        f.write("x: int = 1\n")

    proc = subprocess.Popen(
        ["ty", "server"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    workspace_uri = f"file://{workspace}"

    init_msg = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "initialize",
        "params": {
            "processId": os.getpid(),
            "rootUri": workspace_uri,
            "capabilities": {
                "workspace": {"workspaceFolders": True, "configuration": True},
            },
            "workspaceFolders": [{"uri": workspace_uri, "name": "repro"}],
        },
    }
    proc.stdin.write(build_msg(init_msg))
    proc.stdin.flush()
    init_resp = read_lsp_message(proc.stdout)
    server_info = init_resp["result"].get("serverInfo", {})
    print(f"1. initialize OK  (server: {server_info.get('name')} {server_info.get('version')})")

    proc.stdin.write(build_msg({"jsonrpc": "2.0", "method": "initialized", "params": {}}))
    proc.stdin.flush()

    time.sleep(0.5)
    config_req = read_lsp_message(proc.stdout)
    items = config_req["params"]["items"]
    print(f"2. ty → client: workspace/configuration  (items: {json.dumps(items)})")

    if use_fix:
        result = [None for _ in items]
        label = "correct per LSP spec"
    else:
        result = []
        label = "what Copilot CLI 1.0.6 sends"

    print(f"3. client → ty:  result: {json.dumps(result)}  ({label})")
    proc.stdin.write(build_msg({"jsonrpc": "2.0", "id": config_req["id"], "result": result}))
    proc.stdin.flush()

    time.sleep(2)
    code = proc.poll()
    if code is not None:
        stderr = proc.stderr.read().decode()
        print(f"4. CRASH — ty exited with code {code}")
        for line in stderr.split("\n"):
            if "panic" in line.lower() or "mismatch" in line.lower():
                print(f"   {line.strip()}")
    else:
        print("4. OK — ty is still running (no crash)")
        proc.terminate()
        proc.wait(timeout=3)

    import shutil
    shutil.rmtree(workspace, ignore_errors=True)


if __name__ == "__main__":
    main()
Notes
  • ty should also handle this gracefully rather than panicking (tracked upstream), but the root cause is the spec-violating response from Copilot CLI.
  • The fix is straightforward: respond with [null] (one null per requested item) when Copilot CLI has no configuration to provide.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:toolsBuilt-in tools: file editing, shell, search, LSP, git, and tool call behavior

    Type

    No type
    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