@@ -17,9 +17,10 @@ use tracing::Instrument as _;
1717use uuid:: Uuid ;
1818
1919use crate :: DgwState ;
20+ use crate :: api:: heartbeat:: recording_storage_health;
2021use crate :: extract:: { JrecToken , RecordingDeleteScope , RecordingsReadScope } ;
2122use crate :: http:: { HttpError , HttpErrorBuilder } ;
22- use crate :: recording:: RecordingMessageSender ;
23+ use crate :: recording:: { PushOutcome , RecordingMessageSender } ;
2324use crate :: token:: { JrecTokenClaims , RecordingFileType , RecordingOperation } ;
2425
2526pub fn make_router < S > ( state : DgwState ) -> Router < S > {
@@ -65,6 +66,32 @@ async fn jrec_push(
6566 return Err ( HttpError :: forbidden ( ) . msg ( "expected push operation" ) ) ;
6667 }
6768
69+ let conf = conf_handle. get_conf ( ) ;
70+
71+ // Pre-flight: refuse the upgrade up-front when the recording storage cannot accept
72+ // a new stream. Returning HTTP 507 here gives the client a clear, actionable status
73+ // before the WebSocket is even established, so it can avoid a doomed session entirely.
74+ let storage = recording_storage_health ( conf. recording_path . as_std_path ( ) ) ;
75+ if !storage. recording_storage_is_writeable {
76+ warn ! ( client = %source_addr, %session_id, "Refusing JREC push: recording storage is not writable" ) ;
77+ return Err ( HttpErrorBuilder :: new ( StatusCode :: INSUFFICIENT_STORAGE ) . msg ( "recording storage is not writable" ) ) ;
78+ }
79+ if let ( Some ( min) , Some ( available) ) = (
80+ conf. min_recording_storage_free_space ,
81+ storage. recording_storage_available_space ,
82+ ) && available < min
83+ {
84+ warn ! (
85+ client = %source_addr,
86+ %session_id,
87+ available_bytes = available,
88+ min_bytes = min,
89+ "Refusing JREC push: free space below configured minimum"
90+ ) ;
91+ return Err ( HttpErrorBuilder :: new ( StatusCode :: INSUFFICIENT_STORAGE )
92+ . msg ( "recording storage below minimum free-space threshold" ) ) ;
93+ }
94+
6895 let response = ws. on_upgrade ( move |ws| {
6996 handle_jrec_push (
7097 ws,
@@ -110,14 +137,26 @@ async fn handle_jrec_push(
110137 . instrument ( info_span ! ( "jrec" , client = %source_addr) )
111138 . await ;
112139
113- if let Err ( error) = result {
114- close_handle. server_error ( "forwarding failure" . to_owned ( ) ) . await ;
115- error ! ( client = %source_addr, error = format!( "{error:#}" ) , "WebSocket-JREC failure" ) ;
116- } else {
117- close_handle. normal_close ( ) . await ;
140+ match result {
141+ Ok ( PushOutcome :: Done ) => close_handle. normal_close ( ) . await ,
142+ Ok ( PushOutcome :: StorageFull ) => {
143+ warn ! ( client = %source_addr, %session_id, "JREC push closed: storage full" ) ;
144+ close_handle. app_close ( STORAGE_FULL_CLOSE_CODE ) . await ;
145+ }
146+ Err ( error) => {
147+ close_handle. server_error ( "forwarding failure" . to_owned ( ) ) . await ;
148+ error ! ( client = %source_addr, error = format!( "{error:#}" ) , "WebSocket-JREC failure" ) ;
149+ }
118150 }
119151}
120152
153+ /// WebSocket close code sent on `/jrec/push/{id}` when the recording storage volume is full
154+ /// and the stream cannot continue.
155+ ///
156+ /// Codes in 4000-4999 are reserved for private application use per
157+ /// <https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code>.
158+ const STORAGE_FULL_CLOSE_CODE : u16 = 4010 ;
159+
121160/// Deletes a recording stored on this instance
122161#[ cfg_attr( feature = "openapi" , utoipa:: path(
123162 delete,
0 commit comments