← Back to PRs

#22184: fix(msteams): resolve EADDRINUSE double-start by awaiting port binding

by harshang03 open 2026-02-20 21:03 View on GitHub →
channel: msteams size: S
## Summary Fixes #22169 — msteams provider starts twice on gateway boot, causing EADDRINUSE restart loop. **Root cause** `monitorMSTeamsProvider()` called `expressApp.listen(port)` and then immediately returned `{ app, shutdown }`, before the HTTP server had actually bound to the port. The channel manager in `server-channels.ts` wraps `startAccount` in a tracked promise; when that promise resolves, it marks the provider as `running: false` and triggers the auto-restart logic. With the old code resolving before the port was even bound, the first instance would succeed on port 3978 while a second instance was simultaneously started — which then failed with `EADDRINUSE`. The pattern cascaded through all 10 restart attempts and was then restarted by the health monitor, repeating indefinitely. **Changes** - **`extensions/msteams/src/monitor.ts`** — Core fix. `monitorMSTeamsProvider()` now: 1. Awaits the `'listening'` event on the `http.Server` before proceeding, so a bind failure surfaces as a rejected promise (rather than a silent early return that triggers a restart). 2. Stays alive by awaiting the abort signal, matching the lifecycle contract expected by `server-channels.ts`. The function only resolves after `shutdown()` completes, exactly like the Discord and Slack providers. - The `MonitorMSTeamsResult` type and its early-return pattern are removed; the return type is now `Promise<void>`. - An `onListening` hook (typed as `MSTeamsOnListeningCallback`) is added to `MonitorMSTeamsOpts` so callers can react to the moment the port is confirmed bound. - **`extensions/msteams/src/channel.ts`** — `startAccount` now passes an `onListening` callback that calls `ctx.setStatus({ accountId, port })` only after the server is confirmed listening. Previously the port status was reported before the server was even started, creating a window where the channel appeared live but was not yet accepting connections. - **`extensions/msteams/src/probe.ts`** — Adds `probeWebhookPort(port)`, a lightweight TCP connect that checks whether the webhook server is actually reachable on the local loopback. `probeMSTeams()` now runs this probe in parallel with the Bot Framework credential check and includes the result in `ProbeMSTeamsResult`, giving the health monitor a real liveness signal beyond credential validation. - **`extensions/msteams/src/monitor-types.ts`** — Exports `MSTeamsOnListeningCallback` type. - **`extensions/msteams/src/index.ts`** — Exports the new public surface: `MonitorMSTeamsOpts`, `MSTeamsOnListeningCallback`, `probeWebhookPort`, `ProbeMSTeamsResult`. ## Test plan - [ ] Start gateway with msteams configured: `openclaw gateway start` - [ ] Verify only one `msteams provider started on port 3978` log line appears - [ ] Verify no `auto-restart attempt` log lines appear after a clean start - [ ] Verify provider remains stable and processes incoming webhooks - [ ] Stop gateway: `openclaw gateway stop` — confirm clean shutdown log appears - [ ] Restart gateway — confirm single-start behaviour repeats - [ ] Configure an invalid port (e.g. port 1 on Linux without root) — confirm the error is reported as a startup failure and the provider does not loop

Most Similar PRs