Skip to content

Commit d047c33

Browse files
fix(remote-control): avoid server token refresh retry storms (#30201)
## Why Remote-control websocket reconnects and pairing requests proactively refresh their server token. When `/server/refresh` returns a transient error such as `502`, the still-valid token was discarded as a usable connection path, causing reconnect failures and repeated refresh attempts that could amplify an upstream incident. ## What Changed - Start proactive refresh five minutes before token expiry and distinguish it from a required refresh for missing or expired tokens. - Continue websocket and pairing operations with the existing valid token after `429`, `5xx`, or timeout failures. - Share an in-memory `next_refresh_at` throttle across websocket and pairing callers, honoring both `Retry-After` formats and otherwise using a jittered 24–36 second delay. - Keep required refreshes strict, preserve `404` enrollment replacement, and clear token/throttle state for `401` and `403` auth recovery. - Preserve refresh response metadata internally and add focused wire-level and integration coverage. ## Verification Added behavioral coverage proving that: - a valid near-expiry token still completes websocket and pairing requests after transient refresh failures; - `Retry-After` suppresses a subsequent refresh across websocket and pairing callers; - request and response-body timeouts are classified as transient; - an expired token, including one that expires during refresh, cannot proceed to websocket connection; - auth failures clear the attempted token without overwriting a concurrently rotated token.
1 parent a107b84 commit d047c33

11 files changed

Lines changed: 1634 additions & 345 deletions

File tree

codex-rs/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ glob = "0.3"
318318
globset = "0.4"
319319
hmac = "0.12.1"
320320
http = "1.3.1"
321+
httpdate = "1.0.3"
321322
iana-time-zone = "0.1.64"
322323
icu_decimal = "2.1"
323324
icu_locale_core = "2.1"

codex-rs/app-server-transport/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ constant_time_eq = { workspace = true }
3535
futures = { workspace = true }
3636
gethostname = { workspace = true }
3737
hmac = { workspace = true }
38+
httpdate = { workspace = true }
3839
jsonwebtoken = { workspace = true }
3940
owo-colors = { workspace = true, features = ["supports-colors"] }
41+
rand = { workspace = true }
4042
serde = { workspace = true, features = ["derive"] }
4143
serde_json = { workspace = true }
4244
sha2 = { workspace = true }

codex-rs/app-server-transport/src/transport/remote_control/enroll.rs

Lines changed: 50 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
use super::auth::RemoteControlConnectionAuth;
21
use super::pairing_unavailable_error;
3-
use super::protocol::EnrollRemoteServerRequest;
4-
use super::protocol::EnrollRemoteServerResponse;
5-
use super::protocol::RefreshRemoteServerRequest;
62
use super::protocol::RemoteControlPairingStatusRequest;
73
use super::protocol::RemoteControlPairingStatusResponse as BackendRemoteControlPairingStatusResponse;
84
use super::protocol::RemoteControlTarget;
@@ -14,24 +10,20 @@ use codex_app_server_protocol::RemoteControlPairingStatusResponse;
1410
use codex_login::default_client::build_reqwest_client;
1511
use codex_state::RemoteControlEnrollmentRecord;
1612
use codex_state::StateRuntime;
17-
use serde::Serialize;
18-
use serde::de::DeserializeOwned;
1913
use std::io;
2014
use std::io::ErrorKind;
2115
use time::OffsetDateTime;
2216
use time::format_description::well_known::Rfc3339;
2317
use tracing::info;
2418
use tracing::warn;
2519

26-
const REMOTE_CONTROL_ENROLL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
2720
const REMOTE_CONTROL_PAIRING_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
2821
const REMOTE_CONTROL_RESPONSE_BODY_MAX_BYTES: usize = 4096;
29-
const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS: i64 = 30;
22+
const REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS: i64 = 5 * 60;
3023

3124
const REQUEST_ID_HEADER: &str = "x-request-id";
3225
const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id";
3326
const CF_RAY_HEADER: &str = "cf-ray";
34-
pub(super) const REMOTE_CONTROL_INSTALLATION_ID_HEADER: &str = "x-codex-installation-id";
3527

3628
#[derive(Debug, Clone, PartialEq, Eq)]
3729
pub(super) struct RemoteControlEnrollment {
@@ -42,14 +34,24 @@ pub(super) struct RemoteControlEnrollment {
4234
pub(super) server_name: String,
4335
pub(super) remote_control_token: Option<String>,
4436
pub(super) expires_at: Option<OffsetDateTime>,
37+
pub(super) next_refresh_at: Option<OffsetDateTime>,
38+
}
39+
40+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41+
pub(super) enum RemoteControlServerTokenRefreshRequirement {
42+
Required,
43+
Proactive,
44+
NotNeeded,
4545
}
4646

4747
impl RemoteControlEnrollment {
4848
pub(super) async fn start_pairing(
4949
&self,
5050
request: StartRemoteControlPairingRequest,
5151
) -> io::Result<RemoteControlPairingStartResponse> {
52-
if self.should_refresh_server_token() {
52+
if self.server_token_refresh_requirement()
53+
== RemoteControlServerTokenRefreshRequirement::Required
54+
{
5355
return Err(pairing_unavailable_error());
5456
}
5557
let remote_control_token = self
@@ -142,7 +144,9 @@ impl RemoteControlEnrollment {
142144
&self,
143145
request: RemoteControlPairingStatusRequest,
144146
) -> io::Result<RemoteControlPairingStatusResponse> {
145-
if self.should_refresh_server_token() {
147+
if self.server_token_refresh_requirement()
148+
== RemoteControlServerTokenRefreshRequirement::Required
149+
{
146150
return Err(pairing_unavailable_error());
147151
}
148152
let remote_control_token = self
@@ -201,13 +205,35 @@ impl RemoteControlEnrollment {
201205
})
202206
}
203207

208+
pub(super) fn server_token_refresh_requirement(
209+
&self,
210+
) -> RemoteControlServerTokenRefreshRequirement {
211+
self.server_token_refresh_requirement_at(OffsetDateTime::now_utc())
212+
}
213+
204214
pub(super) fn should_refresh_server_token(&self) -> bool {
205-
self.remote_control_token.is_none()
206-
|| self.expires_at.is_none_or(|expires_at| {
207-
expires_at.unix_timestamp()
208-
<= OffsetDateTime::now_utc().unix_timestamp()
209-
+ REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS
210-
})
215+
self.server_token_refresh_requirement()
216+
!= RemoteControlServerTokenRefreshRequirement::NotNeeded
217+
}
218+
219+
pub(super) fn server_token_refresh_requirement_at(
220+
&self,
221+
now: OffsetDateTime,
222+
) -> RemoteControlServerTokenRefreshRequirement {
223+
let Some(expires_at) = self.remote_control_token.as_ref().and(self.expires_at) else {
224+
return RemoteControlServerTokenRefreshRequirement::Required;
225+
};
226+
if expires_at <= now {
227+
return RemoteControlServerTokenRefreshRequirement::Required;
228+
}
229+
if expires_at > now + time::Duration::seconds(REMOTE_CONTROL_SERVER_TOKEN_REFRESH_SKEW_SECS)
230+
|| self
231+
.next_refresh_at
232+
.is_some_and(|next_refresh_at| next_refresh_at > now)
233+
{
234+
return RemoteControlServerTokenRefreshRequirement::NotNeeded;
235+
}
236+
RemoteControlServerTokenRefreshRequirement::Proactive
211237
}
212238

213239
pub(super) fn clear_server_token(&mut self) {
@@ -267,6 +293,7 @@ pub(super) async fn load_persisted_remote_control_enrollment(
267293
server_name: enrollment.server_name,
268294
remote_control_token: None,
269295
expires_at: None,
296+
next_refresh_at: None,
270297
}))
271298
}
272299
None => {
@@ -398,164 +425,12 @@ pub(crate) fn format_headers(headers: &HeaderMap) -> String {
398425
format!("request-id: {request_id_str}, cf-ray: {cf_ray_str}")
399426
}
400427

401-
pub(super) async fn enroll_remote_control_server(
402-
remote_control_target: &RemoteControlTarget,
403-
auth: &RemoteControlConnectionAuth,
404-
installation_id: &str,
405-
server_name: &str,
406-
) -> io::Result<RemoteControlEnrollment> {
407-
let enroll_url = &remote_control_target.enroll_url;
408-
let request = EnrollRemoteServerRequest {
409-
name: server_name.to_string(),
410-
os: std::env::consts::OS,
411-
arch: std::env::consts::ARCH,
412-
app_server_version: env!("CARGO_PKG_VERSION"),
413-
installation_id: installation_id.to_string(),
414-
};
415-
let enrollment_response = send_remote_control_server_request::<_, EnrollRemoteServerResponse>(
416-
enroll_url,
417-
auth,
418-
installation_id,
419-
&request,
420-
"enroll",
421-
"server enrollment",
422-
)
423-
.await?;
424-
let mut enrollment = RemoteControlEnrollment {
425-
remote_control_target: remote_control_target.clone(),
426-
account_id: auth.account_id.clone(),
427-
environment_id: enrollment_response.environment_id,
428-
server_id: enrollment_response.server_id,
429-
server_name: server_name.to_string(),
430-
remote_control_token: None,
431-
expires_at: None,
432-
};
433-
update_remote_control_server_token(
434-
&mut enrollment,
435-
enroll_url,
436-
enrollment_response.remote_control_token,
437-
enrollment_response.expires_at,
438-
)?;
439-
Ok(enrollment)
440-
}
441-
442-
pub(super) async fn refresh_remote_control_server(
443-
auth: &RemoteControlConnectionAuth,
444-
installation_id: &str,
445-
enrollment: &mut RemoteControlEnrollment,
446-
) -> io::Result<()> {
447-
let refresh_url = enrollment.remote_control_target.refresh_url.clone();
448-
let request = RefreshRemoteServerRequest {
449-
server_id: enrollment.server_id.clone(),
450-
installation_id: installation_id.to_string(),
451-
};
452-
let refreshed = send_remote_control_server_request::<_, EnrollRemoteServerResponse>(
453-
&refresh_url,
454-
auth,
455-
installation_id,
456-
&request,
457-
"refresh",
458-
"server refresh",
459-
)
460-
.await?;
461-
if refreshed.server_id != enrollment.server_id
462-
|| refreshed.environment_id != enrollment.environment_id
463-
{
464-
return Err(io::Error::other(format!(
465-
"remote control server refresh returned mismatched enrollment: expected server_id={}, environment_id={}; got server_id={}, environment_id={}",
466-
enrollment.server_id,
467-
enrollment.environment_id,
468-
refreshed.server_id,
469-
refreshed.environment_id
470-
)));
471-
}
472-
473-
update_remote_control_server_token(
474-
enrollment,
475-
&refresh_url,
476-
refreshed.remote_control_token,
477-
refreshed.expires_at,
478-
)
479-
}
480-
481-
async fn send_remote_control_server_request<Request, Response>(
482-
url: &str,
483-
auth: &RemoteControlConnectionAuth,
484-
installation_id: &str,
485-
request: &Request,
486-
action: &str,
487-
response_kind: &str,
488-
) -> io::Result<Response>
489-
where
490-
Request: Serialize,
491-
Response: DeserializeOwned,
492-
{
493-
let client = build_reqwest_client();
494-
let auth_headers = auth.request_headers()?;
495-
let response = client
496-
.post(url)
497-
.timeout(REMOTE_CONTROL_ENROLL_TIMEOUT)
498-
.headers(auth_headers)
499-
.header(REMOTE_CONTROL_INSTALLATION_ID_HEADER, installation_id)
500-
.json(request)
501-
.send()
502-
.await
503-
.map_err(|err| {
504-
io::Error::other(format!(
505-
"failed to {action} remote control server at `{url}`: {err}"
506-
))
507-
})?;
508-
let headers = response.headers().clone();
509-
let status = response.status();
510-
let body = response.bytes().await.map_err(|err| {
511-
io::Error::other(format!(
512-
"failed to read remote control {response_kind} response from `{url}`: {err}"
513-
))
514-
})?;
515-
let body_preview = preview_remote_control_response_body(&body);
516-
if !status.is_success() {
517-
let headers_str = format_headers(&headers);
518-
let error_kind = match status.as_u16() {
519-
401 | 403 => ErrorKind::PermissionDenied,
520-
404 => ErrorKind::NotFound,
521-
_ => ErrorKind::Other,
522-
};
523-
return Err(io::Error::new(
524-
error_kind,
525-
format!(
526-
"remote control {response_kind} failed at `{url}`: HTTP {status}, {headers_str}, body: {body_preview}"
527-
),
528-
));
529-
}
530-
531-
serde_json::from_slice::<Response>(&body).map_err(|err| {
532-
let headers_str = format_headers(&headers);
533-
io::Error::other(format!(
534-
"failed to parse remote control {response_kind} response from `{url}`: HTTP {status}, {headers_str}, body: {body_preview}, decode error: {err}"
535-
))
536-
})
537-
}
538-
539-
fn update_remote_control_server_token(
540-
enrollment: &mut RemoteControlEnrollment,
541-
url: &str,
542-
token: String,
543-
expires_at: String,
544-
) -> io::Result<()> {
545-
let expires_at = OffsetDateTime::parse(&expires_at, &Rfc3339).map_err(|err| {
546-
io::Error::other(format!(
547-
"failed to parse remote control server token expiry from `{url}`: {err}"
548-
))
549-
})?;
550-
enrollment.remote_control_token = Some(token);
551-
enrollment.expires_at = Some(expires_at);
552-
Ok(())
553-
}
554-
555428
#[cfg(test)]
556429
mod tests {
557430
use super::*;
431+
use crate::transport::remote_control::auth::RemoteControlConnectionAuth;
558432
use crate::transport::remote_control::protocol::normalize_remote_control_url;
433+
use crate::transport::remote_control::server_api::enroll_remote_control_server;
559434
use codex_state::StateRuntime;
560435
use pretty_assertions::assert_eq;
561436
use serde_json::json;
@@ -575,28 +450,6 @@ mod tests {
575450
.expect("state runtime should initialize")
576451
}
577452

578-
#[test]
579-
fn remote_control_enrollment_refreshes_server_token_before_expiry() {
580-
let expires_soon = RemoteControlEnrollment {
581-
remote_control_target: normalize_remote_control_url("http://localhost/backend-api/")
582-
.expect("target should normalize"),
583-
account_id: "account-a".to_string(),
584-
environment_id: "env_first".to_string(),
585-
server_id: "srv_e_first".to_string(),
586-
server_name: "first-server".to_string(),
587-
remote_control_token: Some("expires-soon".to_string()),
588-
expires_at: Some(OffsetDateTime::now_utc() + time::Duration::seconds(29)),
589-
};
590-
let expires_later = RemoteControlEnrollment {
591-
expires_at: Some(OffsetDateTime::now_utc() + time::Duration::seconds(31)),
592-
remote_control_token: Some("expires-later".to_string()),
593-
..expires_soon.clone()
594-
};
595-
596-
assert!(expires_soon.should_refresh_server_token());
597-
assert!(!expires_later.should_refresh_server_token());
598-
}
599-
600453
#[test]
601454
fn preview_remote_control_response_body_redacts_server_token() {
602455
assert_eq!(
@@ -630,6 +483,7 @@ mod tests {
630483
server_name: "first-server".to_string(),
631484
remote_control_token: None,
632485
expires_at: None,
486+
next_refresh_at: None,
633487
};
634488
let second_enrollment = RemoteControlEnrollment {
635489
remote_control_target: second_target.clone(),
@@ -639,6 +493,7 @@ mod tests {
639493
server_name: "second-server".to_string(),
640494
remote_control_token: None,
641495
expires_at: None,
496+
next_refresh_at: None,
642497
};
643498

644499
update_persisted_remote_control_enrollment(
@@ -714,6 +569,7 @@ mod tests {
714569
server_name: "first-server".to_string(),
715570
remote_control_token: None,
716571
expires_at: None,
572+
next_refresh_at: None,
717573
};
718574
let second_enrollment = RemoteControlEnrollment {
719575
remote_control_target: second_target.clone(),
@@ -723,6 +579,7 @@ mod tests {
723579
server_name: "second-server".to_string(),
724580
remote_control_token: None,
725581
expires_at: None,
582+
next_refresh_at: None,
726583
};
727584

728585
update_persisted_remote_control_enrollment(

0 commit comments

Comments
 (0)