Skip to content

URI Normalization Inconsistency Causes Workspace Cache Misses on Windows #697

@Hanatarou

Description

@Hanatarou

URI Normalization Inconsistency Causes Workspace Cache Misses on Windows

Summary

pylsp-rope refactoring operations fail on Windows when the LSP client sends URIs with uppercase drive letters (RFC 8089 standard file:///C:/). The root cause is inconsistent URI normalization: uris.from_fs_path() normalizes drive letters to lowercase, but workspace.put_document() stores URIs without normalization, causing dictionary key mismatches.

Environment

  • OS: Windows 10/11
  • pylsp version: 1.14.0
  • pylsp-rope version: 0.1.17
  • LSP Client: Any client following RFC 8089 standard (uppercase drive letters)

Steps to Reproduce

  1. Setup: Use an LSP client that sends RFC 8089 compliant URIs with uppercase drive letters (file:///C:/)
  2. Open: A Python file on Windows
  3. First refactoring: Perform any refactoring operation (e.g., "Extract Variable") - works fine
  4. DO NOT SAVE the file - leave it with unsaved changes from the refactoring
  5. Second refactoring: Attempt another refactoring operation on the same file
  6. Observe: Second refactoring fails because it operates on the old saved content from disk instead of the current unsaved content in workspace

Root Cause Analysis

The Problem

File: pylsp/uris.py (lines 135-137)

# Normalize drive paths to lower case
if RE_DRIVE_LETTER_PATH.match(path):
    path = path[0] + path[1].lower() + path[2:]

This function normalizes Windows drive letters to lowercase (C:c:).

File: pylsp/workspace.py

  • put_document() stores URIs exactly as received from client (NO normalization)
  • get_maybe_document() retrieves URIs exactly as provided (NO normalization)

The Flow That Breaks

  1. Client sends didOpen: file:///C:/test.py (uppercase C, RFC 8089 standard)
  2. pylsp stores: workspace._docs["file:///C:/test.py"] (uppercase, as received)
  3. pylsp-rope needs file content:
    • Calls uris.from_fs_path("C:\\test.py")
    • Returns file:///c:/test.py (lowercase c, normalized)
    • Calls workspace.get_maybe_document("file:///c:/test.py")
    • Returns None (key mismatch: "C:" vs "c:")
  4. Fallback: Reads from filesystem (stale content)
  5. Result: Refactoring operates on outdated code

Evidence from Logs

Server log shows the problem:

pylsp_rope.project - reading from filesystem: "C:\Users\...\file.py"

Instead of the expected:

pylsp_rope.project - reading from workspace: "C:\Users\...\file.py"

Client-server communication:

Client → Server (didOpen): file:///C:/Users/test.py
Server stores in _docs:    file:///C:/Users/test.py  (uppercase)
Rope generates URI:        file:///c:/Users/test.py  (lowercase)
Workspace lookup:          _docs.get("file:///c:/...") → None

Impact

  • Subsequent refactoring operations fail when file has unsaved changes from previous refactoring
  • First refactoring works, but second/third/etc. fail because they operate on stale disk content
  • Affects all Windows users using LSP clients that follow RFC 8089 (uppercase drive letters)
  • Works by accident if client sends lowercase drive letters (non-standard)
  • Silent failure: No error message, refactoring just produces incorrect results based on old content

Affected LSP Clients

  • Any LSP client following RFC 8089 standard (uppercase drive letters)
  • Potentially: Sublime Text, Vim/Neovim, Emacs, custom editors
  • VSCode likely unaffected (may send lowercase URIs)

Cross-Platform Considerations

⚠️ CRITICAL: Any fix must preserve case-sensitivity on Linux/macOS where filesystems are case-sensitive. The normalization should ONLY affect Windows drive letters (C: vs c:), not entire paths.

Safe on Windows: C:\Users\File.py and c:\users\file.py refer to the same file (case-insensitive filesystem)

NOT safe on Linux: /home/User/File.py and /home/user/file.py are DIFFERENT files (case-sensitive filesystem)

Workaround for Users

LSP client developers can work around this by normalizing drive letters to lowercase before sending URIs to pylsp:

# Client-side workaround (Python example)
if sys.platform == 'win32' and uri[8:9].isupper():
    uri = f"{uri[:8]}{uri[8].lower()}{uri[9:]}"

However, this violates RFC 8089 standard and should not be necessary.

Related Issues

This may also affect other pylsp functionality that relies on workspace document cache, not just pylsp-rope.

References


Thank you for maintaining pylsp! This is a subtle but impactful bug that affects Windows users following LSP standards. Happy to provide additional information or testing if needed.

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