Gateway: honor message-channel header for chat completions#30462
Conversation
Greptile SummaryThis PR fixes a bug where Changes:
The implementation is minimal, well-scoped, and correctly leverages the shared Confidence Score: 5/5
Last reviewed commit: 10ed4ec |
src/gateway/openai-http.ts
Outdated
| const messageChannel = | ||
| normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel") ?? "") ?? "webchat"; |
There was a problem hiding this comment.
Redundant ?? "" coercion
normalizeMessageChannel already accepts string | null | undefined and returns undefined for any falsy input (including null and undefined). The ?? "" is unnecessary since passing null/undefined directly produces the same result as passing "".
| const messageChannel = | |
| normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel") ?? "") ?? "webchat"; | |
| const messageChannel = | |
| normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? "webchat"; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.ts
Line: 231-232
Comment:
**Redundant `?? ""` coercion**
`normalizeMessageChannel` already accepts `string | null | undefined` and returns `undefined` for any falsy input (including `null` and `undefined`). The `?? ""` is unnecessary since passing `null`/`undefined` directly produces the same result as passing `""`.
```suggestion
const messageChannel =
normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? "webchat";
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Applied on head 10ed4ece1a.
- Removed redundant
?? ""beforenormalizeMessageChannel(...)inopenai-http.ts. - Added regression coverage for the backward-compatible default path:
defaults messageChannel to webchat when header is absent.
Verification:
pnpm -s vitest run src/gateway/openai-http.message-channel.test.ts
| it("passes x-openclaw-message-channel through to agentCommand", async () => { | ||
| agentCommand.mockReset(); | ||
| agentCommand.mockResolvedValueOnce({ payloads: [{ text: "ok" }] } as never); | ||
|
|
||
| await withGatewayServer( | ||
| async ({ port }) => { | ||
| const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { | ||
| method: "POST", | ||
| headers: { | ||
| "content-type": "application/json", | ||
| authorization: "Bearer secret", | ||
| "x-openclaw-message-channel": "custom-client-channel", | ||
| }, | ||
| body: JSON.stringify({ | ||
| model: "openclaw", | ||
| messages: [{ role: "user", content: "hi" }], | ||
| }), | ||
| }); | ||
|
|
||
| expect(res.status).toBe(200); | ||
| const firstCall = (agentCommand.mock.calls[0] as unknown[] | undefined)?.[0] as | ||
| | { messageChannel?: string } | ||
| | undefined; | ||
| expect(firstCall?.messageChannel).toBe("custom-client-channel"); | ||
| await res.text(); | ||
| }, | ||
| { | ||
| serverOptions: { | ||
| host: "127.0.0.1", | ||
| auth: { mode: "token", token: "secret" }, | ||
| controlUiEnabled: false, | ||
| openAiChatCompletionsEnabled: true, | ||
| }, | ||
| }, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Missing test for default "webchat" fallback
The test suite covers the header-present case, but there's no assertion that when x-openclaw-message-channel is absent the channel still defaults to "webchat". This is the backward-compatibility guarantee the PR is explicitly preserving, and a regression there would be silent.
Consider adding a second it case that omits the header and asserts firstCall?.messageChannel === "webchat".
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/openai-http.message-channel.test.ts
Line: 7-42
Comment:
**Missing test for default `"webchat"` fallback**
The test suite covers the header-present case, but there's no assertion that when `x-openclaw-message-channel` is absent the channel still defaults to `"webchat"`. This is the backward-compatibility guarantee the PR is explicitly preserving, and a regression there would be silent.
Consider adding a second `it` case that omits the header and asserts `firstCall?.messageChannel === "webchat"`.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Applied on head 10ed4ece1a.
- Removed redundant
?? ""beforenormalizeMessageChannel(...)inopenai-http.ts. - Added regression coverage for the backward-compatible default path:
defaults messageChannel to webchat when header is absent.
Verification:
pnpm -s vitest run src/gateway/openai-http.message-channel.test.ts
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🔵 Unvalidated x-openclaw-message-channel header allows log/prompt injection via arbitrary messageChannel
Description
Vulnerable code: const messageChannel =
normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? "webchat";RecommendationTreat Option A (recommended): enforce a strict, length-bounded identifier format and drop invalid values: function normalizeSafeMessageChannel(raw?: string | null): string | undefined {
const v = raw?.trim().toLowerCase();
if (!v) return undefined;
// 1–64 chars, ASCII safe, prevents bidi/control/whitespace surprises.
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(v)) return undefined;
return normalizeMessageChannel(v); // or inline the built-in/plugin mapping
}
const messageChannel = normalizeSafeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? "webchat";Option B: allow only known channels (built-in + registered plugin ids), otherwise default. Additionally, when logging Analyzed PR: #30462 at commit Last updated on: 2026-03-02T23:58:55Z |
|
@greptile-apps please re-review latest head for updated confidence score. |
|
@greptile review latest commit, please. |
10ed4ec to
9cf5bcd
Compare
|
Landed via temp rebase onto main.
Thanks @bmendonca3! |
Summary
Describe the problem and fix in 2–5 bullets:
POST /v1/chat/completionsignoredx-openclaw-message-channeland always invokedagentCommandwithmessageChannel: "webchat".x-openclaw-message-channel, then passes it through toagentCommandinstead of hardcodingwebchat./v1/responses, or any other HTTP endpoint.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
POST /v1/chat/completionsnow preservesx-openclaw-message-channelwhen invoking the agent run instead of forcing the origin channel towebchat.Security Impact (required)
Yes/No) NoYes/No) NoYes/No) NoYes/No) NoYes/No) NoYes, explain risk + mitigation:Repro + Verification
Environment
POST /v1/chat/completionsopenAiChatCompletionsEnabled: trueSteps
upstream/main, runpnpm exec vitest run --config /tmp/openclaw-repros/vitest.config.cjs /tmp/openclaw-repros/29449-openai-message-channel.test.ts --reporter=verbose.agentCommandcall receivesmessageChannel: "webchat"even when the request header setsx-openclaw-message-channel: custom-client-channel.pnpm exec vitest run src/gateway/openai-http.message-channel.test.ts --reporter=verbose.Expected
/v1/chat/completionsshould pass the normalizedx-openclaw-message-channelheader through toagentCommand.Actual
messageChannel: "webchat".custom-client-channelintoagentCommand.Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
src/gateway/openai-http.message-channel.test.ts; confirmed both the throwaway repro and the in-repo gateway test now observemessageChannel: "custom-client-channel".Verification commands:
pnpm exec vitest run src/gateway/openai-http.message-channel.test.ts --reporter=verbosepnpm exec vitest run --config /tmp/openclaw-repros/vitest.config.cjs /tmp/openclaw-repros/29449-openai-message-channel.test.ts --reporter=verbosepnpm checkcurrently stops on the existing unrelatedsrc/gateway/server-cron.ts(197)type error present on currentupstream/main; this PR does not touch that file.Compatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoFailure Recovery (if this breaks)
4bc1b6ddd3.src/gateway/openai-http.tsandsrc/gateway/openai-http.message-channel.test.ts.Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.webchat.normalizeMessageChannelhelper, matching the behavior already used by other gateway HTTP entry points.