@@ -3627,40 +3627,58 @@ fn url_host_is_youtube_video_endpoint(url: &str) -> bool {
36273627 . any ( |s| h == * s || h. ends_with ( & format ! ( ".{}" , s) ) )
36283628}
36293629
3630- /// Strip top-level field-3 (quality-track selection) entries from a SABR
3631- /// segment-fetch protobuf body.
3630+ /// Strip surplus field-3 (quality-track selection) entries from a SABR
3631+ /// segment-fetch protobuf body, keeping the first one intact .
36323632///
36333633/// YouTube's SABR (Server-Adaptive Bitrate) `videoplayback` POST bodies
36343634/// come in two distinct message shapes:
36353635///
36363636/// * **Segment-fetch** — carries field-2 (tag `0x12`) byte-range entries
36373637/// for video/audio segments. Field-3 (tag `0x1a`) entries here are
3638- /// quality-track selectors that ask googlevideo to bundle multiple
3639- /// simultaneous quality tracks into one response, easily exceeding
3640- /// Apps Script `UrlFetchApp`'s ~10 MB response buffer → 502. Stripping
3641- /// them forces a single-track response that fits.
3638+ /// quality-track selectors that ask googlevideo to return a particular
3639+ /// quality track. When the player asks for *multiple* tracks at once
3640+ /// (multi-track bundling), googlevideo concatenates them into a single
3641+ /// response — easily exceeding Apps Script `UrlFetchApp`'s ~10 MB cap
3642+ /// → 502.
36423643///
36433644/// * **Session-init** — carries field-5 (tag `0x2a`) entries and *no*
36443645/// field-2 entries. Field-3 here is essential session metadata
36453646/// (language, viewer state, ...). Stripping it corrupts the init
36463647/// handshake → CDN returns 403.
36473648///
3648- /// Heuristic: only strip field-3 when at least one field-2 entry is also
3649- /// present at the top level (segment-fetch shape). Otherwise return the
3650- /// body unchanged.
3649+ /// **Heuristic**:
3650+ ///
3651+ /// 1. Body must be segment-fetch shape (≥ 1 field-2 entry). Otherwise
3652+ /// no-op — session-init bodies must not be touched.
3653+ /// 2. Body must carry **2 or more** field-3 entries before stripping
3654+ /// fires. The first field-3 is always kept; only the 2nd, 3rd, ...
3655+ /// are removed.
3656+ ///
3657+ /// **Why keep the first field-3** (#977, unacoder testing, May 2026):
3658+ /// the original "strip all field-3" rule produced empty googlevideo
3659+ /// responses on single-track requests — the player asks for ONE track
3660+ /// via a sole field-3, we strip it, and the CDN answers a request with
3661+ /// zero tracks selected by sending nothing. The player retries
3662+ /// indefinitely with the `rn=` retry counter incrementing, never
3663+ /// advancing its buffer. Keeping the first field-3 means single-track
3664+ /// requests pass through unchanged (no regression at low quality)
3665+ /// while multi-track requests still get capped to one track (the
3666+ /// 10 MB-blowup fix is preserved).
36513667///
36523668/// Only top-level fields are inspected; nested messages are left intact.
36533669/// On a malformed body (truncated tag, unknown wire type) the unparsed
36543670/// tail is appended verbatim so a corrupt request is never silently
3655- /// truncated. Ported from upstream `_strip_sabr_quality_tracks`
3656- /// (commits 9b6d03e + 33db28a).
3671+ /// truncated. Originally ported from upstream
3672+ /// `_strip_sabr_quality_tracks` (commits 9b6d03e + 33db28a); the
3673+ /// keep-first refinement diverges from upstream based on local testing.
36573674pub ( crate ) fn strip_sabr_quality_tracks ( body : & [ u8 ] ) -> Vec < u8 > {
36583675 // Phase 1: single pass — collect (field_number, start, end) for every
3659- // top-level field. We need to know whether field 2 exists before deciding
3660- // to strip, but a two-pass walk would be wasteful.
3676+ // top-level field. We need both the segment-fetch detection (field-2
3677+ // present) AND the field-3 count (≥ 2 to fire) before deciding,
3678+ // and a two-pass walk would be wasteful.
36613679 let mut segments: Vec < ( u32 , usize , usize ) > = Vec :: new ( ) ;
36623680 let mut has_field2 = false ;
3663- let mut has_field3 = false ;
3681+ let mut field3_count : usize = 0 ;
36643682 let mut i = 0usize ;
36653683 let n = body. len ( ) ;
36663684 let mut tail_start = n;
@@ -3778,20 +3796,32 @@ pub(crate) fn strip_sabr_quality_tracks(body: &[u8]) -> Vec<u8> {
37783796 if field_number == 2 {
37793797 has_field2 = true ;
37803798 } else if field_number == 3 {
3781- has_field3 = true ;
3799+ field3_count += 1 ;
37823800 }
37833801 segments. push ( ( field_number, seg_start, i) ) ;
37843802 }
37853803
3786- // Phase 2: only strip when this is a segment-fetch body (has field 2)
3787- // AND there's at least one field-3 entry to strip.
3788- if !has_field2 || !has_field3 {
3804+ // Phase 2: only strip when this is a segment-fetch body (has field
3805+ // 2) AND there are at least 2 field-3 entries — i.e. real multi-
3806+ // track bundling. Single-track requests (one field-3) flow through
3807+ // unchanged so googlevideo still has a track selected.
3808+ if !has_field2 || field3_count < 2 {
37893809 return body. to_vec ( ) ;
37903810 }
37913811
3812+ // Keep the first field-3 entry, strip the rest. `field3_kept`
3813+ // flips to `true` after the first encounter so subsequent ones
3814+ // fall through the strip branch.
37923815 let mut out = Vec :: with_capacity ( body. len ( ) ) ;
3816+ let mut field3_kept = false ;
37933817 for ( field_number, seg_start, seg_end) in segments {
3794- if field_number != 3 {
3818+ if field_number == 3 {
3819+ if !field3_kept {
3820+ field3_kept = true ;
3821+ out. extend_from_slice ( & body[ seg_start..seg_end] ) ;
3822+ }
3823+ // else: strip
3824+ } else {
37953825 out. extend_from_slice ( & body[ seg_start..seg_end] ) ;
37963826 }
37973827 }
@@ -6091,10 +6121,32 @@ hello";
60916121 }
60926122
60936123 #[ test]
6094- fn sabr_strip_segment_fetch_drops_field3 ( ) {
6095- // Segment-fetch shape: has both field-2 (range descriptor) and
6096- // field-3 (quality-track selector). Strip should remove only the
6097- // field-3 entries, leaving everything else intact.
6124+ fn sabr_strip_keeps_sole_field3_unchanged ( ) {
6125+ // The #977 regression case from unacoder's testing: a
6126+ // segment-fetch body with exactly ONE field-3 entry (the
6127+ // single-track request the player sends at low/medium quality).
6128+ // The original "strip all field-3" rule turned this into a
6129+ // request with zero tracks selected, which googlevideo answered
6130+ // with an empty body — buffer never advanced, player retried
6131+ // 11+ times with `rn=` incrementing. Keep-first heuristic
6132+ // returns the body unchanged so the player gets a valid
6133+ // single-track response.
6134+ let mut body: Vec < u8 > = Vec :: new ( ) ;
6135+ enc_length_delim ( & mut body, 2 , b"range-descriptor" ) ;
6136+ enc_length_delim ( & mut body, 3 , b"sole-quality-track" ) ;
6137+ enc_varint_field ( & mut body, 4 , 12345 ) ;
6138+
6139+ // No transform: 1 field-3 < 2-entry threshold.
6140+ assert_eq ! ( strip_sabr_quality_tracks( & body) , body) ;
6141+ }
6142+
6143+ #[ test]
6144+ fn sabr_strip_segment_fetch_keeps_first_field3_strips_rest ( ) {
6145+ // Segment-fetch shape with TWO field-3 entries (multi-track
6146+ // bundling). The first field-3 is kept (preserves a single
6147+ // track on the wire so googlevideo has something to send); the
6148+ // second is stripped (caps the response under 10 MB). Other
6149+ // fields pass through unchanged.
60986150 let mut body: Vec < u8 > = Vec :: new ( ) ;
60996151 enc_length_delim ( & mut body, 2 , b"range-descriptor-1" ) ;
61006152 enc_length_delim ( & mut body, 3 , b"quality-track-selector-1" ) ;
@@ -6104,7 +6156,9 @@ hello";
61046156
61056157 let mut expected: Vec < u8 > = Vec :: new ( ) ;
61066158 enc_length_delim ( & mut expected, 2 , b"range-descriptor-1" ) ;
6159+ enc_length_delim ( & mut expected, 3 , b"quality-track-selector-1" ) ; // KEPT
61076160 enc_length_delim ( & mut expected, 2 , b"range-descriptor-2" ) ;
6161+ // quality-track-selector-2: STRIPPED
61086162 enc_varint_field ( & mut expected, 4 , 12345 ) ;
61096163
61106164 assert_eq ! ( strip_sabr_quality_tracks( & body) , expected) ;
@@ -6159,21 +6213,20 @@ hello";
61596213 }
61606214
61616215 #[ test]
6162- fn sabr_strip_truncated_tag_after_field3_preserves_tail ( ) {
6163- // field-2, field-3, then truncated. Strip should remove the
6164- // field-3 entry (segment-fetch shape) and copy the truncated
6165- // tail verbatim.
6216+ fn sabr_strip_truncated_tag_after_single_field3_is_noop ( ) {
6217+ // field-2 + ONE field-3 (single-track request) + truncated tag.
6218+ // Under the keep-first heuristic, single field-3 is preserved
6219+ // → strip is a no-op → body returned verbatim (truncated tail
6220+ // included). The original behaviour was to strip the sole
6221+ // field-3 here, but that's exactly the regression #977
6222+ // identified.
61666223 let mut body: Vec < u8 > = Vec :: new ( ) ;
61676224 enc_length_delim ( & mut body, 2 , b"range-desc" ) ;
61686225 enc_length_delim ( & mut body, 3 , b"quality-track" ) ;
6169- // truncated tag
6170- body. push ( 0x80 ) ;
6171-
6172- let mut expected: Vec < u8 > = Vec :: new ( ) ;
6173- enc_length_delim ( & mut expected, 2 , b"range-desc" ) ;
6174- expected. push ( 0x80 ) ;
6226+ body. push ( 0x80 ) ; // truncated tag
61756227
6176- assert_eq ! ( strip_sabr_quality_tracks( & body) , expected) ;
6228+ // No transform — single field-3 < 2-entry threshold.
6229+ assert_eq ! ( strip_sabr_quality_tracks( & body) , body) ;
61776230 }
61786231
61796232 #[ test]
@@ -6236,23 +6289,36 @@ hello";
62366289 }
62376290
62386291 #[ test]
6239- fn sabr_strip_truncated_fixed_width_preserves_segment_verbatim ( ) {
6240- // 64-bit fixed (wire type 1) — only 3 of 8 bytes present.
6241- // Without a length-check this used to clamp via .min(n) and
6242- // declare the field "complete." Now bails at the segment.
6292+ fn sabr_strip_truncated_fixed_width_with_single_field3_is_noop ( ) {
6293+ // 64-bit fixed (wire type 1) — only 3 of 8 bytes present. The
6294+ // bail-on-truncated-payload behaviour is unchanged; what's
6295+ // different from the older "strip all field-3" version is
6296+ // that a SOLE field-3 is now kept (keep-first heuristic),
6297+ // so the body comes back unchanged.
62436298 let mut body: Vec < u8 > = Vec :: new ( ) ;
62446299 enc_length_delim ( & mut body, 2 , b"r" ) ;
6245- enc_length_delim ( & mut body, 3 , b"q" ) ;
6246- // Tag = field 4, wire 1 = 0x21, then only 3 bytes follow.
6300+ enc_length_delim ( & mut body, 3 , b"q" ) ; // sole field-3 → kept
6301+ body. push ( 0x21 ) ;
6302+ body. extend_from_slice ( b"\x01 \x02 \x03 " ) ;
6303+
6304+ assert_eq ! ( strip_sabr_quality_tracks( & body) , body) ;
6305+ }
6306+
6307+ #[ test]
6308+ fn sabr_strip_truncated_fixed_width_with_two_field3_strips_extras ( ) {
6309+ // Same fixed-width-truncation shape, but with TWO field-3
6310+ // entries. Keep-first rule fires: first field-3 kept, second
6311+ // stripped, malformed tail verbatim.
6312+ let mut body: Vec < u8 > = Vec :: new ( ) ;
6313+ enc_length_delim ( & mut body, 2 , b"r" ) ;
6314+ enc_length_delim ( & mut body, 3 , b"q1" ) ;
6315+ enc_length_delim ( & mut body, 3 , b"q2" ) ; // stripped
62476316 body. push ( 0x21 ) ;
62486317 body. extend_from_slice ( b"\x01 \x02 \x03 " ) ;
62496318
6250- // The malformed tail starts at the truncated fixed-width tag,
6251- // so the field-2 / field-3 we did parse get emitted (segment-
6252- // fetch shape, field-3 stripped), then the tail verbatim.
62536319 let mut expected: Vec < u8 > = Vec :: new ( ) ;
62546320 enc_length_delim ( & mut expected, 2 , b"r" ) ;
6255- // field-3 stripped here
6321+ enc_length_delim ( & mut expected , 3 , b"q1" ) ; // kept
62566322 expected. push ( 0x21 ) ;
62576323 expected. extend_from_slice ( b"\x01 \x02 \x03 " ) ;
62586324 assert_eq ! ( strip_sabr_quality_tracks( & body) , expected) ;
@@ -6262,44 +6328,56 @@ hello";
62626328
62636329 // ── SABR kill-switch runtime gate (#977) ─────────────────────────────
62646330
6265- /// Build a known segment-fetch body (field-2 + field-3) that the
6266- /// strip would actually shrink — used to prove the gate at runtime
6267- /// rather than just the config-default round-trip.
6331+ /// Build a known segment-fetch body that the strip would actually
6332+ /// shrink — multi-track shape (field-2 + 2× field-3) so the
6333+ /// keep-first heuristic fires and removes the second field-3.
6334+ /// Used to prove the gate at runtime rather than just the
6335+ /// config-default round-trip.
62686336 fn segment_fetch_body ( ) -> Vec < u8 > {
62696337 let mut body: Vec < u8 > = Vec :: new ( ) ;
62706338 enc_length_delim ( & mut body, 2 , b"range-descriptor" ) ;
6271- enc_length_delim ( & mut body, 3 , b"quality-track-selector" ) ;
6339+ enc_length_delim ( & mut body, 3 , b"quality-track-selector-1" ) ;
6340+ enc_length_delim ( & mut body, 3 , b"quality-track-selector-2" ) ;
62726341 body
62736342 }
62746343
62756344 #[ test]
6276- fn sabr_strip_on_strips_segment_fetch_body_via_relay_gate ( ) {
6277- // sabr_strip = true (default): segment-fetch POST to a real
6278- // googlevideo URL is stripped. This protects the main behaviour
6279- // the kill-switch gates — if a future refactor accidentally
6280- // drops the `self.sabr_strip` check from `relay()`, the strip
6281- // would still apply on `true` and the test would pass; if the
6282- // refactor accidentally INVERTS the check, this test fails
6283- // because no bytes are removed.
6345+ fn sabr_strip_on_strips_extra_field3_entries_via_relay_gate ( ) {
6346+ // sabr_strip = true (default), multi-track segment-fetch body
6347+ // (the keep-first heuristic threshold). The first field-3
6348+ // entry must survive (so the player still has a track selected
6349+ // — the #977 lesson); subsequent field-3 entries must be gone
6350+ // (the 10 MB-blowup fix). Protects the main behaviour the
6351+ // kill-switch gates: if a future refactor drops the
6352+ // `self.sabr_strip` check, the strip still applies on `true`
6353+ // and the test passes; if the refactor inverts the check, this
6354+ // fails because no bytes are removed.
62846355 let fronter = fronter_for_test_with ( false , true ) ;
62856356 let body = segment_fetch_body ( ) ;
62866357 let result = fronter. maybe_strip_sabr_body (
62876358 "POST" ,
62886359 "https://rrx---sn-xxx.googlevideo.com/videoplayback?id=42" ,
62896360 & body,
62906361 ) ;
6291- let stripped = result. expect ( "sabr_strip=true must strip a segment-fetch body" ) ;
6362+ let stripped = result. expect ( "sabr_strip=true must strip a multi-track body" ) ;
62926363 assert ! (
62936364 stripped. len( ) < body. len( ) ,
62946365 "strip must remove at least one byte ({} -> {})" ,
62956366 body. len( ) ,
62966367 stripped. len( ) ,
62976368 ) ;
6298- // And the field-3 entry specifically is what was removed.
6369+ // First field-3 kept (single-track preservation), second stripped.
6370+ assert ! (
6371+ stripped
6372+ . windows( b"quality-track-selector-1" . len( ) )
6373+ . any( |w| w == b"quality-track-selector-1" ) ,
6374+ "first field-3 payload (quality-track-selector-1) must SURVIVE the strip" ,
6375+ ) ;
62996376 assert ! (
6300- !stripped. windows( b"quality-track-selector" . len( ) )
6301- . any( |w| w == b"quality-track-selector" ) ,
6302- "field-3 payload must be gone from stripped body" ,
6377+ !stripped
6378+ . windows( b"quality-track-selector-2" . len( ) )
6379+ . any( |w| w == b"quality-track-selector-2" ) ,
6380+ "subsequent field-3 payload (quality-track-selector-2) must be STRIPPED" ,
63036381 ) ;
63046382 }
63056383
@@ -6422,21 +6500,17 @@ hello";
64226500 }
64236501
64246502 #[ test]
6425- fn sabr_strip_truncated_varint_payload_preserves_segment_verbatim ( ) {
6426- // Wire type 0 (varint) with a continuation byte and no terminator.
6503+ fn sabr_strip_truncated_varint_payload_with_single_field3_is_noop ( ) {
6504+ // Wire type 0 (varint) with a continuation byte and no
6505+ // terminator. Sole field-3 → kept under keep-first heuristic
6506+ // → body returned verbatim (truncated tail included).
64276507 let mut body: Vec < u8 > = Vec :: new ( ) ;
64286508 enc_length_delim ( & mut body, 2 , b"r" ) ;
6429- enc_length_delim ( & mut body, 3 , b"q" ) ;
6430- // Tag = field 5, wire 0 = 0x28, then a continuation byte that
6431- // never terminates.
6509+ enc_length_delim ( & mut body, 3 , b"q" ) ; // sole field-3 → kept
64326510 body. push ( 0x28 ) ;
64336511 body. push ( 0x80 ) ; // continuation, then EOF
64346512
6435- let mut expected: Vec < u8 > = Vec :: new ( ) ;
6436- enc_length_delim ( & mut expected, 2 , b"r" ) ;
6437- expected. push ( 0x28 ) ;
6438- expected. push ( 0x80 ) ;
6439- assert_eq ! ( strip_sabr_quality_tracks( & body) , expected) ;
6513+ assert_eq ! ( strip_sabr_quality_tracks( & body) , body) ;
64406514 }
64416515
64426516 // ── StatsSnapshot::fmt_line + to_json (forwarder fields) ────────────
0 commit comments