Skip to content

[BUG] rich.logging.RichHandler: File links on Windows (VS Code) use incorrect format (file:// instead of file:/// and backslashes) #4093

@5j9

Description

@5j9
  • I've checked docs and closed issues for possible solutions. (can't check right now, no internet access in Iran, just a few websites like github are accessible)
  • I can't find my issue in the FAQ.

Describe the bug

When clicking file links generated by RichHandler in VS Code terminal on Windows, the path does not open. The issue is caused by two problems:

  1. Links use file:// (two slashes) instead of the required file:/// (three slashes) for absolute Windows paths.
  2. Paths contain backslashes \ instead of forward slashes / in the URI.

Sample code:

import logging

from rich.logging import RichHandler

logging.basicConfig(
    level='NOTSET',
    format='%(message)s',
    datefmt='[%X]',
    handlers=[
        RichHandler(
            show_path=True,
            enable_link_path=True,
            rich_tracebacks=True,
        )
    ],
)

logger = logging.getLogger(__name__)
logger.info('some message')

Run the script in vscode terminal on Windows. Click on the filename in the generated output, you'll get:

[Window Title]
Visual Studio Code

[Main Instruction]
Path does not exist

[Content]
The path '\' does not exist on this computer.

[OK]

Platform

Click to expand

Windows
vscode's terminal, using cmd.exe


<!-- Failed to upload "image.png" -->

>python -m rich.diagnose
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=222 ColorSystem.TRUECOLOR>                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = 'truecolor'                                                   │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 14                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=222, height=14),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=222,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=14,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=222, height=14)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 222                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭── <class 'rich._windows.WindowsConsoleFeatures'> ───╮
│ Windows features available.                         │
│                                                     │
│ ╭─────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=True, truecolor=True) │ │
│ ╰─────────────────────────────────────────────────╯ │
│                                                     │
│ truecolor = True                                    │
│        vt = True                                    │
╰─────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'CLICOLOR': None,              │
│     'COLORTERM': 'truecolor',      │
│     'COLUMNS': None,               │
│     'JPY_PARENT_PID': None,        │
│     'JUPYTER_COLUMNS': None,       │
│     'JUPYTER_LINES': None,         │
│     'LINES': None,                 │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': 'vscode',      │
│     'TERM': None,                  │
│     'TTY_COMPATIBLE': None,        │
│     'TTY_INTERACTIVE': None,       │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Windows"

>uv pip show rich         
Name: rich
Version: 15.0.0
Location: D:\...\.venv\Lib\site-packages
Requires: markdown-it-py, pygments
Required-by:

If you're using Rich in a Jupyter Notebook, run the following snippet in a cell
and paste the output in your bug report.

from rich.diagnose import report
report()

I've fixed this issue by modifying rich._log_render.LogRender.__call__.

rich/rich/_log_render.py

Lines 32 to 70 in 46cebbb

def __call__(
self,
console: "Console",
renderables: Iterable["ConsoleRenderable"],
log_time: Optional[datetime] = None,
time_format: Optional[Union[str, FormatTimeCallable]] = None,
level: TextType = "",
path: Optional[str] = None,
line_no: Optional[int] = None,
link_path: Optional[str] = None,
) -> "Table":
from .containers import Renderables
from .table import Table
output = Table.grid(padding=(0, 1))
output.expand = True
if self.show_time:
output.add_column(style="log.time")
if self.show_level:
output.add_column(style="log.level", width=self.level_width)
output.add_column(ratio=1, style="log.message", overflow="fold")
if self.show_path and path:
output.add_column(style="log.path")
row: List["RenderableType"] = []
if self.show_time:
log_time = log_time or console.get_datetime()
time_format = time_format or self.time_format
if callable(time_format):
log_time_display = time_format(log_time)
else:
log_time_display = Text(log_time.strftime(time_format))
if log_time_display == self._last_time and self.omit_repeated_times:
row.append(Text(" " * len(log_time_display)))
else:
row.append(log_time_display)
self._last_time = log_time_display
if self.show_level:
row.append(level)

The changes I made were two:

  1. replace both file://s with file:/// in style f-strings.
  2. add link_path = link_path.replace('\\', '/') at the beginning of the function definition.

Why this works:

  1. According to the URI specification (RFC 8089 I believe) , the file URI scheme has the format:

file://<host>/<path>

<host> represents the machine where the file resides.

For local files on the same machine, the <host> must be empty (or localhost).

When the host is empty, the URI becomes file:/// (three slashes), followed by the absolute path.

Most Windows applications (including VS Code, File Explorer, and Windows Terminal) follow the RFC and require the triple-slash format for local file links. The double-slash format is either rejected or interpreted incorrectly (e.g., trying to open a network share named C:).

  1. The URI standard requires that path separators be forward slashes (/), even on Windows. Backslashes are not allowed in URIs because they have special meaning as escape characters.

So the log links generated by the following sample will work as expected:

import logging

from rich._log_render import LogRender
from rich.logging import RichHandler

# Store original method
_original_log_render_call = LogRender.__call__


def _patched_log_render_call(
    self,
    console,
    renderables,
    log_time=None,
    time_format=None,
    level='',
    path=None,
    line_no=None,
    link_path=None,
):
    """Patched version that fixes Windows file links."""

    # Fix Windows paths for the link
    if link_path:
        # Convert backslashes to forward slashes
        link_path = link_path.replace('\\', '/')
        # Add the third slash (will be used in the f-string below)
        # The original code does: f"link file://{link_path}"
        # We need: f"link file:///{link_path}"
        # So we prepend a slash to link_path
        link_path = '/' + link_path

    # Call original with modified link_path
    return _original_log_render_call(
        self,
        console,
        renderables,
        log_time,
        time_format,
        level,
        path,
        line_no,
        link_path,
    )


# Apply the patch BEFORE creating handlers
LogRender.__call__ = _patched_log_render_call

# Now configure logging normally
logging.basicConfig(
    level='NOTSET',
    format='%(message)s',
    datefmt='[%X]',
    handlers=[
        RichHandler(
            show_path=True,
            enable_link_path=True,
            rich_tracebacks=True,
        )
    ],
)

logger = logging.getLogger(__name__)
logger.info('Test message with clickable link')

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions