mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(hooks): return 200 instead of 202 for webhook responses (#28204)
* fix(hooks): return 200 instead of 202 for webhook responses (#22036) * docs(webhook): document 200 status for hooks agent * chore(changelog): add webhook ack note openclaw#28204 thanks @Glucksberg --------- Co-authored-by: Shakker <shakkerdroid@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/Control UI basePath webhook passthrough: let non-read methods under configured `controlUiBasePath` fall through to plugin routes (instead of returning Control UI 405), restoring webhook handlers behind basePath mounts. (#32311) Thanks @ademczuk.
|
||||
- Voice-call/Twilio signature verification: retry signature validation across deterministic URL port variants (with/without port) to handle mixed Twilio signing behavior behind reverse proxies and non-standard ports. (#25140) Thanks @drvoss.
|
||||
- Hooks/webhook ACK compatibility: return `200` (instead of `202`) for successful `/hooks/agent` requests so providers that require `200` (for example Forward Email) accept dispatched agent hook deliveries. (#28204) Thanks @Glucksberg.
|
||||
- Voice-call/Twilio external outbound: auto-register webhook-first `outbound-api` calls (initiated outside OpenClaw) so media streams are accepted and call direction metadata stays accurate. (#31181) Thanks @scoootscooob.
|
||||
- Voice-call/Twilio inbound greeting: run answered-call initial notify greeting for Twilio instead of skipping the manager speak path, with regression coverage for both Twilio and Plivo notify flows. (#29121) Thanks @xinhuagu.
|
||||
- Voice-call/stale call hydration: verify active calls with the provider before loading persisted in-progress calls so stale locally persisted records do not block or misroute new call handling after restarts. (#4325) Thanks @garnetlyx.
|
||||
|
||||
@@ -159,7 +159,7 @@ Mapping options (summary):
|
||||
## Responses
|
||||
|
||||
- `200` for `/hooks/wake`
|
||||
- `202` for `/hooks/agent` (async run started)
|
||||
- `200` for `/hooks/agent` (async run accepted)
|
||||
- `401` on auth failure
|
||||
- `429` after repeated auth failures from the same client (check `Retry-After`)
|
||||
- `400` on invalid payload
|
||||
|
||||
@@ -358,7 +358,7 @@ export function createHooksRequestHandler(
|
||||
}),
|
||||
agentId: targetAgentId,
|
||||
});
|
||||
sendJson(res, 202, { ok: true, runId });
|
||||
sendJson(res, 200, { ok: true, runId });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ export function createHooksRequestHandler(
|
||||
timeoutSeconds: mapped.action.timeoutSeconds,
|
||||
allowUnsafeExternalContent: mapped.action.allowUnsafeExternalContent,
|
||||
});
|
||||
sendJson(res, 202, { ok: true, runId });
|
||||
sendJson(res, 200, { ok: true, runId });
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("gateway server hooks", () => {
|
||||
|
||||
mockIsolatedRunOkOnce();
|
||||
const resAgent = await postHook(port, "/hooks/agent", { message: "Do it", name: "Email" });
|
||||
expect(resAgent.status).toBe(202);
|
||||
expect(resAgent.status).toBe(200);
|
||||
const agentEvents = await waitForSystemEvent();
|
||||
expect(agentEvents.some((e) => e.includes("Hook Email: done"))).toBe(true);
|
||||
drainSystemEvents(resolveMainKey());
|
||||
@@ -83,7 +83,7 @@ describe("gateway server hooks", () => {
|
||||
name: "Email",
|
||||
model: "openai/gpt-4.1-mini",
|
||||
});
|
||||
expect(resAgentModel.status).toBe(202);
|
||||
expect(resAgentModel.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const call = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
job?: { payload?: { model?: string } };
|
||||
@@ -97,7 +97,7 @@ describe("gateway server hooks", () => {
|
||||
name: "Email",
|
||||
agentId: "hooks",
|
||||
});
|
||||
expect(resAgentWithId.status).toBe(202);
|
||||
expect(resAgentWithId.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const routedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
job?: { agentId?: string };
|
||||
@@ -111,7 +111,7 @@ describe("gateway server hooks", () => {
|
||||
name: "Email",
|
||||
agentId: "missing-agent",
|
||||
});
|
||||
expect(resAgentUnknown.status).toBe(202);
|
||||
expect(resAgentUnknown.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const fallbackCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
job?: { agentId?: string };
|
||||
@@ -201,8 +201,15 @@ describe("gateway server hooks", () => {
|
||||
cronIsolatedRun.mockClear();
|
||||
cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" });
|
||||
|
||||
const defaultRoute = await postHook(port, "/hooks/agent", { message: "No key" });
|
||||
expect(defaultRoute.status).toBe(202);
|
||||
const defaultRoute = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer hook-secret",
|
||||
},
|
||||
body: JSON.stringify({ message: "No key" }),
|
||||
});
|
||||
expect(defaultRoute.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const defaultCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as
|
||||
| { sessionKey?: string }
|
||||
@@ -212,8 +219,15 @@ describe("gateway server hooks", () => {
|
||||
|
||||
cronIsolatedRun.mockClear();
|
||||
cronIsolatedRun.mockResolvedValue({ status: "ok", summary: "done" });
|
||||
const mappedOk = await postHook(port, "/hooks/mapped-ok", { subject: "hello", id: "42" });
|
||||
expect(mappedOk.status).toBe(202);
|
||||
const mappedOk = await fetch(`http://127.0.0.1:${port}/hooks/mapped-ok`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer hook-secret",
|
||||
},
|
||||
body: JSON.stringify({ subject: "hello", id: "42" }),
|
||||
});
|
||||
expect(mappedOk.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const mappedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as
|
||||
| { sessionKey?: string }
|
||||
@@ -249,7 +263,7 @@ describe("gateway server hooks", () => {
|
||||
agentId: "hooks",
|
||||
sessionKey: "agent:hooks:slack:channel:c123",
|
||||
});
|
||||
expect(resAgent.status).toBe(202);
|
||||
expect(resAgent.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
|
||||
const routedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as
|
||||
@@ -279,7 +293,7 @@ describe("gateway server hooks", () => {
|
||||
await withGatewayServer(async ({ port }) => {
|
||||
mockIsolatedRunOkOnce();
|
||||
const resNoAgent = await postHook(port, "/hooks/agent", { message: "No explicit agent" });
|
||||
expect(resNoAgent.status).toBe(202);
|
||||
expect(resNoAgent.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const noAgentCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
job?: { agentId?: string };
|
||||
@@ -292,7 +306,7 @@ describe("gateway server hooks", () => {
|
||||
message: "Allowed",
|
||||
agentId: "hooks",
|
||||
});
|
||||
expect(resAllowed.status).toBe(202);
|
||||
expect(resAllowed.status).toBe(200);
|
||||
await waitForSystemEvent();
|
||||
const allowedCall = (cronIsolatedRun.mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
job?: { agentId?: string };
|
||||
|
||||
Reference in New Issue
Block a user