@@ -2340,6 +2340,70 @@ static cbm_daemon_runtime_application_status_t application_set_context(
23402340 return CBM_DAEMON_RUNTIME_APPLICATION_OK ;
23412341}
23422342
2343+ /* Build the JSON-RPC error that replaces a reply too large to frame.
2344+ *
2345+ * Says the two numbers a caller needs to act — what the reply measured and what
2346+ * fits — because the alternative the reporter lived through was a silent exit
2347+ * with nothing to go on. The advice is concrete for the same reason: "narrow the
2348+ * projection or add LIMIT" is actionable, "internal error" is not.
2349+ *
2350+ * `request` may be NULL when the message did not parse; the response then omits
2351+ * the id, which is what JSON-RPC requires for an unidentifiable request. */
2352+ static char * application_oversized_response_error (const cbm_jsonrpc_request_t * request ,
2353+ size_t response_length ) {
2354+ char message [CBM_SZ_256 ];
2355+ (void )snprintf (message , sizeof (message ),
2356+ "response too large: %zu bytes exceeds the %u byte transport limit; "
2357+ "narrow the projection, add LIMIT, or paginate" ,
2358+ response_length , (unsigned )CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX );
2359+
2360+ yyjson_mut_doc * doc = yyjson_mut_doc_new (NULL );
2361+ if (!doc ) {
2362+ return NULL ;
2363+ }
2364+ yyjson_mut_val * err = yyjson_mut_obj (doc );
2365+ yyjson_mut_doc_set_root (doc , err );
2366+ /* -32603 (internal error) is the closest standard code: the request was
2367+ * well-formed and the failure is server-side. */
2368+ yyjson_mut_obj_add_int (doc , err , "code" , -32603 );
2369+ yyjson_mut_obj_add_str (doc , err , "message" , message );
2370+ char * error_json = yyjson_mut_write (doc , 0 , NULL );
2371+ yyjson_mut_doc_free (doc );
2372+ if (!error_json ) {
2373+ return NULL ;
2374+ }
2375+
2376+ cbm_jsonrpc_response_t response = {
2377+ .id = request && request -> has_id ? request -> id : 0 ,
2378+ .id_str = request ? request -> id_str : NULL ,
2379+ .error_json = error_json ,
2380+ .error_code = -32603 ,
2381+ };
2382+ char * encoded = cbm_jsonrpc_format_response (& response );
2383+ free (error_json );
2384+ return encoded ;
2385+ }
2386+
2387+ /* Decide whether `response` can be framed, substituting the error when it
2388+ * cannot. Owns `response` either way and returns the reply to send, or NULL
2389+ * only on allocation failure. This is ONE function on purpose: the bug was the
2390+ * DECISION (an oversized reply travelling on as if it were fine), so the
2391+ * decision and the substitution have to be testable together — a test that
2392+ * only builds the error passes even with the size check removed. */
2393+ static char * application_framable_response (char * response , const cbm_jsonrpc_request_t * request ) {
2394+ if (!response || strlen (response ) <= CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX ) {
2395+ return response ;
2396+ }
2397+ char * replacement = application_oversized_response_error (request , strlen (response ));
2398+ free (response );
2399+ return replacement ;
2400+ }
2401+
2402+ char * cbm_daemon_application_framable_response_for_test (char * response ,
2403+ const cbm_jsonrpc_request_t * request ) {
2404+ return application_framable_response (response , request );
2405+ }
2406+
23432407static cbm_daemon_runtime_application_status_t application_mcp_request (
23442408 cbm_daemon_application_session_t * session , const uint8_t * request , uint32_t request_length ,
23452409 uint8_t * * response_out , uint32_t * response_length_out ) {
@@ -2365,18 +2429,37 @@ static cbm_daemon_runtime_application_status_t application_mcp_request(
23652429 } else if (tool_request && response ) {
23662430 application_update_notice_inject (session , & response );
23672431 }
2368- if (parsed_ok ) {
2369- cbm_jsonrpc_request_free (& parsed );
2370- }
23712432 if (response ) {
23722433 size_t response_length = strlen (response );
23732434 if (response_length > UINT32_MAX ) {
2435+ if (parsed_ok ) {
2436+ cbm_jsonrpc_request_free (& parsed );
2437+ }
23742438 free (response );
23752439 return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR ;
23762440 }
2441+ /* A reply larger than one frame has to become a JSON-RPC error HERE,
2442+ * while the request id is still in hand. Handing it to the transport
2443+ * instead is indistinguishable from a dead socket at the frontend
2444+ * worker, which responds by _Exit()ing the process — right for a broken
2445+ * transport, a fatal over-reaction to a large answer. Under an MCP host
2446+ * that does not respawn the server, that took every tool on it out for
2447+ * the rest of the session, leaving exit=1 and an empty stderr to debug
2448+ * from (#1375). */
2449+ response = application_framable_response (response , parsed_ok ? & parsed : NULL );
2450+ if (!response ) {
2451+ if (parsed_ok ) {
2452+ cbm_jsonrpc_request_free (& parsed );
2453+ }
2454+ return CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR ;
2455+ }
2456+ response_length = strlen (response );
23772457 * response_out = (uint8_t * )response ;
23782458 * response_length_out = (uint32_t )response_length ;
23792459 }
2460+ if (parsed_ok ) {
2461+ cbm_jsonrpc_request_free (& parsed );
2462+ }
23802463 application_refresh_watch (session );
23812464 return CBM_DAEMON_RUNTIME_APPLICATION_OK ;
23822465}
0 commit comments