refactor(gateway): dedupe json endpoint prelude

This commit is contained in:
Peter Steinberger
2026-02-15 13:24:33 +00:00
parent 052d988add
commit 9e2233da7f
4 changed files with 195 additions and 90 deletions

View File

@@ -0,0 +1,80 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { describe, expect, it, vi } from "vitest";
import type { ResolvedGatewayAuth } from "./auth.js";
import { handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers.js";
vi.mock("./http-auth-helpers.js", () => {
return {
authorizeGatewayBearerRequestOrReply: vi.fn(),
};
});
vi.mock("./http-common.js", () => {
return {
readJsonBodyOrError: vi.fn(),
sendMethodNotAllowed: vi.fn(),
};
});
const { authorizeGatewayBearerRequestOrReply } = await import("./http-auth-helpers.js");
const { readJsonBodyOrError, sendMethodNotAllowed } = await import("./http-common.js");
describe("handleGatewayPostJsonEndpoint", () => {
it("returns false when path does not match", async () => {
const result = await handleGatewayPostJsonEndpoint(
{
url: "/nope",
method: "POST",
headers: { host: "localhost" },
} as unknown as IncomingMessage,
{} as unknown as ServerResponse,
{ pathname: "/v1/ok", auth: {} as unknown as ResolvedGatewayAuth, maxBodyBytes: 1 },
);
expect(result).toBe(false);
});
it("returns undefined and replies when method is not POST", async () => {
const mockedSendMethodNotAllowed = vi.mocked(sendMethodNotAllowed);
mockedSendMethodNotAllowed.mockClear();
const result = await handleGatewayPostJsonEndpoint(
{
url: "/v1/ok",
method: "GET",
headers: { host: "localhost" },
} as unknown as IncomingMessage,
{} as unknown as ServerResponse,
{ pathname: "/v1/ok", auth: {} as unknown as ResolvedGatewayAuth, maxBodyBytes: 1 },
);
expect(result).toBeUndefined();
expect(mockedSendMethodNotAllowed).toHaveBeenCalledTimes(1);
});
it("returns undefined when auth fails", async () => {
vi.mocked(authorizeGatewayBearerRequestOrReply).mockResolvedValue(false);
const result = await handleGatewayPostJsonEndpoint(
{
url: "/v1/ok",
method: "POST",
headers: { host: "localhost" },
} as unknown as IncomingMessage,
{} as unknown as ServerResponse,
{ pathname: "/v1/ok", auth: {} as unknown as ResolvedGatewayAuth, maxBodyBytes: 1 },
);
expect(result).toBeUndefined();
});
it("returns body when auth succeeds and JSON parsing succeeds", async () => {
vi.mocked(authorizeGatewayBearerRequestOrReply).mockResolvedValue(true);
vi.mocked(readJsonBodyOrError).mockResolvedValue({ hello: "world" });
const result = await handleGatewayPostJsonEndpoint(
{
url: "/v1/ok",
method: "POST",
headers: { host: "localhost" },
} as unknown as IncomingMessage,
{} as unknown as ServerResponse,
{ pathname: "/v1/ok", auth: {} as unknown as ResolvedGatewayAuth, maxBodyBytes: 123 },
);
expect(result).toEqual({ body: { hello: "world" } });
});
});

View File

@@ -0,0 +1,45 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { AuthRateLimiter } from "./auth-rate-limit.js";
import type { ResolvedGatewayAuth } from "./auth.js";
import { authorizeGatewayBearerRequestOrReply } from "./http-auth-helpers.js";
import { readJsonBodyOrError, sendMethodNotAllowed } from "./http-common.js";
export async function handleGatewayPostJsonEndpoint(
req: IncomingMessage,
res: ServerResponse,
opts: {
pathname: string;
auth: ResolvedGatewayAuth;
maxBodyBytes: number;
trustedProxies?: string[];
rateLimiter?: AuthRateLimiter;
},
): Promise<false | { body: unknown } | undefined> {
const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname !== opts.pathname) {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res);
return undefined;
}
const authorized = await authorizeGatewayBearerRequestOrReply({
req,
res,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
rateLimiter: opts.rateLimiter,
});
if (!authorized) {
return undefined;
}
const body = await readJsonBodyOrError(req, res, opts.maxBodyBytes);
if (body === undefined) {
return undefined;
}
return { body };
}

View File

