@@ -16,6 +16,7 @@ pub const MAX_HEADER_MUTATION_BYTES: usize = 32 * 1024;
1616pub enum HeaderAuthority {
1717 Request ,
1818 Response ,
19+ ResponseTrailers ,
1920}
2021
2122#[ derive( Debug , Clone , PartialEq , Eq ) ]
@@ -30,6 +31,7 @@ pub enum HeaderMutationError {
3031 InvalidExistingAction ,
3132 MissingExistingAction { name : String } ,
3233 UnsupportedExistingAction ,
34+ AbsentTrailerName { name : String } ,
3335 Empty ,
3436}
3537
@@ -47,6 +49,7 @@ impl HeaderMutationError {
4749 Self :: InvalidExistingAction => "header_mutation_invalid_existing_action" ,
4850 Self :: MissingExistingAction { .. } => "header_mutation_missing_existing_action" ,
4951 Self :: UnsupportedExistingAction => "header_mutation_unsupported_existing_action" ,
52+ Self :: AbsentTrailerName { .. } => "trailer_mutation_absent_name" ,
5053 Self :: Empty => "header_mutation_empty" ,
5154 }
5255 }
@@ -104,6 +107,10 @@ impl std::fmt::Display for HeaderMutationError {
104107 "middleware returned unsupported on_existing action"
105108 )
106109 }
110+ Self :: AbsentTrailerName { name } => write ! (
111+ formatter,
112+ "middleware cannot create absent response trailer '{name}'"
113+ ) ,
107114 Self :: Empty => write ! ( formatter, "middleware returned an empty header mutation" ) ,
108115 }
109116 }
@@ -133,6 +140,15 @@ pub fn apply(
133140 Some ( header_mutation:: Operation :: Write ( write) ) => {
134141 let name = validate_name ( & write. name ) ?;
135142 validate_authority ( authority, MutationKind :: Write , & write. name , & name) ?;
143+ if authority == HeaderAuthority :: ResponseTrailers
144+ && !existing_headers
145+ . iter ( )
146+ . any ( |existing| existing. name . eq_ignore_ascii_case ( & name) )
147+ {
148+ return Err ( HeaderMutationError :: AbsentTrailerName {
149+ name : write. name . clone ( ) ,
150+ } ) ;
151+ }
136152 if is_connection_nominated ( connection_nominated_headers, & name) {
137153 return Err ( HeaderMutationError :: HopByHop {
138154 name : write. name . clone ( ) ,
@@ -229,6 +245,7 @@ fn validate_authority(
229245 is_response_protected ( normalized_name)
230246 || ( kind == MutationKind :: Write && is_response_remove_only ( normalized_name) )
231247 }
248+ HeaderAuthority :: ResponseTrailers => is_response_protected ( normalized_name) ,
232249 } ;
233250 if protected {
234251 return Err ( HeaderMutationError :: Protected {
@@ -650,4 +667,115 @@ mod tests {
650667 }
651668 }
652669 }
670+
671+ #[ test]
672+ fn empty_response_trailers_accept_pass_through ( ) {
673+ assert_eq ! (
674+ apply( HeaderAuthority :: ResponseTrailers , & [ ] , & [ ] , & [ ] ) ,
675+ Ok ( Vec :: new( ) )
676+ ) ;
677+ }
678+
679+ #[ test]
680+ fn response_trailer_pass_through_preserves_fields_and_order ( ) {
681+ let existing = [
682+ header ( "x-checksum" , "one" ) ,
683+ header ( "x-trace" , "middle" ) ,
684+ header ( "x-checksum" , "two" ) ,
685+ ] ;
686+
687+ let updated = apply ( HeaderAuthority :: ResponseTrailers , & existing, & [ ] , & [ ] )
688+ . expect ( "empty mutation list" ) ;
689+
690+ assert_eq ! ( updated, existing) ;
691+ }
692+
693+ #[ test]
694+ fn response_trailer_mutations_modify_remove_and_preserve_order ( ) {
695+ let existing = [
696+ header ( "x-checksum" , "one" ) ,
697+ header ( "x-remove" , "gone" ) ,
698+ header ( "x-trace" , "middle" ) ,
699+ header ( "x-checksum" , "two" ) ,
700+ ] ;
701+
702+ let updated = apply (
703+ HeaderAuthority :: ResponseTrailers ,
704+ & existing,
705+ & [ ] ,
706+ & [
707+ write ( "X-Checksum" , "replacement" , ExistingHeaderAction :: Overwrite ) ,
708+ remove ( "X-Remove" ) ,
709+ write ( "X-Trace" , "last" , ExistingHeaderAction :: Append ) ,
710+ ] ,
711+ )
712+ . expect ( "permitted response trailer mutations" ) ;
713+
714+ assert_eq ! (
715+ updated,
716+ vec![
717+ header( "x-trace" , "middle" ) ,
718+ header( "x-checksum" , "replacement" ) ,
719+ header( "x-trace" , "last" ) ,
720+ ]
721+ ) ;
722+ }
723+
724+ #[ test]
725+ fn response_trailer_write_cannot_introduce_an_absent_name ( ) {
726+ let existing = [ header ( "x-checksum" , "one" ) ] ;
727+ let error = apply (
728+ HeaderAuthority :: ResponseTrailers ,
729+ & existing,
730+ & [ ] ,
731+ & [ write (
732+ "X-New-Trailer" ,
733+ "value" ,
734+ ExistingHeaderAction :: Overwrite ,
735+ ) ] ,
736+ )
737+ . expect_err ( "absent response trailer name" ) ;
738+
739+ assert_eq ! (
740+ error,
741+ HeaderMutationError :: AbsentTrailerName {
742+ name: "X-New-Trailer" . into( )
743+ }
744+ ) ;
745+ }
746+
747+ #[ test]
748+ fn response_trailer_removal_of_an_absent_name_is_a_noop ( ) {
749+ let existing = [ header ( "x-checksum" , "one" ) ] ;
750+ let updated = apply (
751+ HeaderAuthority :: ResponseTrailers ,
752+ & existing,
753+ & [ ] ,
754+ & [ remove ( "X-Missing" ) ] ,
755+ )
756+ . expect ( "absent response trailer removal" ) ;
757+
758+ assert_eq ! ( updated, existing) ;
759+ }
760+
761+ #[ test]
762+ fn response_trailer_protected_fields_cannot_be_mutated ( ) {
763+ for mutation in [
764+ write ( "Content-Length" , "10" , ExistingHeaderAction :: Overwrite ) ,
765+ remove ( "Set-Cookie" ) ,
766+ ] {
767+ let error = apply (
768+ HeaderAuthority :: ResponseTrailers ,
769+ & [
770+ header ( "content-length" , "5" ) ,
771+ header ( "set-cookie" , "session=upstream" ) ,
772+ ] ,
773+ & [ ] ,
774+ & [ mutation] ,
775+ )
776+ . expect_err ( "protected response trailer mutation" ) ;
777+
778+ assert ! ( matches!( error, HeaderMutationError :: Protected { .. } ) ) ;
779+ }
780+ }
653781}
0 commit comments