History

Answer dead-session standalone GETs with 405 to stop reconnect loops (#1622)

Rhys Sullivan committed 2 months ago

Commit f3ec48d1 parent a8d3d3c6 files · +156 −15Browse files

Showing the changes to apps/cloud/src (1 of 6 files). Show all

Unified · Split

apps/cloud/src/mcp/agent-handler.ts+23−4
@@ -45,6 +45,25 @@ const jsonRpcResponse = ( · 44 unmodified lines
45 ? jsonRpcErrorBody(status, code, message)45 ? jsonRpcErrorBody(status, code, message)
46 : jsonRpcErrorBody(status, code, message, { challenge });46 : jsonRpcErrorBody(status, code, message, { challenge });
47 47
48/**
49 * A dead session id answers by request method. POST/DELETE keep the 404 that
50 * tells a compliant client to re-initialize. A standalone GET gets 405: the
51 * v1 SDK treats that as "no SSE stream offered" and stops retrying quietly,
52 * which breaks the reconnect loops of pre-cutover always-on deployments —
53 * their GET-404 path never re-initialized, it just retried forever.
54 */
55const deadSessionResponse = (method: string, message: string): Response =>
56 method === "GET"
57 ? new Response(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message }, id: null }), {
58 status: 405,
59 headers: {
60 "content-type": "application/json",
61 allow: "POST, DELETE",
62 "access-control-allow-origin": "*",
63 },
64 })
65 : jsonRpcResponse(404, -32001, message);
66
48const renderAuthError = (67const renderAuthError = (
49 auth: McpAuthProvider["Service"],68 auth: McpAuthProvider["Service"],
50 request: Request,69 request: Request,
@@ -217,7 +236,7 @@ export const makeCloudMcpAgentHandler = () => { · 166 unmodified lines
217 236
218 const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null;237 const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null;
219 if (sessionId && !existingSession) {238 if (sessionId && !existingSession) {
220 return jsonRpcResponse(404, -32001, "Session not found");239 return deadSessionResponse(request.method, "Session not found");
221 }240 }
222 if (existingSession) {241 if (existingSession) {
223 const owner = await existingSession.validateMcpSessionOwner({242 const owner = await existingSession.validateMcpSessionOwner({
@@ -225,13 +244,13 @@ export const makeCloudMcpAgentHandler = () => { · 1 unmodified line
225 organizationId: outcome.principal.organizationId,244 organizationId: outcome.principal.organizationId,
226 });245 });
227 if (owner === "not_found") {246 if (owner === "not_found") {
228 return jsonRpcResponse(404, -32001, "Session not found");247 return deadSessionResponse(request.method, "Session not found");
229 }248 }
230 if (owner === "terminated") {249 if (owner === "terminated") {
231 // DELETE-condemned but the deferred destroy alarm hasn't wiped storage250 // DELETE-condemned but the deferred destroy alarm hasn't wiped storage
232 // yet. Same envelope as the post-destroy race below: the client must251 // yet. Same envelope as the post-destroy race below: the client must
233 // treat the id as dead and reconnect.252 // treat the id as dead and reconnect.
234 return jsonRpcResponse(404, -32001, "Session timed out, please reconnect");253 return deadSessionResponse(request.method, "Session timed out, please reconnect");
235 }254 }
236 if (owner === "forbidden") {255 if (owner === "forbidden") {
237 return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer");256 return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer");
@@ -267,7 +286,7 @@ export const makeCloudMcpAgentHandler = () => { · 29 unmodified lines
267 // client to be told to reconnect, matching a timed-out session).286 // client to be told to reconnect, matching a timed-out session).
268 // oxlint-disable-next-line executor/no-unknown-error-message -- adapter boundary: the abort reason is a plain runtime Error whose message IS the signal287 // oxlint-disable-next-line executor/no-unknown-error-message -- adapter boundary: the abort reason is a plain runtime Error whose message IS the signal
269 if (Predicate.isError(error) && error.message === "destroyed") {288 if (Predicate.isError(error) && error.message === "destroyed") {
270 return jsonRpcResponse(404, -32001, "Session timed out, please reconnect");289 return deadSessionResponse(request.method, "Session timed out, please reconnect");
271 }290 }
272 // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: rethrow anything that isn't the condemned-DO abort to the Workers runtime unchanged291 // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: rethrow anything that isn't the condemned-DO abort to the Workers runtime unchanged
273 throw error;292 throw error;