Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions internal/api/src/routes/agent-request.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,31 @@ export default async function handleAgentRequest(
const query = await db.selectAgentDeploymentByRequestID(id);
if (!query) {
// There is no agent for this request, check if it's a dev request.
const response = await c.env.devhook?.handleRequest(id, c.req.raw);
if (response) {
return response;
if (c.env.devhook) {
// Rewrite the URL so the devhook receives just the subpath, not the
// full `/api/webhook/{id}/...` prefix. This mirrors the rewriting
// done for deployed agents below (lines 99-106).
const incomingUrl = new URL(c.req.raw.url);
let devhookPath: string;
if (routing.mode === "webhook") {
devhookPath = routing.subpath || "/";
} else {
devhookPath = incomingUrl.pathname;
}
const devhookUrl = new URL(devhookPath, incomingUrl.origin);
devhookUrl.search = incomingUrl.search;

const devhookReq = new Request(devhookUrl.toString(), {
method: c.req.raw.method,
headers: c.req.raw.headers,
body: c.req.raw.body,
// @ts-expect-error - Required for Node.js streaming.
duplex: c.req.raw.body ? "half" : undefined,
});
const response = await c.env.devhook.handleRequest(id, devhookReq);
if (response) {
return response;
}
}
return c.json({ message: "No agent exists for this webook" }, 404);
}
Expand Down
49 changes: 49 additions & 0 deletions internal/api/src/routes/agent-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,55 @@ describe("subdomain requests", () => {
});
});

describe("devhook URL rewriting", () => {
const fakeId = "00000000-0000-0000-0000-000000000000";

test("devhook receives subpath for webhook requests", async () => {
let receivedUrl: string | undefined;
const { url: apiUrl } = await serve({
bindings: {
devhook: {
handleRequest: async (_id: string, req: Request) => {
receivedUrl = req.url;
return new Response("devhook OK");
},
},
},
});

// Request with subpath /slack — devhook should receive just /slack
const response = await fetch(
`${apiUrl}/api/webhook/${fakeId}/slack?foo=bar`
);
expect(response.status).toBe(200);
expect(await response.text()).toBe("devhook OK");

const parsed = new URL(receivedUrl!);
expect(parsed.pathname).toBe("/slack");
expect(parsed.searchParams.get("foo")).toBe("bar");
});

test("devhook receives / for webhook requests without subpath", async () => {
let receivedUrl: string | undefined;
const { url: apiUrl } = await serve({
bindings: {
devhook: {
handleRequest: async (_id: string, req: Request) => {
receivedUrl = req.url;
return new Response("devhook OK");
},
},
},
});

const response = await fetch(`${apiUrl}/api/webhook/${fakeId}`);
expect(response.status).toBe(200);

const parsed = new URL(receivedUrl!);
expect(parsed.pathname).toBe("/");
});
});

describe("Slack verification expiration", () => {
test("clears expired slack_verification and skips verification processing", async () => {
// Set expiresAt to 1 hour ago (already expired)
Expand Down