Skip to content

Commit c2ec477

Browse files
authored
[core] add optional status_code to error events (#6865)
We want to better uncover error status code for clients. Add an optional status_code to error events (thread error, error, stream error) so app server could uncover the status code from the client side later. in event log: ``` < { < "method": "codex/event/stream_error", < "params": { < "conversationId": "019a9a32-f576-7292-9711-8e57e8063536", < "id": "0", < "msg": { < "message": "Reconnecting... 5/5", < "status_code": 401, < "type": "stream_error" < } < } < } < { < "method": "codex/event/error", < "params": { < "conversationId": "019a9a32-f576-7292-9711-8e57e8063536", < "id": "0", < "msg": { < "message": "exceeded retry limit, last status: 401 Unauthorized, request id: 9a0cb03a485067f7-SJC", < "status_code": 401, < "type": "error" < } < } < } ```
1 parent 20982d5 commit c2ec477

File tree

10 files changed

+101
-24
lines changed

10 files changed

+101
-24
lines changed

codex-rs/core/src/codex.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ use crate::context_manager::ContextManager;
6666
use crate::environment_context::EnvironmentContext;
6767
use crate::error::CodexErr;
6868
use crate::error::Result as CodexResult;
69+
use crate::error::http_status_code_value;
6970
#[cfg(test)]
7071
use crate::exec::StreamOutput;
7172
use crate::mcp::auth::compute_auth_statuses;
@@ -79,7 +80,6 @@ use crate::protocol::ApplyPatchApprovalRequestEvent;
7980
use crate::protocol::AskForApproval;
8081
use crate::protocol::BackgroundEventEvent;
8182
use crate::protocol::DeprecationNoticeEvent;
82-
use crate::protocol::ErrorEvent;
8383
use crate::protocol::Event;
8484
use crate::protocol::EventMsg;
8585
use crate::protocol::ExecApprovalRequestEvent;
@@ -133,6 +133,7 @@ use codex_protocol::user_input::UserInput;
133133
use codex_utils_readiness::Readiness;
134134
use codex_utils_readiness::ReadinessFlag;
135135
use codex_utils_tokenizer::warm_model_cache;
136+
use reqwest::StatusCode;
136137

137138
/// The high-level interface to the Codex system.
138139
/// It operates as a queue pair where you send submissions and receive events.
@@ -1186,9 +1187,11 @@ impl Session {
11861187
&self,
11871188
turn_context: &TurnContext,
11881189
message: impl Into<String>,
1190+
http_status_code: Option<StatusCode>,
11891191
) {
11901192
let event = EventMsg::StreamError(StreamErrorEvent {
11911193
message: message.into(),
1194+
http_status_code: http_status_code_value(http_status_code),
11921195
});
11931196
self.send_event(turn_context, event).await;
11941197
}
@@ -1680,6 +1683,7 @@ mod handlers {
16801683
id: sub_id.clone(),
16811684
msg: EventMsg::Error(ErrorEvent {
16821685
message: "Failed to shutdown rollout recorder".to_string(),
1686+
http_status_code: None,
16831687
}),
16841688
};
16851689
sess.send_event_raw(event).await;
@@ -1933,10 +1937,8 @@ pub(crate) async fn run_task(
19331937
}
19341938
Err(e) => {
19351939
info!("Turn error: {e:#}");
1936-
let event = EventMsg::Error(ErrorEvent {
1937-
message: e.to_string(),
1938-
});
1939-
sess.send_event(&turn_context, event).await;
1940+
sess.send_event(&turn_context, EventMsg::Error(e.to_error_event(None)))
1941+
.await;
19401942
// let the user continue the conversation
19411943
break;
19421944
}
@@ -2060,6 +2062,7 @@ async fn run_turn(
20602062
sess.notify_stream_error(
20612063
&turn_context,
20622064
format!("Reconnecting... {retries}/{max_retries}"),
2065+
e.http_status_code(),
20632066
)
20642067
.await;
20652068

codex-rs/core/src/compact.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use crate::error::Result as CodexResult;
1010
use crate::features::Feature;
1111
use crate::protocol::AgentMessageEvent;
1212
use crate::protocol::CompactedItem;
13-
use crate::protocol::ErrorEvent;
1413
use crate::protocol::EventMsg;
1514
use crate::protocol::TaskStartedEvent;
1615
use crate::protocol::TurnContextItem;
@@ -128,10 +127,8 @@ async fn run_compact_task_inner(
128127
continue;
129128
}
130129
sess.set_total_tokens_full(turn_context.as_ref()).await;
131-
let event = EventMsg::Error(ErrorEvent {
132-
message: e.to_string(),
133-
});
134-
sess.send_event(&turn_context, event).await;
130+
sess.send_event(&turn_context, EventMsg::Error(e.to_error_event(None)))
131+
.await;
135132
return;
136133
}
137134
Err(e) => {
@@ -141,15 +138,14 @@ async fn run_compact_task_inner(
141138
sess.notify_stream_error(
142139
turn_context.as_ref(),
143140
format!("Reconnecting... {retries}/{max_retries}"),
141+
e.http_status_code(),
144142
)
145143
.await;
146144
tokio::time::sleep(delay).await;
147145
continue;
148146
} else {
149-
let event = EventMsg::Error(ErrorEvent {
150-
message: e.to_string(),
151-
});
152-
sess.send_event(&turn_context, event).await;
147+
sess.send_event(&turn_context, EventMsg::Error(e.to_error_event(None)))
148+
.await;
153149
return;
154150
}
155151
}

codex-rs/core/src/compact_remote.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use crate::codex::TurnContext;
66
use crate::error::Result as CodexResult;
77
use crate::protocol::AgentMessageEvent;
88
use crate::protocol::CompactedItem;
9-
use crate::protocol::ErrorEvent;
109
use crate::protocol::EventMsg;
1110
use crate::protocol::RolloutItem;
1211
use crate::protocol::TaskStartedEvent;
@@ -30,10 +29,8 @@ pub(crate) async fn run_remote_compact_task(sess: Arc<Session>, turn_context: Ar
3029

3130
async fn run_remote_compact_task_inner(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) {
3231
if let Err(err) = run_remote_compact_task_inner_impl(sess, turn_context).await {
33-
let event = EventMsg::Error(ErrorEvent {
34-
message: format!("Error running remote compact task: {err}"),
35-
});
36-
sess.send_event(turn_context, event).await;
32+
let event = err.to_error_event(Some("Error running remote compact task".to_string()));
33+
sess.send_event(turn_context, EventMsg::Error(event)).await;
3734
}
3835
}
3936

codex-rs/core/src/error.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use chrono::Local;
1010
use chrono::Utc;
1111
use codex_async_utils::CancelErr;
1212
use codex_protocol::ConversationId;
13+
use codex_protocol::protocol::ErrorEvent;
1314
use codex_protocol::protocol::RateLimitSnapshot;
1415
use reqwest::StatusCode;
1516
use serde_json;
@@ -430,6 +431,37 @@ impl CodexErr {
430431
pub fn downcast_ref<T: std::any::Any>(&self) -> Option<&T> {
431432
(self as &dyn std::any::Any).downcast_ref::<T>()
432433
}
434+
435+
pub fn http_status_code(&self) -> Option<StatusCode> {
436+
match self {
437+
CodexErr::UnexpectedStatus(err) => Some(err.status),
438+
CodexErr::RetryLimit(err) => Some(err.status),
439+
CodexErr::UsageLimitReached(_) | CodexErr::UsageNotIncluded => {
440+
Some(StatusCode::TOO_MANY_REQUESTS)
441+
}
442+
CodexErr::InternalServerError => Some(StatusCode::INTERNAL_SERVER_ERROR),
443+
CodexErr::ResponseStreamFailed(err) => err.source.status(),
444+
CodexErr::ConnectionFailed(err) => err.source.status(),
445+
_ => None,
446+
}
447+
}
448+
449+
pub fn to_error_event(&self, message_prefix: Option<String>) -> ErrorEvent {
450+
let error_message = self.to_string();
451+
let message: String = match message_prefix {
452+
Some(prefix) => format!("{prefix}: {error_message}"),
453+
None => error_message,
454+
};
455+
456+
ErrorEvent {
457+
message,
458+
http_status_code: http_status_code_value(self.http_status_code()),
459+
}
460+
}
461+
}
462+
463+
pub fn http_status_code_value(http_status_code: Option<StatusCode>) -> Option<u16> {
464+
http_status_code.as_ref().map(StatusCode::as_u16)
433465
}
434466

435467
pub fn get_error_message_ui(e: &CodexErr) -> String {
@@ -775,4 +807,43 @@ mod tests {
775807
assert_eq!(err.to_string(), expected);
776808
});
777809
}
810+
811+
#[test]
812+
fn error_event_includes_http_status_code_when_available() {
813+
let err = CodexErr::UnexpectedStatus(UnexpectedResponseError {
814+
status: StatusCode::BAD_REQUEST,
815+
body: "oops".to_string(),
816+
request_id: Some("req-1".to_string()),
817+
});
818+
let event = err.to_error_event(None);
819+
820+
assert_eq!(
821+
event.message,
822+
"unexpected status 400 Bad Request: oops, request id: req-1"
823+
);
824+
assert_eq!(
825+
event.http_status_code,
826+
Some(StatusCode::BAD_REQUEST.as_u16())
827+
);
828+
}
829+
830+
#[test]
831+
fn error_event_omits_http_status_code_when_unknown() {
832+
let event = CodexErr::Fatal("boom".to_string()).to_error_event(None);
833+
834+
assert_eq!(event.message, "Fatal error: boom");
835+
assert_eq!(event.http_status_code, None);
836+
}
837+
838+
#[test]
839+
fn error_event_applies_message_wrapper() {
840+
let event = CodexErr::Fatal("boom".to_string())
841+
.to_error_event(Some("Error running remote compact task".to_string()));
842+
843+
assert_eq!(
844+
event.message,
845+
"Error running remote compact task: Fatal error: boom"
846+
);
847+
assert_eq!(event.http_status_code, None);
848+
}
778849
}

