Showing the changes to apps/cloud/src (1 of 6 files). Show all
Unified · Split
| @@ -45,6 +45,25 @@ const jsonRpcResponse = ( · 44 unmodified lines | ||
| 45 | 45 | ? jsonRpcErrorBody(status, code, message) |
| 46 | 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 | + */ | |
| 55 | +const 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 | + | |
| 48 | 67 | const renderAuthError = ( |
| 49 | 68 | auth: McpAuthProvider["Service"], |
| 50 | 69 | request: Request, |
| @@ -217,7 +236,7 @@ export const makeCloudMcpAgentHandler = () => { · 166 unmodified lines | ||
| 217 | 236 | |
| 218 | 237 | const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null; |
| 219 | 238 | if (sessionId && !existingSession) { |
| 220 | − return jsonRpcResponse(404, -32001, "Session not found"); | |
| 239 | + return deadSessionResponse(request.method, "Session not found"); | |
| 221 | 240 | } |
| 222 | 241 | if (existingSession) { |
| 223 | 242 | const owner = await existingSession.validateMcpSessionOwner({ |
| @@ -225,13 +244,13 @@ export const makeCloudMcpAgentHandler = () => { · 1 unmodified line | ||
| 225 | 244 | organizationId: outcome.principal.organizationId, |
| 226 | 245 | }); |
| 227 | 246 | if (owner === "not_found") { |
| 228 | − return jsonRpcResponse(404, -32001, "Session not found"); | |
| 247 | + return deadSessionResponse(request.method, "Session not found"); | |
| 229 | 248 | } |
| 230 | 249 | if (owner === "terminated") { |
| 231 | 250 | // DELETE-condemned but the deferred destroy alarm hasn't wiped storage |
| 232 | 251 | // yet. Same envelope as the post-destroy race below: the client must |
| 233 | 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 | 255 | if (owner === "forbidden") { |
| 237 | 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 | 286 | // client to be told to reconnect, matching a timed-out session). |
| 268 | 287 | // 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 | 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 | 291 | // 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 | 292 | throw error; |