Skip to content

[Bug] HTTP 500 on all requests after gateway restart - Cannot find module '@slack/web-api' (v2026.3.31) #58922

@qingsongzhou

Description

@qingsongzhou

Summary

After restarting the openclaw gateway (any reason: config change, system reboot, manual restart), all HTTP requests return 500 Internal Server Error. WebSocket connections continue to work normally. The root cause is a module resolution failure for @slack/web-api that is silently swallowed by a bare catch {} block.


Environment

  • openclaw version: 2026.3.31 (213a704)
  • Node.js version: v25.8.1
  • OS: macOS (Darwin 25.4.0)
  • Install method: npm install -g openclaw
  • Install path: /opt/homebrew/lib/node_modules/openclaw
  • Service manager: macOS LaunchAgent (~/Library/LaunchAgents/ai.openclaw.gateway.plist)

Steps to Reproduce

  1. Install openclaw v2026.3.31 globally: npm install -g openclaw
  2. Start the gateway (via LaunchAgent or openclaw gateway start)
  3. Access any HTTP endpoint — all return 500 immediately after a fresh process start

The issue is most noticeable after changing any config that triggers a gateway restart (e.g., modifying gateway.port or gateway.controlUi.allowedOrigins in the Web UI), because it exposes that HTTP was silently broken since the process started.


Expected Behavior

  • HTTP requests to /, /__openclaw/control-ui-config.json, /health, etc. should return their appropriate responses (200, 302, etc.)
  • The Web UI (control-ui) should load normally after a gateway restart

Actual Behavior

  • Every HTTP request returns 500 Internal Server Error
  • The gateway log shows no errors (the exception is silently swallowed)
  • The Web UI never loads; the WebSocket client (webchat) never reconnects
  • WebSocket connections still work normally (they bypass the broken code path)

Root Cause Analysis

The Error

By temporarily patching the catch {} block in gateway-cli-ChUE8Mp7.js to log the error, the actual exception is:

Error: Cannot find module '@slack/web-api'
Require stack:
- /opt/homebrew/lib/node_modules/openclaw/dist/runtime-api-DARHIvKf.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1475:15)
    at jitiResolve (.../jiti.cjs:1:148703)
    at jitiRequire (.../jiti.cjs:1:150290)
    ...

Call Chain

Every HTTP request goes through this chain:

handleRequest(req, res)                              [gateway-cli-ChUE8Mp7.js ~L24107]
  └─ requestStages["slack"].run()                   [always included, regardless of Slack config]
       └─ handleSlackHttpRequest(req, res)           [slack-surface-BW-gJBzz.js]
            └─ loadFacadeModule()
                 └─ loadBundledPluginPublicSurfaceModuleSync(
                        { dirName: 'slack', artifactBasename: 'api.js' }
                    )                                [facade-runtime-D_UMLPAC.js]
                         └─ getJiti(modulePath)(modulePath)
                              └─ extensions/slack/api.js
                                   └─ import "../../runtime-api-DARHIvKf.js"
                                        └─ import { WebClient } from "@slack/web-api"
                                             └─ MODULE NOT FOUND ← throws here

Why Module Resolution Fails

@slack/web-api is installed at:

/opt/homebrew/lib/node_modules/openclaw/dist/extensions/slack/node_modules/@slack/web-api

But runtime-api-DARHIvKf.js is located at:

/opt/homebrew/lib/node_modules/openclaw/dist/runtime-api-DARHIvKf.js

The getJiti() function in facade-runtime-D_UMLPAC.js creates the jiti instance using import.meta.url (i.e., the facade-runtime file's own URL, inside /dist/) as the resolution base:

// facade-runtime-D_UMLPAC.js ~L71
const loader = createJiti(import.meta.url, {   // ← base is /dist/, not /dist/extensions/slack/
    ...buildPluginLoaderJitiOptions(aliasMap),
    tryNative
});

Node.js module resolution starting from /dist/ looks for @slack/web-api in:

  • /dist/node_modules/@slack/web-api ← does not exist
  • /node_modules/openclaw/node_modules/@slack/web-api ← does not exist
  • (etc., never descends into extensions/slack/node_modules/)

It never finds the package installed at extensions/slack/node_modules/@slack/web-api.

Why the Bare catch {} Makes This Catastrophic

The handleSlackHttpRequest stage is registered unconditionally for every HTTP request, even when Slack is not configured. When it throws, the outer catch {} absorbs the error silently and returns 500:

// gateway-cli-ChUE8Mp7.js ~L24270
} catch {                          // ← no error variable bound, no logging
    res.statusCode = 500;
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("Internal Server Error");
}