codex-rs/docs/protocol_v1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
7272
- `EventMsg::AgentMessage` – Messages from the `Model`
7373
- `EventMsg::ExecApprovalRequest` – Request approval from user to execute a command
7474
- `EventMsg::TaskComplete` – A task completed successfully
75-
- `EventMsg::Error` – A task stopped with an error
75+
- `EventMsg::Error` – A task stopped with an error (includes an optional `http_status_code` when available)
7676
- `EventMsg::Warning` – A non-fatal warning that the client should surface to the user
7777
- `EventMsg::TurnComplete` – Contains a `response_id` bookmark for last `response_id` executed by the task. This can be used to continue the task at a later point in time, perhaps with additional user input.
7878

codex-rs/exec/src/event_processor_with_human_output.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
161161
fn process_event(&mut self, event: Event) -> CodexStatus {
162162
let Event { id: _, msg } = event;
163163
match msg {
164-
EventMsg::Error(ErrorEvent { message }) => {
164+
EventMsg::Error(ErrorEvent { message, .. }) => {
165165
let prefix = "ERROR:".style(self.red);
166166
ts_msg!(self, "{prefix} {message}");
167167
}
@@ -221,7 +221,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
221221
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
222222
ts_msg!(self, "{}", message.style(self.dimmed));
223223
}
224-
EventMsg::StreamError(StreamErrorEvent { message }) => {
224+
EventMsg::StreamError(StreamErrorEvent { message, .. }) => {
225225
ts_msg!(self, "{}", message.style(self.dimmed));
226226
}
227227
EventMsg::TaskStarted(_) => {

codex-rs/exec/tests/event_processor_with_json_output.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ fn error_event_produces_error() {
539539
"e1",
540540
EventMsg::Error(codex_core::protocol::ErrorEvent {
541541
message: "boom".to_string(),
542+
http_status_code: Some(500),
542543
}),
543544
));
544545
assert_eq!(
@@ -578,6 +579,7 @@ fn stream_error_event_produces_error() {
578579
"e1",
579580
EventMsg::StreamError(codex_core::protocol::StreamErrorEvent {
580581
message: "retrying".to_string(),
582+
http_status_code: Some(500),
581583
}),
582584
));
583585
assert_eq!(
@@ -596,6 +598,7 @@ fn error_followed_by_task_complete_produces_turn_failed() {
596598
"e1",
597599
EventMsg::Error(ErrorEvent {
598600
message: "boom".to_string(),
601+
http_status_code: Some(500),
599602
}),
600603
);
601604
assert_eq!(

codex-rs/protocol/src/protocol.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,8 @@ pub struct ExitedReviewModeEvent {
686686
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
687687
pub struct ErrorEvent {
688688
pub message: String,
689+
#[serde(default)]
690+
pub http_status_code: Option<u16>,
689691
}
690692

691693
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
@@ -1363,6 +1365,8 @@ pub struct UndoCompletedEvent {
13631365
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
13641366
pub struct StreamErrorEvent {
13651367
pub message: String,
1368+
#[serde(default)]
1369+
pub http_status_code: Option<u16>,
13661370
}
13671371

13681372
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]

codex-rs/tui/src/chatwidget.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,7 +1627,7 @@ impl ChatWidget {
16271627
self.on_rate_limit_snapshot(ev.rate_limits);
16281628
}
16291629
EventMsg::Warning(WarningEvent { message }) => self.on_warning(message),
1630-
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
1630+
EventMsg::Error(ErrorEvent { message, .. }) => self.on_error(message),
16311631
EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev),
16321632
EventMsg::McpStartupComplete(ev) => self.on_mcp_startup_complete(ev),
16331633
EventMsg::TurnAborted(ev) => match ev.reason {
@@ -1670,7 +1670,9 @@ impl ChatWidget {
16701670
}
16711671
EventMsg::UndoStarted(ev) => self.on_undo_started(ev),
16721672
EventMsg::UndoCompleted(ev) => self.on_undo_completed(ev),
1673-
EventMsg::StreamError(StreamErrorEvent { message }) => self.on_stream_error(message),
1673+
EventMsg::StreamError(StreamErrorEvent { message, .. }) => {
1674+
self.on_stream_error(message)
1675+
}
16741676
EventMsg::UserMessage(ev) => {
16751677
if from_replay {
16761678
self.on_user_message_event(ev);

codex-rs/tui/src/chatwidget/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2502,6 +2502,7 @@ fn stream_error_updates_status_indicator() {
25022502
id: "sub-1".into(),
25032503
msg: EventMsg::StreamError(StreamErrorEvent {
25042504
message: msg.to_string(),
2505+
http_status_code: None,
25052506
}),
25062507
});
25072508

0 commit comments

Comments
 (0)