@@ -11,14 +11,8 @@ import {
buildAgentMessageFromConversationEntries,
type ConversationEntry,
} from "./agent-prompt.js";
import { authorizeGatewayBearerRequestOrReply } from "./http-auth-helpers.js";
import {
readJsonBodyOrError,
sendJson,
sendMethodNotAllowed,
setSseHeaders,
writeDone,
} from "./http-common.js";
import { sendJson, setSseHeaders, writeDone } from "./http-common.js";
import { handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers.js";
import { resolveAgentIdForRequest, resolveSessionKey } from "./http-utils.js";
type OpenAiHttpOptions = {
@@ -151,33 +145,21 @@ export async function handleOpenAiHttpRequest(
res: ServerResponse,
opts: OpenAiHttpOptions,
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname !== "/v1/chat/completions") {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res);
return true;
}
const authorized = await authorizeGatewayBearerRequestOrReply({
req,
res,
const handled = await handleGatewayPostJsonEndpoint(req, res, {
pathname: "/v1/chat/completions",
auth: opts.auth,
trustedProxies: opts.trustedProxies,
rateLimiter: opts.rateLimiter,
maxBodyBytes: opts.maxBodyBytes ?? 1024 * 1024,
});
if (!authorized) {
if (handled === false) {
return false;
}
if (!handled) {
return true;
}
const body = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? 1024 * 1024);
if (body === undefined) {
return true;
}
const payload = coerceRequest(body);
const payload = coerceRequest(handled.body);
const stream = Boolean(payload.stream);
const model = typeof payload.model === "string" ? payload.model : "openclaw";
const user = typeof payload.user === "string" ? payload.user : undefined;

View File

@@ -40,14 +40,8 @@ import {
buildAgentMessageFromConversationEntries,
type ConversationEntry,
} from "./agent-prompt.js";
import { authorizeGatewayBearerRequestOrReply } from "./http-auth-helpers.js";
import {
readJsonBodyOrError,
sendJson,
sendMethodNotAllowed,
setSseHeaders,
writeDone,
} from "./http-common.js";
import { sendJson, setSseHeaders, writeDone } from "./http-common.js";
import { handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers.js";
import { resolveAgentIdForRequest, resolveSessionKey } from "./http-utils.js";
import {
CreateResponseBodySchema,
@@ -319,45 +313,61 @@ function createAssistantOutputItem(params: {
};
}
async function runResponsesAgentCommand(params: {
message: string;
images: ImageContent[];
clientTools: ClientToolDefinition[];
extraSystemPrompt: string;
streamParams: { maxTokens: number } | undefined;
sessionKey: string;
runId: string;
deps: ReturnType<typeof createDefaultDeps>;
}) {
return agentCommand(
{
message: params.message,
images: params.images.length > 0 ? params.images : undefined,
clientTools: params.clientTools.length > 0 ? params.clientTools : undefined,
extraSystemPrompt: params.extraSystemPrompt || undefined,
streamParams: params.streamParams ?? undefined,
sessionKey: params.sessionKey,
runId: params.runId,
deliver: false,
messageChannel: "webchat",
bestEffortDeliver: false,
},
defaultRuntime,
params.deps,
);
}
export async function handleOpenResponsesHttpRequest(
req: IncomingMessage,
res: ServerResponse,
opts: OpenResponsesHttpOptions,
): Promise<boolean> {
const url = new URL(req.url ?? "/", `http://${req.headers.host || "localhost"}`);
if (url.pathname !== "/v1/responses") {
return false;
}
if (req.method !== "POST") {
sendMethodNotAllowed(res);
return true;
}
const authorized = await authorizeGatewayBearerRequestOrReply({
req,
res,
auth: opts.auth,
trustedProxies: opts.trustedProxies,
rateLimiter: opts.rateLimiter,
});
if (!authorized) {
return true;
}
const limits = resolveResponsesLimits(opts.config);
const maxBodyBytes =
opts.maxBodyBytes ??
(opts.config?.maxBodyBytes
? limits.maxBodyBytes
: Math.max(limits.maxBodyBytes, limits.files.maxBytes * 2, limits.images.maxBytes * 2));
const body = await readJsonBodyOrError(req, res, maxBodyBytes);
if (body === undefined) {
const handled = await handleGatewayPostJsonEndpoint(req, res, {
pathname: "/v1/responses",
auth: opts.auth,
trustedProxies: opts.trustedProxies,
rateLimiter: opts.rateLimiter,
maxBodyBytes,
});
if (handled === false) {
return false;
}
if (!handled) {
return true;
}
// Validate request body with Zod
const parseResult = CreateResponseBodySchema.safeParse(body);
const parseResult = CreateResponseBodySchema.safeParse(handled.body);
if (!parseResult.success) {
const issue = parseResult.error.issues[0];
const message = issue ? `${issue.path.join(".")}: ${issue.message}` : "Invalid request body";
@@ -520,22 +530,16 @@ export async function handleOpenResponsesHttpRequest(
if (!stream) {
try {
const result = await agentCommand(
{
message: prompt.message,
images: images.length > 0 ? images : undefined,
clientTools: resolvedClientTools.length > 0 ? resolvedClientTools : undefined,
extraSystemPrompt: extraSystemPrompt || undefined,
streamParams: streamParams ?? undefined,
sessionKey,
runId: responseId,
deliver: false,
messageChannel: "webchat",
bestEffortDeliver: false,
},
defaultRuntime,
const result = await runResponsesAgentCommand({
message: prompt.message,
images,
clientTools: resolvedClientTools,
extraSystemPrompt,
streamParams,
sessionKey,
runId: responseId,
deps,
);
});
const payloads = (result as { payloads?: Array<{ text?: string }> } | null)?.payloads;
const usage = extractUsageFromResult(result);
@@ -760,22 +764,16 @@ export async function handleOpenResponsesHttpRequest(
void (async () => {
try {
const result = await agentCommand(
{
message: prompt.message,
images: images.length > 0 ? images : undefined,
clientTools: resolvedClientTools.length > 0 ? resolvedClientTools : undefined,
extraSystemPrompt: extraSystemPrompt || undefined,
streamParams: streamParams ?? undefined,
sessionKey,
runId: responseId,
deliver: false,
messageChannel: "webchat",
bestEffortDeliver: false,
},
defaultRuntime,
const result = await runResponsesAgentCommand({
message: prompt.message,
images,
clientTools: resolvedClientTools,
extraSystemPrompt,
streamParams,
sessionKey,
runId: responseId,
deps,
);
});
finalUsage = extractUsageFromResult(result);
maybeFinalize();