openclaw - ✅(Solved) Fix Windows: ESM import fails in startLazyPluginServiceModule (ERR_UNSUPPORTED_ESM_URL_SCHEME, protocol c:) [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72573Fetched 2026-04-28 06:34:20
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×3closed ×1commented ×1mentioned ×1

On Windows, dynamic import() of a bare absolute filesystem path (e.g. C:\path\to\module.mjs) throws:

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: ... Received protocol 'c:'

This affects the browser plugin service when OPENCLAW_BROWSER_CONTROL_MODULE is set to a Windows drive path, and may surface as:

plugin service failed (browser-control, ...): Only URLs with a scheme in: file, data, and node are supported...

Error Message

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: ... Received protocol 'c:' 3. Observe the ERR_UNSUPPORTED_ESM_URL_SCHEME / Received protocol 'c:' error.

Root Cause

Root cause (code)

Fix Action

Fixed

PR fix notes

PR #72582: fix(plugins): normalize lazy service override imports

Description (problem / solution / changelog)

Summary

  • Share the Windows-safe dynamic import specifier normalization between the plugin loader and lazy service override path.
  • Normalize the default lazy service import() target before importing, so drive-letter overrides become file:// URLs on Windows.
  • Add coverage for the lazy service override path and keep the existing loader Windows conversion coverage intact.

Fixes #72573.

Tests

  • pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/plugins/import-specifier.ts src/plugins/lazy-service-module.ts src/plugins/lazy-service-module.test.ts src/plugins/loader.ts
  • pnpm test src/plugins/lazy-service-module.test.ts
  • pnpm test src/plugins/loader.test.ts -- -t "converts Windows absolute import specifiers"
  • pnpm build
  • pnpm check:changed

Changed files

  • CHANGELOG.md (modified, +2/-0)
  • src/cli/command-registration-policy.ts (modified, +7/-2)
  • src/cli/program/command-descriptor-types.ts (added, +5/-0)
  • src/cli/program/command-descriptor-utils.ts (modified, +1/-1)
  • src/cli/program/command-group-descriptors.ts (modified, +2/-5)
  • src/cli/program/core-command-descriptors.ts (modified, +1/-1)
  • src/cli/program/root-help.test.ts (modified, +14/-0)
  • src/cli/program/subcli-descriptors.ts (modified, +1/-1)
  • src/plugins/import-specifier.ts (added, +27/-0)
  • src/plugins/lazy-service-module.test.ts (modified, +16/-0)
  • src/plugins/lazy-service-module.ts (modified, +3/-1)
  • src/plugins/loader.ts (modified, +1/-26)

PR #72599: fix(plugins): normalize Windows absolute paths in lazy plugin service loader

Description (problem / solution / changelog)

PR: fix(plugins): normalize Windows absolute paths in lazy plugin service loader

Fixes: openclaw/openclaw#72573 Branch: fix/win-esm-import-72573 Patch file: 72573-win-esm-import.patch (in this same folder) Commit: 5 files changed, 187 insertions(+), 29 deletions(-)

Suggested PR title

fix(plugins): normalize Windows absolute paths in lazy plugin service loader

Suggested PR body

Summary

On Windows, startLazyPluginServiceModule could fail when the override env var (e.g. OPENCLAW_BROWSER_CONTROL_MODULE) was set to an absolute drive-letter path. The default loader called await import(specifier) directly, and Node's ESM loader rejects bare paths like C:\path\to\module.mjs with:

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: ... Received protocol 'c:'

src/plugins/loader.ts already had a toSafeImportPath helper that converts such paths to file:///C:/... URLs, but it was a private function inside loader.ts, so the lazy plugin service loader couldn't reuse it.

Changes

  • New module src/plugins/safe-import-path.ts — extracted toSafeImportPath so it can be reused. Behavior is unchanged from the original loader.ts implementation.
  • src/plugins/loader.ts — now imports toSafeImportPath from the new module. The existing __testing.toSafeImportPath re-export is preserved, so the existing "converts Windows absolute import specifiers to file URLs only for module loading" test in loader.test.ts keeps passing without modification.
  • src/plugins/lazy-service-module.ts — factored the default loader into a named defaultLoadOverrideModule(specifier, importModule?). It routes the specifier through toSafeImportPath before handing it to import(). The optional importModule parameter exists purely so tests can assert on the normalized specifier without needing a real Windows host.
  • src/plugins/safe-import-path.test.ts (new) — 8 cases covering both branches: drive-letter paths, UNC paths, percent-encoding (spaces, unicode), pre-formed file:// URLs, relative specifiers, bare module specifiers; plus posix and darwin no-op behavior.
  • src/plugins/lazy-service-module.test.ts — 3 new cases for defaultLoadOverrideModule: linux passthrough, win32 normalization (the regression test for this issue), and idempotence on already-formed file:// URLs.

Why this fix shape

I chose to extract toSafeImportPath into a standalone module rather than re-export it through loader.ts because:

  1. loader.ts is large (~3000 lines) and pulls in a lot of transitive imports — any module that just needs the path helper shouldn't have to load all of that.
  2. There may be other call sites in the future that need the same normalization (the issue itself mentions validateBrowserControlOverrideSpecifier as a candidate for tightening; that would also benefit from being able to reuse this helper). A standalone module makes that easy and avoids cycles.

I deliberately scoped the PR to just the lazy-service-module.ts call site (the actual root cause of #72573) and left the optional follow-ups for a separate PR:

  • The validateBrowserControlOverrideSpecifier validator in extensions/browser/src/plugin-service.ts could be tightened to fail fast on raw Windows drive paths (or normalize them at validation time). Not strictly necessary for the bug since we now normalize at the import site, but worth doing as defense-in-depth.

Test plan

Run the focused plugins suite:

pnpm exec vitest run --config test/vitest/vitest.plugins.config.ts \
  src/plugins/safe-import-path.test.ts \
  src/plugins/lazy-service-module.test.ts

Local result: 16/16 passing (8 + 8). The existing 5 cases in lazy-service-module.test.ts continue to pass alongside the 3 new ones.

The existing __testing.toSafeImportPath test in loader.test.ts was not modified and continues to exercise the same helper now sourced from the new module.

Backwards compatibility

  • No public API changes.
  • defaultLoadOverrideModule is a new export; nothing else in the module's surface changed.
  • The __testing.toSafeImportPath re-export in loader.ts is preserved.
  • No behavior change on non-Windows platforms; on Windows, previously-broken drive-letter override paths now work as documented.

Closes

Closes #72573


How to push this and open the PR

The patch is ready as 72573-win-esm-import.patch in this folder. To push it from your machine:

# 1. Fork openclaw/openclaw on GitHub (web UI) if you haven't already.

# 2. Clone your fork (or add it as a remote to an existing clone):
git clone [email protected]:<your-username>/openclaw.git
cd openclaw

# 3. Make sure you're on a fresh main:
git fetch origin && git checkout main && git reset --hard origin/main

# 4. Apply the patch and create the branch:
git checkout -b fix/win-esm-import-72573
git am /path/to/72573-win-esm-import.patch
# (or `git apply ...` then `git commit -am '...'` if you'd prefer to author the commit yourself)

# 5. Push and open the PR:
git push -u origin fix/win-esm-import-72573
# Then visit the URL GitHub prints, or:
gh pr create --repo openclaw/openclaw \
  --title "fix(plugins): normalize Windows absolute paths in lazy plugin service loader" \
  --body-file PR-72573.md

If you'd rather I push it directly, install a GitHub MCP connector and I can take it from there.

Changed files

  • src/cli/argv.ts (modified, +94/-11)
  • src/cli/command-registration-policy.ts (modified, +4/-3)
  • src/plugins/lazy-service-module.test.ts (modified, +54/-1)
  • src/plugins/lazy-service-module.ts (modified, +18/-2)
  • src/plugins/loader.ts (modified, +1/-26)
  • src/plugins/safe-import-path.test.ts (added, +87/-0)
  • src/plugins/safe-import-path.ts (added, +27/-0)

Code Example

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: ... Received protocol 'c:'

---

const loadOverrideModule =
  params.loadOverrideModule ?? (async (specifier: string) => await import(specifier));
RAW_BUFFERClick to expand / collapse

Summary

On Windows, dynamic import() of a bare absolute filesystem path (e.g. C:\path\to\module.mjs) throws:

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: ... Received protocol 'c:'

This affects the browser plugin service when OPENCLAW_BROWSER_CONTROL_MODULE is set to a Windows drive path, and may surface as:

plugin service failed (browser-control, ...): Only URLs with a scheme in: file, data, and node are supported...

Environment

  • OS: Windows
  • OpenClaw: 2026.4.24 (current npm global install)
  • Node: 22.x (e.g. via nvm-windows)

Root cause (code)

startLazyPluginServiceModule uses the default override loader:

const loadOverrideModule =
  params.loadOverrideModule ?? (async (specifier: string) => await import(specifier));

On Windows, Node's ESM loader does not accept raw C:\... as an import specifier; it must be a file:// URL (see toSafeImportPath and comments in src/plugins/loader.ts for the same pattern).

Bundled dist matches this: dist/lazy-service-module-*.js contains await import(specifier) with no Windows normalization.

extensions/browser/src/plugin-service.ts documents OPENCLAW_BROWSER_CONTROL_MODULE as an override but validation only blocks data|http|https|node schemes — Windows absolute paths are allowed through and then fail at import time.

Reproduction

  1. On Windows, set a valid override path as a bare drive path (User env or session):
    • OPENCLAW_BROWSER_CONTROL_MODULE=C:\path\to\custom-browser-service.mjs
  2. Start the gateway / OpenClaw so the browser-control plugin service runs.
  3. Observe the ERR_UNSUPPORTED_ESM_URL_SCHEME / Received protocol 'c:' error.

Suggested fix

  • In startLazyPluginServiceModule (or a shared helper), if process.platform === 'win32' and the override specifier is an absolute path, convert with pathToFileURL / align with toSafeImportPath in src/plugins/loader.ts before import().
  • Optionally document that override values should be file:///C:/... until fixed.
  • Add a small unit test for Windows-style absolute specifiers (can mock import or test URL conversion in isolation).

Related files

  • src/plugins/lazy-service-module.ts
  • extensions/browser/src/plugin-service.ts (OPENCLAW_BROWSER_CONTROL_MODULE)
  • src/plugins/loader.ts (toSafeImportPath)

extent analysis

TL;DR

Convert Windows absolute paths to file:/// URLs before importing to fix the ERR_UNSUPPORTED_ESM_URL_SCHEME error.

Guidance

  • Check if the process.platform is 'win32' and the override specifier is an absolute path, then convert it using pathToFileURL or a similar approach.
  • Align the conversion with the toSafeImportPath function in src/plugins/loader.ts for consistency.
  • Consider adding a unit test for Windows-style absolute specifiers to ensure the fix works as expected.
  • Until the fix is implemented, document that override values should be in the format file:///C:/... to avoid the error.

Example

import { pathToFileURL } from 'url';
import path from 'path';

// ...

if (process.platform === 'win32' && path.isAbsolute(specifier)) {
  const fileUrl = pathToFileURL(specifier);
  await import(fileUrl.href);
} else {
  await import(specifier);
}

Notes

This fix assumes that the issue is specific to Windows and the use of absolute paths as import specifiers. The provided example code snippet is a minimal illustration of the conversion process and may need to be adapted to the actual implementation.

Recommendation

Apply the workaround by converting Windows absolute paths to file:/// URLs before importing, as this directly addresses the root cause of the error and provides a clear solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING