Skip to content

Commit 928e934

Browse files
author
Nils Bars
committed
Add web request timeout and unify user-facing failure messages
Apply a 30s per-request timeout to the web API client so a wedged web backend cannot keep an SSH session hanging indefinitely. When a container connection fails after auth, send a UUID-tagged message on stderr that recommends retrying first and falling back to contacting course staff. The same wording is now used for the auth-time backend-failure path and for the shell, exec, and subsystem request paths, so support can correlate the error code the student reports against a single log site.
1 parent bd6be8d commit 928e934

2 files changed

Lines changed: 45 additions & 18 deletions

File tree

ssh-reverse-proxy/src/api.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ pub struct ApiClient {
8585
signing_key: Vec<u8>,
8686
}
8787

88+
/// Per-request timeout for web API calls. The web side can stall under load
89+
/// (e.g. waiting for Docker to start a container during provision); 30s gives
90+
/// it room while still bounding the SSH session if the web is wedged.
91+
const WEB_REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
92+
8893
/// Response from /api/getkeys
8994
#[derive(Debug, Deserialize)]
9095
pub struct GetKeysResponse {
@@ -135,8 +140,12 @@ struct ProvisionRequest {
135140
impl ApiClient {
136141
/// Create a new API client.
137142
pub fn new(base_url: String, signing_key: Vec<u8>) -> Self {
143+
let client = Client::builder()
144+
.timeout(WEB_REQUEST_TIMEOUT)
145+
.build()
146+
.expect("failed to build reqwest client");
138147
Self {
139-
client: Client::new(),
148+
client,
140149
base_url,
141150
signing_key,
142151
}

ssh-reverse-proxy/src/server.rs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,45 @@ impl SshConnection {
216216
);
217217
self.state.startup_error = Some(format!(
218218
"The {} environment could not be started.\r\n\
219-
Please contact the course staff and include error code {}.\r\n",
219+
This may be a temporary issue — please try connecting again in a moment.\r\n\
220+
If the problem persists, contact the course staff and include error code {}.\r\n",
220221
self.state.exercise_name, error_id
221222
));
222223
None
223224
}
224225
}
225226
}
226227

228+
/// Deliver a UUID-tagged failure message to the client when a runtime
229+
/// container connection fails (after auth has already succeeded). Logs
230+
/// the underlying error server-side, writes a retry-recommending message
231+
/// to the channel's stderr, sets a non-zero exit status, and closes the
232+
/// channel. Always returns `()` for the caller to bubble up.
233+
fn emit_container_failure(
234+
&self,
235+
channel_id: ChannelId,
236+
session: &mut Session,
237+
stage: &str,
238+
err: &anyhow::Error,
239+
) -> Result<(), russh::Error> {
240+
let error_id = Uuid::new_v4();
241+
error!(
242+
"{} container connection failed [error_id={}]: {:#}",
243+
stage, error_id, err
244+
);
245+
let msg = format!(
246+
"Failed to connect to your {} environment.\r\n\
247+
This may be a temporary issue — please try connecting again in a moment.\r\n\
248+
If the problem persists, contact the course staff and include error code {}.\r\n",
249+
self.state.exercise_name, error_id
250+
);
251+
session.channel_success(channel_id)?;
252+
session.extended_data(channel_id, 1, CryptoVec::from_slice(msg.as_bytes()))?;
253+
session.exit_status_request(channel_id, 1)?;
254+
session.close(channel_id)?;
255+
Ok(())
256+
}
257+
227258
/// If a startup error was recorded during auth, deliver it to the client
228259
/// on the given channel and close the channel with exit status 1.
229260
/// Returns `true` if an error was delivered.
@@ -568,14 +599,7 @@ impl server::Handler for SshConnection {
568599
{
569600
Ok(f) => f,
570601
Err(e) => {
571-
let error_id = Uuid::new_v4();
572-
error!("Failed to connect to container [error_id={}]: {}", error_id, e);
573-
let msg = format!(
574-
"Error: Internal error (id: {}). If this issue persists, please contact a supervisor and provide the error id.\r\n",
575-
error_id
576-
);
577-
session.data(channel_id, CryptoVec::from_slice(msg.as_bytes()))?;
578-
session.close(channel_id)?;
602+
self.emit_container_failure(channel_id, session, "shell", &e)?;
579603
return Ok(());
580604
}
581605
};
@@ -668,10 +692,7 @@ impl server::Handler for SshConnection {
668692
{
669693
Ok(f) => f,
670694
Err(e) => {
671-
let error_id = Uuid::new_v4();
672-
error!("Failed to connect to container [error_id={}]: {}", error_id, e);
673-
session.channel_failure(channel_id)?;
674-
session.close(channel_id)?;
695+
self.emit_container_failure(channel_id, session, "exec", &e)?;
675696
return Ok(());
676697
}
677698
};
@@ -743,10 +764,7 @@ impl server::Handler for SshConnection {
743764
{
744765
Ok(f) => f,
745766
Err(e) => {
746-
let error_id = Uuid::new_v4();
747-
error!("Failed to connect to container [error_id={}]: {}", error_id, e);
748-
session.channel_failure(channel_id)?;
749-
session.close(channel_id)?;
767+
self.emit_container_failure(channel_id, session, "subsystem", &e)?;
750768
return Ok(());
751769
}
752770
};

0 commit comments

Comments
 (0)