This means:

  1. The error produces zero log output — impossible to diagnose without patching the binary
  2. Every HTTP request fails, not just Slack-related ones
  3. The Web UI is completely inaccessible

Why It Appeared to Work Before Restart

loadedFacadeModules is a process-level Map cache in facade-runtime-D_UMLPAC.js. On a fresh process start, the cache is empty. The first HTTP request triggers the load, which fails. On failure, the cache entry is deleted, so every subsequent request retries and fails again.

The previous process (v2026.3.28 or an earlier v2026.3.31 instance) either: (a) used a different code path that didn't have this issue, or (b) successfully loaded the module using a different resolution strategy that was changed in v2026.3.31.


Workaround

Create symlinks in /dist/node_modules/ so that Node.js resolution can find the @slack/* packages:

DIST=/opt/homebrew/lib/node_modules/openclaw/dist
mkdir -p "$DIST/node_modules/@slack"

for pkg in web-api bolt logger oauth socket-mode types; do
    ln -sf "$DIST/extensions/slack/node_modules/@slack/$pkg" \
           "$DIST/node_modules/@slack/$pkg"
done

After creating the links, restart the gateway — HTTP requests return 200 immediately.

Note: This workaround is overwritten if openclaw is updated via npm install -g openclaw. Re-run the commands after each update until the bug is fixed.


Suggested Fixes

Option A: Fix jiti resolution base (preferred)

When loading a plugin's facade module, create the jiti instance with the plugin's directory as the resolution base, not the facade-runtime file's location:

// facade-runtime-D_UMLPAC.js
// Change:
const loader = createJiti(import.meta.url, { ... });

// To (use plugin module's directory as resolution base):
const loader = createJiti(
    pathToFileURL(path.dirname(location.modulePath)).href,
    { ... }
);

Option B: Add @slack/* to jiti alias map

In buildPluginLoaderAliasMap, add explicit aliases for @slack/* packages pointing to the extension's node_modules:

aliasMap['@slack/web-api'] = path.resolve(extensionDir, 'node_modules/@slack/web-api');
// (similar for other @slack/* packages)

Option C: Bundle @slack/web-api into the plugin artifact

Bundle @slack/web-api and its dependencies directly into runtime-api-DARHIvKf.js so no external resolution is needed.

Option D: Improve error handling to not break all HTTP requests (recommended regardless)

As a defensive measure, the handleSlackHttpRequest stage should degrade gracefully on module load failures, and the outer catch should log the error:

// Slack stage: degrade gracefully if module unavailable
{
    name: "slack",
    run: async () => {
        try {
            return await handleSlackHttpRequest(req, res);
        } catch (err) {
            if (err.code === 'MODULE_NOT_FOUND') return false;
            throw err;
        }
    }
}

// Outer catch: log instead of silently swallowing
} catch (err) {
    log.error(`[gateway] unhandled error in handleRequest: ${String(err)}`);
    res.statusCode = 500;
    res.end("Internal Server Error");
}

Option D is the most important fix regardless of which root-cause fix is chosen — a single plugin's module resolution failure should never silently take down the entire HTTP server with no log output.


Files Involved

File Role
dist/gateway-cli-ChUE8Mp7.js HTTP request handler; bare catch {} hides the error
dist/facade-runtime-D_UMLPAC.js Plugin facade loader; getJiti() uses wrong resolution base
dist/runtime-api-DARHIvKf.js Slack runtime API; import { WebClient } from "@slack/web-api" fails
dist/slack-surface-BW-gJBzz.js Exports handleSlackHttpRequest via facade loader
dist/extensions/slack/node_modules/@slack/ Correct install location of @slack/* packages

Additional Context

  • The gateway has no Slack accounts configured — yet the Slack module is loaded on every HTTP request
  • The bug is 100% reproducible on a fresh openclaw-gateway process start with v2026.3.31
  • The symptom (HTTP 500 after config-triggered restart) is extremely difficult to diagnose because the bare catch {} produces zero log output

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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