openclaw - 💡(How to fix) Fix [Bug] Plugin loader rewraps require(ws) as namespace object instead of constructor — custom plugins silently break [2 comments, 3 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#74547Fetched 2026-04-30 06:23:09
View on GitHub
Comments
2
Participants
3
Timeline
3
Reactions
2
Author
Timeline (top)
commented ×2closed ×1

Custom plugins that use require("ws") receive a namespace object (typeof "object") instead of the constructor function (typeof "function") when running inside the OpenClaw plugin loader. This causes new require("ws")(url) to throw WebSocket is not a constructor, silently breaking any plugin that uses the standard Node ws import pattern.

Error Message

  • This caused the depeches-flow custom plugin to silently fail all WebSocket RPC calls (wakes and surfaces) for an unknown duration, with zero error logging and zero delivery.

Root Cause

The OpenClaw plugin loader (jiti-based transpile + sandbox) rewraps or transforms require() calls for bundled modules. The transformation for ws preserves the namespace object shape but doesn't expose the default export as a callable constructor — matching import * as ws from "ws" semantics rather than const ws = require("ws") CommonJS semantics.

In CommonJS, require("ws") returns the constructor directly ([email protected] module.exports = WebSocket). In ESM-style rewrap, it returns { default: WebSocket, WebSocket, Server, ... }.

Fix Action

Workaround

Custom plugins can resolve the constructor at runtime:

const wsModule = require("ws");
const WebSocket = wsModule.WebSocket || wsModule.default || wsModule;

Or match the upstream pattern:

import * as ws from "ws";
// use ws.WebSocket as constructor

Code Example

const WebSocket = require("ws");
// ...
const ws = new WebSocket(url);

---

const wsModule = require("ws");
const WebSocket = wsModule.WebSocket || wsModule.default || wsModule;

---

import * as ws from "ws";
// use ws.WebSocket as constructor
RAW_BUFFERClick to expand / collapse

Summary

Custom plugins that use require("ws") receive a namespace object (typeof "object") instead of the constructor function (typeof "function") when running inside the OpenClaw plugin loader. This causes new require("ws")(url) to throw WebSocket is not a constructor, silently breaking any plugin that uses the standard Node ws import pattern.

Symptom

Inside a custom plugin (loaded via extensions/), require("ws") returns a namespace object with .WebSocket, .Server, etc. as properties, rather than the ws constructor function that standalone Node returns for require("ws") with [email protected].

Plugins that do:

const WebSocket = require("ws");
// ...
const ws = new WebSocket(url);

Get: TypeError: WebSocket is not a constructor

Bundled upstream plugins (discord, feishu, browser, nostr) avoid this by using import * as ws from "ws" + ws.WebSocket as the constructor, which works with both shapes. But custom plugins following the standard require("ws") pattern from ws docs will break.

Root cause

The OpenClaw plugin loader (jiti-based transpile + sandbox) rewraps or transforms require() calls for bundled modules. The transformation for ws preserves the namespace object shape but doesn't expose the default export as a callable constructor — matching import * as ws from "ws" semantics rather than const ws = require("ws") CommonJS semantics.

In CommonJS, require("ws") returns the constructor directly ([email protected] module.exports = WebSocket). In ESM-style rewrap, it returns { default: WebSocket, WebSocket, Server, ... }.

Impact

  • Any custom plugin using require("ws") and treating the result as a constructor will fail at runtime.
  • The failure is silent in Promise executors — new WebSocket(url) throws inside new Promise(...), and if the surrounding code only has .catch() on the outer promise chain (not inside the executor), the promise never settles.
  • This caused the depeches-flow custom plugin to silently fail all WebSocket RPC calls (wakes and surfaces) for an unknown duration, with zero error logging and zero delivery.

Workaround

Custom plugins can resolve the constructor at runtime:

const wsModule = require("ws");
const WebSocket = wsModule.WebSocket || wsModule.default || wsModule;

Or match the upstream pattern:

import * as ws from "ws";
// use ws.WebSocket as constructor

Suggested fix

Option A: Plugin loader should preserve CommonJS module.exports semantics for require(), making require("ws") return the constructor function as it would in plain Node.

Option B: Document that require() in the plugin sandbox returns ESM namespace objects, and custom plugins must use the wsModule.WebSocket || wsModule.default || wsModule pattern. Add a helper to plugin-sdk: resolveWsConstructor().

Option A is preferable — it eliminates the surprise factor and keeps custom plugin code matching standalone Node behavior.

Environment

  • OpenClaw 2026.4.25
  • macOS Darwin 24.3.0 arm64
  • Custom plugin in ~/.openclaw/extensions/
  • [email protected] (bundled with openclaw)

Related

  • #74312 (auth-epoch session reset — discovery of this bug was a side-effect of debugging silent plugin failures)

extent analysis

TL;DR

Custom plugins using require("ws") should resolve the WebSocket constructor at runtime to avoid the TypeError: WebSocket is not a constructor error.

Guidance

  • Verify that the issue is caused by the OpenClaw plugin loader's transformation of require() calls by checking the type of the object returned by require("ws").
  • Use the suggested workaround: const wsModule = require("ws"); const WebSocket = wsModule.WebSocket || wsModule.default || wsModule; to resolve the constructor at runtime.
  • Alternatively, match the upstream pattern by using import * as ws from "ws"; and ws.WebSocket as the constructor.
  • Consider updating custom plugins to use the wsModule.WebSocket || wsModule.default || wsModule pattern to ensure compatibility with the OpenClaw plugin loader.

Example

const wsModule = require("ws");
const WebSocket = wsModule.WebSocket || wsModule.default || wsModule;
const ws = new WebSocket(url);

Notes

The OpenClaw plugin loader's transformation of require() calls may cause issues with other modules that expect the CommonJS module.exports semantics. Custom plugins should be aware of this behavior and use the suggested workaround to ensure compatibility.

Recommendation

Apply the workaround by resolving the WebSocket constructor at runtime using the wsModule.WebSocket || wsModule.default || wsModule pattern. This approach ensures compatibility with the OpenClaw plugin loader and avoids the TypeError: WebSocket is not a constructor error.

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

openclaw - 💡(How to fix) Fix [Bug] Plugin loader rewraps require(ws) as namespace object instead of constructor — custom plugins silently break [2 comments, 3 participants]