@@ -198,8 +198,8 @@ fn panic_wire() -> Vec<u8> {
198198#[ path = "jni_impl_support.rs" ]
199199mod support;
200200use support:: {
201- push_unless_header_failed , setup_full_stream , setup_full_stream_with_header , setup_stream ,
202- setup_stream_with_header , should_fire_fallback_header , throw_streaming_abort,
201+ PanicHeaderAction , panic_post_header_action , push_unless_header_failed , setup_full_stream ,
202+ setup_full_stream_with_header , setup_stream , setup_stream_with_header , throw_streaming_abort,
203203} ;
204204
205205/// `com.devfive.vespera.bridge.VesperaBridge.dispatchBytes(byte[]) -> byte[]`
@@ -641,8 +641,11 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchStr
641641 // the consumer once with a 500 header below so the documented
642642 // "header consumer invoked exactly once on every code path"
643643 // contract holds and the Java caller is not left hanging. A
644- // panic AFTER the header fired leaves Spring's response partially
645- // committed — unrecoverable, but the contract is already met.
644+ // panic AFTER the header fired truncates the body past a header the
645+ // host already committed; `panic_post_header_action` then throws
646+ // IOException to abort the response (symmetric with the body-error /
647+ // sink-stop abort on the `Ok` branch) instead of finishing cleanly
648+ // over a short body.
646649 let header_sent = Arc :: new ( AtomicBool :: new ( false ) ) ;
647650 let header_failed = Arc :: new ( AtomicBool :: new ( false ) ) ;
648651 let header_sent_cb = Arc :: clone ( & header_sent) ;
@@ -694,24 +697,29 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchStr
694697 }
695698 }
696699 Err ( _) => {
697- // See `should_fire_fallback_header`: a panic re-enters the
698- // header consumer ONLY when it was never invoked (neither
699- // succeeded nor threw), upholding the "invoked exactly once on
700- // every code path" contract.
701- if should_fire_fallback_header (
700+ // A panic unwound out of the dispatch future. The action
701+ // depends on whether the response header was already committed
702+ // (see `panic_post_header_action`).
703+ match panic_post_header_action (
702704 header_sent. load ( Ordering :: Relaxed ) ,
703705 header_failed. load ( Ordering :: Acquire ) ,
704706 ) {
705- let err = panic_wire ( ) ;
706- // On the JNI entry thread `header_consumer` is still a
707- // valid LOCAL ref, so deliver the mandatory fallback
708- // header through it directly. Promoting it to a
709- // `Global` here added an avoidable allocation AND a
710- // failure point: a failed `new_global_ref` (e.g. OOM)
711- // silently skipped the required single callback and
712- // hung the Java caller. `call_header_consumer_local`
713- // exists for exactly this cold on-thread fallback.
714- let _ = call_header_consumer_local ( env, & header_consumer, & err) ;
707+ // Header never reached the consumer: deliver the one-shot
708+ // 500 fallback through the still-valid LOCAL `header_consumer`
709+ // ref (no `Global` promotion to fail first and hang the
710+ // caller), upholding "invoked exactly once on every code path".
711+ PanicHeaderAction :: FireFallbackHeader => {
712+ let err = panic_wire ( ) ;
713+ let _ = call_header_consumer_local ( env, & header_consumer, & err) ;
714+ }
715+ // Header already committed (or its delivery threw): the body
716+ // is now truncated past a header the host already wrote, so
717+ // throw IOException to abort the response instead of finishing
718+ // cleanly over a short body — symmetric with the body-error /
719+ // sink-stop abort on the `Ok` branch above.
720+ PanicHeaderAction :: ThrowAbort => {
721+ throw_streaming_abort ( env, header_failed. load ( Ordering :: Acquire ) ) ;
722+ }
715723 }
716724 }
717725 }
@@ -851,24 +859,29 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchFul
851859 }
852860 }
853861 Err ( _) => {
854- // See `should_fire_fallback_header`: a panic re-enters the
855- // header consumer ONLY when it was never invoked (neither
856- // succeeded nor threw), upholding the "invoked exactly once on
857- // every code path" contract.
858- if should_fire_fallback_header (
862+ // A panic unwound out of the dispatch future. The action
863+ // depends on whether the response header was already committed
864+ // (see `panic_post_header_action`).
865+ match panic_post_header_action (
859866 header_sent. load ( Ordering :: Relaxed ) ,
860867 header_failed. load ( Ordering :: Acquire ) ,
861868 ) {
862- let err = panic_wire ( ) ;
863- // On the JNI entry thread `header_consumer` is still a
864- // valid LOCAL ref, so deliver the mandatory fallback
865- // header through it directly. Promoting it to a
866- // `Global` here added an avoidable allocation AND a
867- // failure point: a failed `new_global_ref` (e.g. OOM)
868- // silently skipped the required single callback and
869- // hung the Java caller. `call_header_consumer_local`
870- // exists for exactly this cold on-thread fallback.
871- let _ = call_header_consumer_local ( env, & header_consumer, & err) ;
869+ // Header never reached the consumer: deliver the one-shot
870+ // 500 fallback through the still-valid LOCAL `header_consumer`
871+ // ref (no `Global` promotion to fail first and hang the
872+ // caller), upholding "invoked exactly once on every code path".
873+ PanicHeaderAction :: FireFallbackHeader => {
874+ let err = panic_wire ( ) ;
875+ let _ = call_header_consumer_local ( env, & header_consumer, & err) ;
876+ }
877+ // Header already committed (or its delivery threw): the body
878+ // is now truncated past a header the host already wrote, so
879+ // throw IOException to abort the response instead of finishing
880+ // cleanly over a short body — symmetric with the body-error /
881+ // sink-stop abort on the `Ok` branch above.
882+ PanicHeaderAction :: ThrowAbort => {
883+ throw_streaming_abort ( env, header_failed. load ( Ordering :: Acquire ) ) ;
884+ }
872885 }
873886 }
874887 }
0 commit comments