Lift app-server JSON-RPC error handling to request boundary (#19484)

## Why

App-server request handling had a lot of repeated JSON-RPC error
construction and one-off `send_error`/`return` branches. This made small
handlers noisy and pushed error response details into leaf code that
otherwise only needed to validate input or call the underlying API.

## What Changed

- Added shared JSON-RPC error constructors in
`codex-rs/app-server/src/error_code.rs`.
- Lifted straightforward request result emission into
`codex-rs/app-server/src/message_processor.rs` so response/error
dispatch happens at the request boundary.
- Reused the result helpers across command exec, config, filesystem,
device-key, external-agent config, fs-watch, and outgoing-message paths.
- Removed leaf wrapper handlers where the method body was only
forwarding to a response helper.
- Returned request validation errors upward in the simple cases instead
of sending an error locally and immediately returning.

## Verification

- `cargo test -p codex-app-server --lib command_exec::tests`
- `cargo test -p codex-app-server --lib outgoing_message::tests`
- `cargo test -p codex-app-server --lib in_process::tests`
- `cargo test -p codex-app-server --test all v2::fs`
- `cargo test -p codex-app-server --test all v2::config_rpc`
- `cargo test -p codex-app-server --test all v2::external_agent_config`
- `cargo test -p codex-app-server --test all v2::initialize`
- `just fix -p codex-app-server`
- `git diff --check`

Note: full `cargo test -p codex-app-server` was attempted and stopped in
`message_processor::tracing_tests::turn_start_jsonrpc_span_parents_core_turn_spans`
with a stack overflow after unrelated tests had already passed.
This commit is contained in:
pakrym-oai
2026-04-26 15:10:35 -07:00
committed by GitHub
parent deaa307fb2
commit 2a020f1a0a
9 changed files with 270 additions and 525 deletions

View File

@@ -34,9 +34,9 @@ use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::sync::watch;
use crate::error_code::INTERNAL_ERROR_CODE;
use crate::error_code::INVALID_PARAMS_ERROR_CODE;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use crate::error_code::internal_error;
use crate::error_code::invalid_params;
use crate::error_code::invalid_request;
use crate::outgoing_message::ConnectionId;
use crate::outgoing_message::ConnectionRequestId;
use crate::outgoing_message::OutgoingMessageSender;
@@ -158,7 +158,7 @@ impl CommandExecManager {
} = params;
if process_id.is_none() && (tty || stream_stdin || stream_stdout_stderr) {
return Err(invalid_request(
"command/exec tty or streaming requires a client-supplied processId".to_string(),
"command/exec tty or streaming requires a client-supplied processId",
));
}
let process_id = process_id.map_or_else(
@@ -178,12 +178,12 @@ impl CommandExecManager {
if matches!(exec_request.sandbox, SandboxType::WindowsRestrictedToken) {
if tty || stream_stdin || stream_stdout_stderr {
return Err(invalid_request(
"streaming command/exec is not supported with windows sandbox".to_string(),
"streaming command/exec is not supported with windows sandbox",
));
}
if output_bytes_cap != Some(DEFAULT_OUTPUT_BYTES_CAP) {
return Err(invalid_request(
"custom outputBytesCap is not supported with windows sandbox".to_string(),
"custom outputBytesCap is not supported with windows sandbox",
));
}
if let InternalProcessId::Client(_) = &process_id {
@@ -249,7 +249,7 @@ impl CommandExecManager {
let sessions = Arc::clone(&self.sessions);
let (program, args) = command
.split_first()
.ok_or_else(|| invalid_request("command must not be empty".to_string()))?;
.ok_or_else(|| invalid_request("command must not be empty"))?;
{
let mut sessions = self.sessions.lock().await;
if sessions.contains_key(&process_key) {
@@ -312,7 +312,7 @@ impl CommandExecManager {
) -> Result<CommandExecWriteResponse, JSONRPCErrorError> {
if params.delta_base64.is_none() && !params.close_stdin {
return Err(invalid_params(
"command/exec/write requires deltaBase64 or closeStdin".to_string(),
"command/exec/write requires deltaBase64 or closeStdin",
));
}
@@ -421,7 +421,7 @@ impl CommandExecManager {
};
let CommandExecSession::Active { control_tx } = session else {
return Err(invalid_request(
"command/exec/write, command/exec/terminate, and command/exec/resize are not supported for windows sandbox processes".to_string(),
"command/exec/write, command/exec/terminate, and command/exec/resize are not supported for windows sandbox processes",
));
};
let (response_tx, response_rx) = oneshot::channel();
@@ -635,7 +635,7 @@ async fn handle_process_write(
) -> Result<(), JSONRPCErrorError> {
if !stream_stdin {
return Err(invalid_request(
"stdin streaming is not enabled for this command/exec".to_string(),
"stdin streaming is not enabled for this command/exec",
));
}
if !delta.is_empty() {
@@ -643,7 +643,7 @@ async fn handle_process_write(
.writer_sender()
.send(delta)
.await
.map_err(|_| invalid_request("stdin is already closed".to_string()))?;
.map_err(|_| invalid_request("stdin is already closed"))?;
}
if close_stdin {
session.close_stdin();
@@ -665,7 +665,7 @@ pub(crate) fn terminal_size_from_protocol(
) -> Result<TerminalSize, JSONRPCErrorError> {
if size.rows == 0 || size.cols == 0 {
return Err(invalid_params(
"command/exec size rows and cols must be greater than 0".to_string(),
"command/exec size rows and cols must be greater than 0",
));
}
Ok(TerminalSize {
@@ -681,34 +681,11 @@ fn command_no_longer_running_error(process_id: &InternalProcessId) -> JSONRPCErr
))
}
fn invalid_request(message: String) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message,
data: None,
}
}
fn invalid_params(message: String) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INVALID_PARAMS_ERROR_CODE,
message,
data: None,
}
}
fn internal_error(message: String) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message,
data: None,
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;