{"record":{"id":"cc67a1a50e152455","repo":"github/copilot-sdk","slug":"permission-handlers-cannot-return-no-result-when","errorCode":null,"errorMessage":"Permission handlers cannot return 'no-result' when connected to a protocol v2 server.","messagePattern":"Permission handlers cannot return 'no-result' when connected to a protocol v2 server\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java","lineNumber":298,"sourceCode":"            try {\n                String sessionId = params.get(\"sessionId\").asText();\n                JsonNode permissionRequest = params.get(\"permissionRequest\");\n\n                CopilotSession session = sessions.get(sessionId);\n                if (session == null) {\n                    var result = new PermissionRequestResult()\n                            .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER);\n                    rpc.sendResponse(requestIdLong, Map.of(\"result\", result));\n                    return;\n                }\n\n                session.handlePermissionRequest(permissionRequest).thenAccept(result -> {\n                    try {\n                        if (PermissionRequestResultKind.NO_RESULT.getValue().equalsIgnoreCase(result.getKind())) {\n                            // Protocol v2 does not support NO_RESULT — the server\n                            // expects exactly one response per request, so abstaining\n                            // would leave it hanging.\n                            throw new IllegalStateException(\n                                    \"Permission handlers cannot return 'no-result' when connected to a protocol v2 server.\");\n                        }\n                        rpc.sendResponse(requestIdLong, Map.of(\"result\", result));\n                    } catch (IOException e) {\n                        LOG.log(Level.SEVERE, \"Error sending permission result\", e);\n                    }\n                }).exceptionally(ex -> {\n                    try {\n                        var result = new PermissionRequestResult()\n                                .setKind(PermissionRequestResultKind.DENIED_COULD_NOT_REQUEST_FROM_USER);\n                        rpc.sendResponse(requestIdLong, Map.of(\"result\", result));\n                    } catch (IOException e) {\n                        LOG.log(Level.SEVERE, \"Error sending permission denied\", e);\n                    }\n                    return null;\n                });\n            } catch (Exception e) {\n                LOG.log(Level.SEVERE, \"Error handling permission request\", e);","sourceCodeStart":280,"sourceCodeEnd":316,"githubUrl":"https://github.com/github/copilot-sdk/blob/cd8cf15dc3f9e762615790aaed0a771a0f392755/java/sdk/src/main/java/com/github/copilot/RpcHandlerDispatcher.java#L280-L316","documentation":"This IllegalStateException is raised by RpcHandlerDispatcher.handlePermissionRequest when a registered permission handler resolves a permission request with kind 'no-result' while connected to a protocol v2 server. Protocol v2 requires exactly one response per request, so an abstaining handler would leave the server waiting forever; the dispatcher rejects it. Note the exception is thrown inside a thenAccept callback, so it propagates to the completion handler rather than the caller.","triggerScenarios":"A session.handlePermissionRequest implementation returns a PermissionRequestResult whose getKind() equals the NO_RESULT value, while the dispatcher is serving a v2 protocol server (registerHandlers wired the permission handler for v2).","commonSituations":"Shared handler code written for protocol v1 (where no-result/abstain was allowed) reused against a v2 server; UI permission prompts that can be dismissed without an answer and map dismissal to NO_RESULT; upgrading the server to v2 without auditing handler return kinds.","solutions":["Change the permission handler to always return a concrete decision (approve or deny) instead of NO_RESULT when on v2","Map 'dismissed/no answer' UI outcomes to an explicit deny before returning the result","Log the NO_RESULT case and fall back to a deny response so the server always receives exactly one answer","Verify the negotiated protocol version and use v1-compatible abstention only when connected to v1"],"exampleFix":"// before\nif (userDismissed) {\n    return new PermissionRequestResult(PermissionRequestResultKind.NO_RESULT);\n}\n// after\nif (userDismissed) {\n    return new PermissionRequestResult(PermissionRequestResultKind.DENY);\n}","handlingStrategy":"validation","validationCode":"if (PermissionRequestResultKind.NO_RESULT.getValue().equalsIgnoreCase(result.getKind())) {\n    result = new PermissionRequestResult(PermissionRequestResultKind.DENY);\n}","typeGuard":null,"tryCatchPattern":"session.handlePermissionRequest(req)\n    .thenAccept(result -> { /* ensure kind != NO_RESULT before dispatch */ })\n    .exceptionally(ex -> { LOG.log(Level.SEVERE, \"permission handler failed\", ex); return null; });","preventionTips":["Never return NO_RESULT in handlers registered for protocol v2","Map dismissed/abstained UI outcomes to explicit deny","Audit handler return kinds when upgrading server protocol version","Attach exceptionally() to handler futures so callback throws are logged"],"tags":["protocol","permissions","rpc","v2"],"backgroundTag":"unsupported-operation","analyzedSha":"cd8cf15dc3f9e762615790aaed0a771a0f392755","analyzedAt":"2026-09-09T18:32:31.973Z","contentChangedAt":"2026-09-09T18:32:31.973Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}