Repository: hyperium/h2 (Rust)

Issue:
Add a setter for `header_table_size`
I'd like to make a PR to uncomment the setter for `header_table_size`, found here:
https://github.com/hyperium/h2/blob/87969c1f296173a7838956165014b4828dc5d5db/src/frame/settings.rs#L124-L128


When playing around with the setter I found out that connections would sometimes fail the following check, Since the decoder's size is never updated from the default `DEFAULT_SETTINGS_HEADER_TABLE_SIZE`.
https://github.com/hyperium/h2/blob/88b078925416d9d220b69d66192f7be63457d6b8/src/hpack/decoder.rs#L256-L258

Would [this](https://github.com/4JX/h2/commit/75f243426f40ae1cd51a05a5c51c76f7c5517cb3) be the place to fix that?


Discussion:
What would adding the setter do? The `frame`s aren't part of any public API, so a user couldn't do anything with function. Is there something extra that `h2` should be doing? What's the underlying problem you're trying to solve?
> The `frame`s aren't part of any public API, so a user couldn't do anything with function.

I'd want to expose that option through `h2::client::Builder`, the same way `max_concurrent_streams` and others are.

> What's the underlying problem you're trying to solve?

I'm doing some research into HTTP2 based fingerprinting. Its mostly about the flexibility of being able to set that value at will. (And also other things, which would not really matter for the average user)

I understand that adding something like this may not be in the scope of the project.
> I'd want to expose that option through h2::client::Builder, the same way max_concurrent_streams and others are.

That seems fair!



Patch:
diff --git a/src/client.rs b/src/client.rs
index b329121ab..35cfc1414 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1072,6 +1072,39 @@ impl Builder {
         self
     }
 
+    /// Sets the header table size.
+    ///
+    /// This setting informs the peer of the maximum size of the header compression
+    /// table used to encode header blocks, in octets. The encoder may select any value
+    /// equal to or less than the header table size specified by the sender.
+    ///
+    /// The default value is 4,096.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use tokio::io::{AsyncRead, AsyncWrite};
+    /// # use h2::client::*;
+    /// # use bytes::Bytes;
+    /// #
+    /// # async fn doc<T: AsyncRead + AsyncWrite + Unpin>(my_io: T)
+    /// # -> Result<((SendRequest<Bytes>, Connection<T, Bytes>)), h2::Error>
+    /// # {
+    /// // `client_fut` is a future representing the completion of the HTTP/2
+    /// // handshake.
+    /// let client_fut = Builder::new()
+    ///     .header_table_size(1_000_000)
+    ///     .handshake(my_io);
+    /// # client_fut.await
+    /// # }
+    /// #
+    /// # pub fn main() {}
+    /// ```
+    pub fn header_table_size(&mut self, size: u32) -> &mut Self {
+        self.settings.set_header_table_size(Some(size));
+        self
+    }
+
     /// Sets the first stream ID to something other than 1.
     #[cfg(feature = "unstable")]
     pub fn initial_stream_id(&mut self, stream_id: u32) -> &mut Self {
diff --git a/src/codec/framed_read.rs b/src/codec/framed_read.rs
index a874d7732..3b0030d93 100644
--- a/src/codec/framed_read.rs
+++ b/src/codec/framed_read.rs
@@ -88,6 +88,12 @@ impl<T> FramedRead<T> {
     pub fn set_max_header_list_size(&mut self, val: usize) {
         self.max_header_list_size = val;
     }
+
+    /// Update the header table size setting.
+    #[inline]
+    pub fn set_header_table_size(&mut self, val: usize) {
+        self.hpack.queue_size_update(val);
+    }
 }
 
 /// Decodes a frame.
diff --git a/src/codec/mod.rs b/src/codec/mod.rs
index 359adf6e4..6cbdc1e18 100644
--- a/src/codec/mod.rs
+++ b/src/codec/mod.rs
@@ -95,6 +95,11 @@ impl<T, B> Codec<T, B> {
         self.framed_write().set_header_table_size(val)
     }
 
+    /// Set the decoder header table size size.
+    pub fn set_recv_header_table_size(&mut self, val: usize) {
+        self.inner.set_header_table_size(val)
+    }
+
     /// Set the max header list size that can be received.
     pub fn set_max_recv_header_list_size(&mut self, val: usize) {
         self.inner.set_max_header_list_size(val);
diff --git a/src/frame/settings.rs b/src/frame/settings.rs
index 0c913f059..484498a9d 100644
--- a/src/frame/settings.rs
+++ b/src/frame/settings.rs
@@ -121,11 +121,9 @@ impl Settings {
         self.header_table_size
     }
 
-    /*
     pub fn set_header_table_size(&mut self, size: Option<u32>) {
         self.header_table_size = size;
     }
-    */
 
     pub fn load(head: Head, payload: &[u8]) -> Result<Settings, Error> {
         use self::Setting::*;
diff --git a/src/proto/settings.rs b/src/proto/settings.rs
index 6cc617209..28065cc68 100644
--- a/src/proto/settings.rs
+++ b/src/proto/settings.rs
@@ -60,6 +60,10 @@ impl Settings {
                         codec.set_max_recv_header_list_size(max as usize);
                     }
 
+                    if let Some(val) = local.header_table_size() {
+                        codec.set_recv_header_table_size(val as usize);
+                    }
+
                     streams.apply_local_settings(local)?;
                     self.local = Local::Synced;
                     Ok(())


Test patch:
diff --git a/tests/h2-support/src/frames.rs b/tests/h2-support/src/frames.rs
index d302d3ce5..a76dd3b60 100644
--- a/tests/h2-support/src/frames.rs
+++ b/tests/h2-support/src/frames.rs
@@ -391,6 +391,11 @@ impl Mock<frame::Settings> {
         self.0.set_enable_connect_protocol(Some(val));
         self
     }
+
+    pub fn header_table_size(mut self, val: u32) -> Self {
+        self.0.set_header_table_size(Some(val));
+        self
+    }
 }
 
 impl From<Mock<frame::Settings>> for frame::Settings {
diff --git a/tests/h2-tests/tests/client_request.rs b/tests/h2-tests/tests/client_request.rs
index 7b4316004..88c7df464 100644
--- a/tests/h2-tests/tests/client_request.rs
+++ b/tests/h2-tests/tests/client_request.rs
@@ -1627,6 +1627,40 @@ async fn rogue_server_reused_headers() {
     join(srv, h2).await;
 }
 
+#[tokio::test]
+async fn client_builder_header_table_size() {
+    h2_support::trace_init!();
+    let (io, mut srv) = mock::new();
+    let mut settings = frame::Settings::default();
+
+    settings.set_header_table_size(Some(10000));
+
+    let srv = async move {
+        let recv_settings = srv.assert_client_handshake().await;
+        assert_frame_eq(recv_settings, settings);
+
+        srv.recv_frame(
+            frames::headers(1)
+                .request("GET", "https://example.com/")
+                .eos(),
+        )
+        .await;
+        srv.send_frame(frames::headers(1).response(200).eos()).await;
+    };
+
+    let mut builder = client::Builder::new();
+    builder.header_table_size(10000);
+
+    let h2 = async move {
+        let (mut client, mut h2) = builder.handshake::<_, Bytes>(io).await.unwrap();
+        let request = Request::get("https://example.com/").body(()).unwrap();
+        let (response, _) = client.send_request(request, true).unwrap();
+        h2.drive(response).await.unwrap();
+    };
+
+    join(srv, h2).await;
+}
+
 const SETTINGS: &[u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0];
 const SETTINGS_ACK: &[u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0];