http: http/2 CONTINUATION frame support 07/43007/15
authorMatus Fabian <[email protected]>
Thu, 22 May 2025 17:45:08 +0000 (17:45 +0000)
committerMatus Fabian <[email protected]>
Fri, 6 Jun 2025 08:35:16 +0000 (08:35 +0000)
We can now receive and send headers split into multiple frames.

Type: improvement

Change-Id: I3a2a21c67dbc7bbd0bc19b8c6a0ff1516bcfc6fd
Signed-off-by: Matus Fabian <[email protected]>
extras/hs-test/http2_test.go
extras/hs-test/http_test.go
extras/hs-test/infra/suite_h2.go
src/plugins/hs_apps/http_tps.c
src/plugins/http/http.c
src/plugins/http/http2/frame.c
src/plugins/http/http2/frame.h
src/plugins/http/http2/http2.c

index dc2d210..0d170eb 100644 (file)
@@ -2,6 +2,7 @@ package main
 
 import (
        "fmt"
+       "strconv"
        "strings"
        "time"
 
@@ -9,7 +10,7 @@ import (
 )
 
 func init() {
-       RegisterH2Tests(Http2TcpGetTest, Http2TcpPostTest, Http2MultiplexingTest, Http2TlsTest)
+       RegisterH2Tests(Http2TcpGetTest, Http2TcpPostTest, Http2MultiplexingTest, Http2TlsTest, Http2ContinuationTxTest)
        RegisterH2SoloTests(Http2MultiplexingMTTest)
 }
 
@@ -106,3 +107,16 @@ func Http2TlsTest(s *H2Suite) {
        s.AssertContains(log, "ALPN: server accepted h2")
        s.AssertContains(writeOut, "version")
 }
+
+func Http2ContinuationTxTest(s *H2Suite) {
+       vpp := s.Containers.Vpp.VppInstance
+       serverAddress := s.VppAddr() + ":" + s.Ports.Port1
+       vpp.Vppctl("http tps uri tcp://" + serverAddress + " no-zc")
+       args := fmt.Sprintf("-w %%{size_header} --max-time 10 --noproxy '*' --http2-prior-knowledge http://%s/test_file_64?test_header=32k", serverAddress)
+       writeOut, log := s.RunCurlContainer(s.Containers.Curl, args)
+       s.AssertContains(log, "HTTP/2 200")
+       s.AssertContains(log, "[64 bytes data]")
+       sizeHeader, err := strconv.Atoi(strings.ReplaceAll(writeOut, "\x00", ""))
+       s.AssertNil(err, fmt.Sprint(err))
+       s.AssertGreaterThan(sizeHeader, 32768)
+}
index 953898d..19f54d1 100644 (file)
@@ -1192,11 +1192,11 @@ func HttpCliBadRequestTest(s *NoTopoSuite) {
 
 func HttpStaticHttp1OnlyTest(s *NoTopoSuite) {
        vpp := s.Containers.Vpp.VppInstance
-       serverAddress := s.VppAddr()
-       s.Log(vpp.Vppctl("http static server uri tls://" + serverAddress + "/80 url-handlers http1-only debug"))
+       serverAddress := s.VppAddr() + ":" + s.Ports.Http
+       s.Log(vpp.Vppctl("http static server uri tls://" + serverAddress + " url-handlers http1-only debug"))
 
        client := NewHttpClient(defaultHttpTimeout, true)
-       req, err := http.NewRequest("GET", "https://"+serverAddress+":80/version.json", nil)
+       req, err := http.NewRequest("GET", "https://"+serverAddress+"/version.json", nil)
        s.AssertNil(err, fmt.Sprint(err))
        resp, err := client.Do(req)
        s.AssertNil(err, fmt.Sprint(err))
index 36fc294..b007086 100644 (file)
@@ -88,7 +88,6 @@ func (s *H2Suite) VppAddr() string {
        return s.Interfaces.Tap.Peer.Ip4AddressString()
 }
 
-// Marked as pending since http plugin is not build with http/2 enabled by default
 var _ = Describe("Http2Suite", Ordered, ContinueOnFailure, func() {
        var s H2Suite
        BeforeAll(func() {
@@ -118,7 +117,6 @@ var _ = Describe("Http2Suite", Ordered, ContinueOnFailure, func() {
        }
 })
 
-// Marked as pending since http plugin is not build with http/2 enabled by default
 var _ = Describe("Http2SoloSuite", Ordered, ContinueOnFailure, Serial, func() {
        var s H2Suite
        BeforeAll(func() {
@@ -230,26 +228,21 @@ var http2Tests = []h2specTest{
        {desc: "http2/5.1/1"},
        {desc: "http2/5.1/2"},
        {desc: "http2/5.1/3"},
-       // TODO: CONTINUATION
-       // {desc: "http2/5.1/4"},
+       {desc: "http2/5.1/4"},
        {desc: "http2/5.1/5"},
        {desc: "http2/5.1/6"},
-       // TODO: CONTINUATION
-       // {desc: "http2/5.1/7"},
+       {desc: "http2/5.1/7"},
        {desc: "http2/5.1/8"},
        {desc: "http2/5.1/9"},
-       // TODO: CONTINUATION
-       // {desc: "http2/5.1/10"},
+       {desc: "http2/5.1/10"},
        {desc: "http2/5.1/11"},
        {desc: "http2/5.1/12"},
-       // TODO: CONTINUATION
-       // {desc: "http2/5.1/13"},
+       {desc: "http2/5.1/13"},
        // http2/5.3.1/* PRIORITY is deprecated
        {desc: "http2/5.4.1/1"},
        {desc: "http2/5.4.1/2"},
        {desc: "http2/5.5/1"},
-       // TODO: CONTINUATION
-       // {desc: "http2/5.5/2"},
+       {desc: "http2/5.5/2"},
        {desc: "http2/6.1/1"},
        {desc: "http2/6.1/2"},
        {desc: "http2/6.1/3"},
@@ -287,13 +280,12 @@ var http2Tests = []h2specTest{
        // TODO: message framing without content length using END_STREAM flag
        // {desc: "http2/6.9/2"},
        {desc: "http2/6.9/3"},
-       // TODO: CONTINUATION
-       // {desc: "http2/6.10/1"},
-       // {desc: "http2/6.10/2"},
-       // {desc: "http2/6.10/3"},
-       // {desc: "http2/6.10/4"},
-       // {desc: "http2/6.10/5"},
-       // {desc: "http2/6.10/6"},
+       {desc: "http2/6.10/1"},
+       {desc: "http2/6.10/2"},
+       {desc: "http2/6.10/3"},
+       {desc: "http2/6.10/4"},
+       {desc: "http2/6.10/5"},
+       {desc: "http2/6.10/6"},
        {desc: "http2/7/1"},
        // TODO: message framing without content length using END_STREAM flag
        // {desc: "http2/7/2"},
@@ -363,14 +355,14 @@ var _ = Describe("H2SpecSuite", Ordered, ContinueOnFailure, func() {
                                s.Log(testName + ": BEGIN")
                                vpp := s.Containers.Vpp.VppInstance
                                serverAddress := s.VppAddr()
-                               s.Log(vpp.Vppctl("http static server uri tls://" + serverAddress + "/" + s.Ports.Port1 + " url-handlers debug 2"))
+                               s.Log(vpp.Vppctl("http static server uri tls://" + serverAddress + "/" + s.Ports.Port1 + " url-handlers debug 2 fifo-size 16k"))
                                s.Log(vpp.Vppctl("test-url-handler enable"))
                                conf := &config.Config{
                                        Host:         serverAddress,
                                        Port:         s.Ports.Port1AsInt,
                                        Path:         "/test1",
                                        Timeout:      time.Second * 5,
-                                       MaxHeaderLen: 1024,
+                                       MaxHeaderLen: 4096,
                                        TLS:          true,
                                        Insecure:     true,
                                        Sections:     []string{test.desc},
index 486d4a5..617ced5 100644 (file)
@@ -73,6 +73,7 @@ typedef struct hs_main_
   u8 no_zc;
   u8 *default_uri;
   u32 seed;
+  u8 *test_header_value;
 } hts_main_t;
 
 static hts_main_t hts_main;
@@ -370,8 +371,10 @@ hts_ts_rx_callback (session_t *ts)
 {
   hts_main_t *htm = &hts_main;
   hts_session_t *hs;
-  u8 *target = 0;
+  u8 *target = 0, *query = 0;
   http_msg_t msg;
+  unformat_input_t input;
+  u64 test_header_len;
   int rv;
 
   hs = hts_session_get (ts->thread_index, ts->opaque);
@@ -415,6 +418,35 @@ hts_ts_rx_callback (session_t *ts)
                      msg.method_type == HTTP_REQ_GET ? "GET" : "POST",
                      target);
 
+      if (msg.data.target_query_len != 0)
+       {
+         vec_validate (query, msg.data.target_query_len - 1);
+         rv = svm_fifo_peek (ts->rx_fifo, msg.data.target_query_offset,
+                             msg.data.target_query_len, query);
+         ASSERT (rv == msg.data.target_query_len);
+         if (htm->debug_level)
+           clib_warning ("query: %v", query);
+         unformat_init_vector (&input, query);
+         if (unformat (&input, "test_header=%U", unformat_memory_size,
+                       &test_header_len))
+           {
+             if (test_header_len > vec_len (htm->test_header_value))
+               {
+                 test_header_len = vec_len (htm->test_header_value);
+                 clib_warning ("test_header_len too big, truncated to %U",
+                               format_memory_size, test_header_len);
+               }
+             vec_resize (hs->resp_headers_buf,
+                         sizeof (http_app_header_t) + test_header_len);
+             hs->resp_headers.len = vec_len (hs->resp_headers_buf);
+             hs->resp_headers.buf = hs->resp_headers_buf;
+             http_add_custom_header (
+               &hs->resp_headers, http_token_lit ("x-test"),
+               (const char *) htm->test_header_value, test_header_len);
+           }
+         vec_free (query);
+       }
+
       if (msg.method_type == HTTP_REQ_GET)
        {
          if (try_test_file (hs, target))
@@ -766,6 +798,8 @@ hts_create (vlib_main_t *vm)
   if (htm->no_zc)
     vec_validate (htm->test_data, (64 << 10) - 1);
 
+  vec_validate_init_empty (htm->test_header_value, htm->fifo_size - 1024, 'x');
+
   if (hts_attach (htm))
     {
       clib_warning ("failed to attach server");
index 2c923c6..951bf3a 100644 (file)
@@ -317,6 +317,7 @@ http_get_app_header_list (http_req_t *req, http_msg_t *msg)
   else
     {
       app_headers = hm->app_header_lists[as->thread_index];
+      vec_validate (app_headers, msg->data.headers_len - 1);
       rv = svm_fifo_dequeue (as->tx_fifo, msg->data.headers_len, app_headers);
       ASSERT (rv == msg->data.headers_len);
     }
@@ -837,7 +838,7 @@ http_transport_enable (vlib_main_t *vm, u8 is_en)
       vec_validate (hm->tx_bufs[i],
                    HTTP_UDP_PAYLOAD_MAX_LEN +
                      HTTP_UDP_PROXY_DATAGRAM_CAPSULE_OVERHEAD);
-      vec_validate (hm->app_header_lists[i], 32 << 10);
+      vec_validate (hm->app_header_lists[i], 64 << 10);
     }
 
   clib_timebase_init (&hm->timebase, 0 /* GMT */, CLIB_TIMEBASE_DAYLIGHT_NONE,
index 580ffff..07821de 100644 (file)
@@ -204,15 +204,13 @@ http2_frame_write_rst_stream (http2_error_t error_code, u32 stream_id,
   clib_memcpy_fast (p, &value, RST_STREAM_LENGTH);
 }
 
-#define GOAWAY_MIN_SIZE 8
-
 __clib_export http2_error_t
 http2_frame_read_goaway (u32 *error_code, u32 *last_stream_id, u8 *payload,
                         u32 payload_len)
 {
   u32 *value;
 
-  if (payload_len < GOAWAY_MIN_SIZE)
+  if (payload_len < HTTP2_GOAWAY_MIN_SIZE)
     return HTTP2_ERROR_FRAME_SIZE_ERROR;
 
   value = (u32 *) payload;
@@ -222,7 +220,6 @@ http2_frame_read_goaway (u32 *error_code, u32 *last_stream_id, u8 *payload,
   value = (u32 *) payload;
   *error_code = clib_net_to_host_u32 (*value);
 
-  /* TODO: Additional Debug Data */
   return HTTP2_ERROR_NO_ERROR;
 }
 
@@ -236,11 +233,11 @@ http2_frame_write_goaway (http2_error_t error_code, u32 last_stream_id,
   ASSERT (last_stream_id <= 0x7FFFFFFF);
 
   http2_frame_header_t fh = { .type = HTTP2_FRAME_TYPE_GOAWAY,
-                             .length = GOAWAY_MIN_SIZE };
+                             .length = HTTP2_GOAWAY_MIN_SIZE };
   p = http2_frame_header_alloc (dst);
   http2_frame_header_write (&fh, p);
 
-  vec_add2 (*dst, p, GOAWAY_MIN_SIZE);
+  vec_add2 (*dst, p, HTTP2_GOAWAY_MIN_SIZE);
   value = clib_host_to_net_u32 (last_stream_id);
   clib_memcpy_fast (p, &value, 4);
   p += 4;
@@ -308,6 +305,19 @@ http2_frame_write_headers_header (u32 headers_len, u32 stream_id, u8 flags,
   http2_frame_header_write (&fh, dst);
 }
 
+void
+http2_frame_write_continuation_header (u32 headers_len, u32 stream_id,
+                                      u8 flags, u8 *dst)
+{
+  ASSERT (stream_id > 0 && stream_id <= 0x7FFFFFFF);
+
+  http2_frame_header_t fh = { .type = HTTP2_FRAME_TYPE_CONTINUATION,
+                             .length = headers_len,
+                             .flags = flags,
+                             .stream_id = stream_id };
+  http2_frame_header_write (&fh, dst);
+}
+
 __clib_export http2_error_t
 http2_frame_read_data (u8 **data, u32 *data_len, u8 *payload, u32 payload_len,
                       u8 flags)
index 53a37c1..e19cfa7 100644 (file)
@@ -11,6 +11,7 @@
 
 #define HTTP2_FRAME_HEADER_SIZE 9
 #define HTTP2_PING_PAYLOAD_LEN 8
+#define HTTP2_GOAWAY_MIN_SIZE  8
 
 #define foreach_http2_frame_type                                              \
   _ (0x00, DATA, "DATA")                                                      \
@@ -161,14 +162,14 @@ void http2_frame_write_rst_stream (http2_error_t error_code, u32 stream_id,
 /**
  * Parse GOAWAY frame payload
  *
- * @param last_stream_id Parsed last stream ID
  * @param error_code     Parsed error code
+ * @param last_stream_id Parsed last stream ID
  * @param payload        Payload to parse
  * @param payload_len    Payload length
  *
  * @return @c HTTP2_ERROR_NO_ERROR on success, error otherwise
  */
-http2_error_t http2_frame_read_goaway (u32 *last_stream_id, u32 *error_code,
+http2_error_t http2_frame_read_goaway (u32 *error_code, u32 *last_stream_id,
                                       u8 *payload, u32 payload_len);
 
 /**
@@ -218,6 +219,19 @@ http2_error_t http2_frame_read_headers (u8 **headers, u32 *headers_len,
 void http2_frame_write_headers_header (u32 headers_len, u32 stream_id,
                                       u8 flags, u8 *dst);
 
+/**
+ * Write CONTINUATION frame header
+ *
+ * @param headers_len Header block fragment length
+ * @param stream_id   Stream ID, except 0
+ * @param flags       Frame header flags
+ * @param dst         Pointer where frame header will be written
+ *
+ * @note Use @c http2_frame_header_alloc before
+ */
+void http2_frame_write_continuation_header (u32 headers_len, u32 stream_id,
+                                           u8 flags, u8 *dst);
+
 /**
  * Parse DATA frame payload
  *
index 38a3cca..8d47083 100644 (file)
@@ -56,10 +56,13 @@ typedef struct http2_req_
   u8 *payload;
   u32 payload_len;
   clib_llist_anchor_t sched_list;
+  void (*dispatch_headers_cb) (struct http2_req_ *req, http_conn_t *hc,
+                              u8 *n_emissions, clib_llist_index_t *next_ri);
 } http2_req_t;
 
 #define foreach_http2_conn_flags                                              \
   _ (EXPECT_PREFACE, "expect-preface")                                        \
+  _ (EXPECT_CONTINUATION, "expect-continuation")                              \
   _ (PREFACE_VERIFIED, "preface-verified")                                    \
   _ (TS_DESCHED, "ts-descheduled")
 
@@ -92,6 +95,9 @@ typedef struct http2_conn_ctx_
   clib_llist_index_t old_tx_streams; /* data */
   http2_conn_settings_t settings;
   clib_llist_anchor_t sched_list;
+  u8 *unparsed_headers; /* temporary storing rx fragmented headers */
+  u8 *unsent_headers;  /* temporary storing tx fragmented headers */
+  u32 unsent_headers_offset;
 } http2_conn_ctx_t;
 
 typedef struct http2_worker_ctx_
@@ -99,6 +105,7 @@ typedef struct http2_worker_ctx_
   http2_conn_ctx_t *conn_pool;
   http2_req_t *req_pool;
   clib_llist_index_t sched_head;
+  u8 *header_list; /* buffer for headers decompression */
 } http2_worker_ctx_t;
 
 typedef struct http2_main_
@@ -111,6 +118,7 @@ typedef struct http2_main_
 typedef enum
 {
   HTTP2_SCHED_WEIGHT_DATA_PTR = 1,
+  HTTP2_SCHED_WEIGHT_HEADERS_CONTINUATION = 1,
   HTTP2_SCHED_WEIGHT_DATA_INLINE = 2,
   HTTP2_SCHED_WEIGHT_HEADERS_PTR = 3,
   HTTP2_SCHED_WEIGHT_HEADERS_INLINE = 4,
@@ -449,17 +457,77 @@ http2_send_server_preface (http_conn_t *hc)
 /* stream TX scheduler */
 /***********************/
 
+static void
+http2_sched_dispatch_continuation (http2_req_t *req, http_conn_t *hc,
+                                  u8 *n_emissions,
+                                  clib_llist_index_t *next_ri)
+{
+  u8 fh[HTTP2_FRAME_HEADER_SIZE];
+  u8 flags = 0;
+  u32 n_written, stream_id, max_write, headers_len, headers_left;
+  http2_conn_ctx_t *h2c;
+  http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index);
+
+  *n_emissions += HTTP2_SCHED_WEIGHT_HEADERS_CONTINUATION;
+
+  h2c = http2_conn_ctx_get_w_thread (hc);
+
+  max_write = http_io_ts_max_write (hc, 0);
+  max_write -= HTTP2_FRAME_HEADER_SIZE;
+  max_write = clib_min (max_write, h2c->peer_settings.max_frame_size);
+
+  stream_id = req->stream_id;
+
+  ASSERT (vec_len (h2c->unsent_headers) > h2c->unsent_headers_offset);
+  headers_left = vec_len (h2c->unsent_headers) - h2c->unsent_headers_offset;
+  headers_len = clib_min (max_write, headers_left);
+  flags |= (headers_len == headers_left) ? HTTP2_FRAME_FLAG_END_HEADERS : 0;
+  http2_frame_write_continuation_header (headers_len, stream_id, flags, fh);
+  svm_fifo_seg_t segs[2] = {
+    { fh, HTTP2_FRAME_HEADER_SIZE },
+    { h2c->unsent_headers + h2c->unsent_headers_offset, headers_len }
+  };
+  n_written = http_io_ts_write_segs (hc, segs, 2, 0);
+  ASSERT (n_written == (HTTP2_FRAME_HEADER_SIZE + headers_len));
+  http_io_ts_after_write (hc, 0);
+
+  if (headers_len == headers_left)
+    {
+      HTTP_DBG (1, "sent last headers fragment");
+      vec_free (h2c->unsent_headers);
+      *next_ri = clib_llist_next_index (req, sched_list);
+      clib_llist_remove (wrk->req_pool, sched_list, req);
+      flags |= HTTP2_FRAME_FLAG_END_HEADERS;
+      if (http_buffer_bytes_left (&req->base.tx_buf))
+       {
+         /* start sending the actual data */
+         HTTP_DBG (1, "adding to data queue req_index %x",
+                   ((http_req_handle_t) req->base.hr_req_handle).req_index);
+         http2_req_schedule_data_tx (hc, req);
+       }
+      else
+       http2_stream_close (req, hc);
+    }
+  else
+    {
+      HTTP_DBG (1, "need another headers fragment");
+      *next_ri = clib_llist_entry_index (wrk->req_pool, req);
+      h2c->unsent_headers_offset += headers_len;
+    }
+}
+
 static void
 http2_sched_dispatch_headers (http2_req_t *req, http_conn_t *hc,
-                             u8 *n_emissions)
+                             u8 *n_emissions, clib_llist_index_t *next_ri)
 {
   http_msg_t msg;
   u8 *response, *date, *app_headers = 0;
   u8 fh[HTTP2_FRAME_HEADER_SIZE];
   hpack_response_control_data_t control_data;
-  u8 flags = HTTP2_FRAME_FLAG_END_HEADERS;
-  u32 n_written, stream_id, n_deq;
+  u8 flags = 0;
+  u32 n_written, stream_id, n_deq, max_write, headers_len, headers_left;
   http2_conn_ctx_t *h2c;
+  http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index);
 
   http_get_app_msg (&req->base, &msg);
   ASSERT (msg.type == HTTP_MSG_REPLY);
@@ -488,38 +556,62 @@ http2_sched_dispatch_headers (http2_req_t *req, http_conn_t *hc,
   hpack_serialize_response (app_headers, msg.data.headers_len, &control_data,
                            &response);
   vec_free (date);
+  headers_len = vec_len (response);
 
   h2c = http2_conn_ctx_get_w_thread (hc);
-  if (vec_len (response) > h2c->peer_settings.max_frame_size)
-    {
-      /* TODO: CONTINUATION (headers fragmentation) */
-      clib_warning ("resp headers greater than SETTINGS_MAX_FRAME_SIZE");
-      http2_stream_error (hc, req, HTTP2_ERROR_INTERNAL_ERROR, 0);
-      return;
-    }
+
+  max_write = http_io_ts_max_write (hc, 0);
+  max_write -= HTTP2_FRAME_HEADER_SIZE;
+  max_write = clib_min (max_write, h2c->peer_settings.max_frame_size);
 
   stream_id = req->stream_id;
+
+  /* END_STREAM flag need to be set in HEADERS frame */
   if (msg.data.body_len)
     {
-      /* start sending the actual data */
       http_req_tx_buffer_init (&req->base, &msg);
-      HTTP_DBG (1, "adding to data queue req_index %x",
-               ((http_req_handle_t) req->base.hr_req_handle).req_index);
-      http2_req_schedule_data_tx (hc, req);
       http_io_as_dequeue_notify (&req->base, n_deq);
     }
   else
+    flags |= HTTP2_FRAME_FLAG_END_STREAM;
+
+  if (headers_len <= max_write)
     {
-      /* no response body, we are done */
-      flags |= HTTP2_FRAME_FLAG_END_STREAM;
-      http2_stream_close (req, hc);
+      *next_ri = clib_llist_next_index (req, sched_list);
+      clib_llist_remove (wrk->req_pool, sched_list, req);
+      flags |= HTTP2_FRAME_FLAG_END_HEADERS;
+      if (msg.data.body_len)
+       {
+         /* start sending the actual data */
+         HTTP_DBG (1, "adding to data queue req_index %x",
+                   ((http_req_handle_t) req->base.hr_req_handle).req_index);
+         http2_req_schedule_data_tx (hc, req);
+       }
+      else
+       http2_stream_close (req, hc);
     }
-
-  http2_frame_write_headers_header (vec_len (response), stream_id, flags, fh);
+  else
+    {
+      /* we need to send CONTINUATION frame as next */
+      HTTP_DBG (1, "response headers need to be fragmented");
+      *next_ri = clib_llist_entry_index (wrk->req_pool, req);
+      headers_len = max_write;
+      headers_left = vec_len (response) - headers_len;
+      req->dispatch_headers_cb = http2_sched_dispatch_continuation;
+      /* move unsend portion of headers to connection ctx */
+      ASSERT (h2c->unsent_headers == 0);
+      vec_validate (h2c->unsent_headers, headers_left - 1);
+      clib_memcpy_fast (h2c->unsent_headers, response + headers_len,
+                       headers_left);
+      h2c->unsent_headers_offset = 0;
+      *n_emissions += HTTP2_SCHED_WEIGHT_HEADERS_CONTINUATION;
+    }
+
+  http2_frame_write_headers_header (headers_len, stream_id, flags, fh);
   svm_fifo_seg_t segs[2] = { { fh, HTTP2_FRAME_HEADER_SIZE },
-                            { response, vec_len (response) } };
+                            { response, headers_len } };
   n_written = http_io_ts_write_segs (hc, segs, 2, 0);
-  ASSERT (n_written == (HTTP2_FRAME_HEADER_SIZE + vec_len (response)));
+  ASSERT (n_written == (HTTP2_FRAME_HEADER_SIZE + headers_len));
   http_io_ts_after_write (hc, 0);
 }
 
@@ -641,11 +733,9 @@ http2_update_time_callback (f64 now, u8 thread_index)
             n_emissions < HTTP2_SCHED_MAX_EMISSIONS)
        {
          req = clib_llist_elt (wrk->req_pool, ri);
-         ri = clib_llist_next_index (req, sched_list);
          HTTP_DBG (1, "sending headers req_index %x",
                    ((http_req_handle_t) req->base.hr_req_handle).req_index);
-         clib_llist_remove (wrk->req_pool, sched_list, req);
-         http2_sched_dispatch_headers (req, hc, &n_emissions);
+         req->dispatch_headers_cb (req, hc, &n_emissions, &ri);
        }
 
       /* handle old responses (data frames), if we had any prior to processing
@@ -699,24 +789,26 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
 {
   http2_conn_ctx_t *h2c;
   hpack_request_control_data_t control_data;
-  u8 *buf = 0;
   http_msg_t msg;
   int rv;
   http_req_state_t new_state = HTTP_REQ_STATE_WAIT_APP_REPLY;
+  http2_worker_ctx_t *wrk = http2_get_worker (hc->c_thread_index);
 
   h2c = http2_conn_ctx_get_w_thread (hc);
 
-  /* TODO: configurable buf size with bigger default value */
-  vec_validate_init_empty (buf, 1023, 0);
-  *error = hpack_parse_request (req->payload, req->payload_len, buf, 1023,
-                               &control_data, &req->base.headers,
-                               &h2c->decoder_dynamic_table);
+  *error =
+    hpack_parse_request (req->payload, req->payload_len, wrk->header_list,
+                        vec_len (wrk->header_list), &control_data,
+                        &req->base.headers, &h2c->decoder_dynamic_table);
   if (*error != HTTP2_ERROR_NO_ERROR)
     {
       HTTP_DBG (1, "hpack_parse_request failed");
       return HTTP_SM_ERROR;
     }
 
+  HTTP_DBG (1, "decompressed headers size %u", control_data.headers_len);
+  HTTP_DBG (1, "dynamic table size %u", h2c->decoder_dynamic_table.used);
+
   if (!(control_data.parsed_bitmap & HPACK_PSEUDO_HEADER_METHOD_PARSED))
     {
       HTTP_DBG (1, ":method pseudo-header missing in request");
@@ -759,13 +851,13 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
     }
 
   req->base.control_data_len = control_data.control_data_len;
-  req->base.headers_offset = control_data.headers - buf;
+  req->base.headers_offset = control_data.headers - wrk->header_list;
   req->base.headers_len = control_data.headers_len;
   if (control_data.content_len_header_index != ~0)
     {
       req->base.content_len_header_index =
        control_data.content_len_header_index;
-      rv = http_parse_content_length (&req->base, buf);
+      rv = http_parse_content_length (&req->base, wrk->header_list);
       if (rv)
        {
          http2_stream_error (hc, req, HTTP2_ERROR_PROTOCOL_ERROR, sp);
@@ -784,20 +876,20 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
   req->base.to_recv = req->base.body_len;
 
   req->base.target_path_len = control_data.path_len;
-  req->base.target_path_offset = control_data.path - buf;
+  req->base.target_path_offset = control_data.path - wrk->header_list;
   /* drop leading slash */
   req->base.target_path_offset++;
   req->base.target_path_len--;
   req->base.target_query_offset = 0;
   req->base.target_query_len = 0;
-  http_identify_optional_query (&req->base, buf);
+  http_identify_optional_query (&req->base, wrk->header_list);
 
   msg.type = HTTP_MSG_REQUEST;
   msg.method_type = control_data.method;
   msg.data.type = HTTP_MSG_DATA_INLINE;
   msg.data.len = req->base.connection_header_index;
   msg.data.scheme = control_data.scheme;
-  msg.data.target_authority_offset = control_data.authority - buf;
+  msg.data.target_authority_offset = control_data.authority - wrk->header_list;
   msg.data.target_authority_len = control_data.authority_len;
   msg.data.target_path_offset = req->base.target_path_offset;
   msg.data.target_path_len = req->base.target_path_len;
@@ -811,8 +903,10 @@ http2_req_state_wait_transport_method (http_conn_t *hc, http2_req_t *req,
   msg.data.body_len = req->base.body_len;
 
   svm_fifo_seg_t segs[2] = { { (u8 *) &msg, sizeof (msg) },
-                            { buf, req->base.control_data_len } };
-  HTTP_DBG (3, "%U", format_http_bytes, buf, req->base.control_data_len);
+                            { wrk->header_list,
+                              req->base.control_data_len } };
+  HTTP_DBG (3, "%U", format_http_bytes, wrk->header_list,
+           req->base.control_data_len);
   http_io_as_write_segs (&req->base, segs, 2);
   http_req_state_change (&req->base, new_state);
   http_app_worker_rx_notify (&req->base);
@@ -982,16 +1076,12 @@ static http2_error_t
 http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh)
 {
   http2_req_t *req;
-  u8 *rx_buf;
+  u8 *rx_buf, *headers_start;
+  u32 headers_len;
+  uword n_del, n_dec;
   http2_error_t rv;
   http2_conn_ctx_t *h2c;
 
-  if (!(fh->flags & HTTP2_FRAME_FLAG_END_HEADERS))
-    {
-      /* TODO: fragmented headers */
-      return HTTP2_ERROR_INTERNAL_ERROR;
-    }
-
   if (hc->flags & HTTP_CONN_F_IS_SERVER)
     {
       h2c = http2_conn_ctx_get_w_thread (hc);
@@ -1018,6 +1108,7 @@ http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh)
          return HTTP2_ERROR_NO_ERROR;
        }
       req = http2_conn_alloc_req (hc, fh->stream_id);
+      req->dispatch_headers_cb = http2_sched_dispatch_headers;
       http_conn_accept_request (hc, &req->base);
       http_req_state_change (&req->base, HTTP_REQ_STATE_WAIT_TRANSPORT_METHOD);
       req->stream_state = HTTP2_STREAM_STATE_OPEN;
@@ -1031,6 +1122,31 @@ http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh)
        }
       if (fh->flags & HTTP2_FRAME_FLAG_END_STREAM)
        req->stream_state = HTTP2_STREAM_STATE_HALF_CLOSED;
+
+      if (!(fh->flags & HTTP2_FRAME_FLAG_END_HEADERS))
+       {
+         HTTP_DBG (1, "fragmented headers stream id %u", fh->stream_id);
+         h2c->flags |= HTTP2_CONN_F_EXPECT_CONTINUATION;
+         vec_validate (h2c->unparsed_headers, fh->length - 1);
+         http_io_ts_read (hc, h2c->unparsed_headers, fh->length, 0);
+         rv = http2_frame_read_headers (&headers_start, &headers_len,
+                                        h2c->unparsed_headers, fh->length,
+                                        fh->flags);
+         if (rv != HTTP2_ERROR_NO_ERROR)
+           return rv;
+
+         /* in case frame has padding */
+         if (PREDICT_FALSE (headers_start != h2c->unparsed_headers))
+           {
+             n_dec = fh->length - headers_len;
+             n_del = headers_start - h2c->unparsed_headers;
+             n_dec -= n_del;
+             vec_delete (h2c->unparsed_headers, n_del, 0);
+             vec_dec_len (h2c->unparsed_headers, n_dec);
+           }
+
+         return HTTP2_ERROR_NO_ERROR;
+       }
     }
   else
     {
@@ -1051,6 +1167,53 @@ http2_handle_headers_frame (http_conn_t *hc, http2_frame_header_t *fh)
   return http2_req_run_state_machine (hc, req, 0, 0);
 }
 
+static http2_error_t
+http2_handle_continuation_frame (http_conn_t *hc, http2_frame_header_t *fh)
+{
+  http2_req_t *req;
+  http2_conn_ctx_t *h2c;
+  u8 *p;
+  http2_error_t rv = HTTP2_ERROR_NO_ERROR;
+
+  if (hc->flags & HTTP_CONN_F_IS_SERVER)
+    {
+      h2c = http2_conn_ctx_get_w_thread (hc);
+
+      if (!(h2c->flags & HTTP2_CONN_F_EXPECT_CONTINUATION))
+       {
+         HTTP_DBG (1, "unexpected CONTINUATION frame");
+         return HTTP2_ERROR_PROTOCOL_ERROR;
+       }
+
+      if (fh->stream_id != h2c->last_opened_stream_id)
+       {
+         HTTP_DBG (1, "invalid stream id %u", fh->stream_id);
+         return HTTP2_ERROR_PROTOCOL_ERROR;
+       }
+
+      vec_add2 (h2c->unparsed_headers, p, fh->length);
+      http_io_ts_read (hc, p, fh->length, 0);
+
+      if (fh->flags & HTTP2_FRAME_FLAG_END_HEADERS)
+       {
+         req = http2_conn_get_req (hc, fh->stream_id);
+         h2c->flags &= ~HTTP2_CONN_F_EXPECT_CONTINUATION;
+         req->payload = h2c->unparsed_headers;
+         req->payload_len = vec_len (h2c->unparsed_headers);
+         HTTP_DBG (1, "run state machine");
+         rv = http2_req_run_state_machine (hc, req, 0, 0);
+         vec_free (h2c->unparsed_headers);
+       }
+    }
+  else
+    {
+      /* TODO: client */
+      return HTTP2_ERROR_INTERNAL_ERROR;
+    }
+
+  return rv;
+}
+
 static http2_error_t
 http2_handle_data_frame (http_conn_t *hc, http2_frame_header_t *fh)
 {
@@ -1325,7 +1488,8 @@ http2_handle_goaway_frame (http_conn_t *hc, http2_frame_header_t *fh)
   if (rv != HTTP2_ERROR_NO_ERROR)
     return rv;
 
-  HTTP_DBG (1, "received GOAWAY %U", format_http2_error, error_code);
+  HTTP_DBG (1, "received GOAWAY %U, last stream id %u", format_http2_error,
+           error_code, last_stream_id);
 
   if (error_code == HTTP2_ERROR_NO_ERROR)
     {
@@ -1333,6 +1497,10 @@ http2_handle_goaway_frame (http_conn_t *hc, http2_frame_header_t *fh)
     }
   else
     {
+      if (fh->length > HTTP2_GOAWAY_MIN_SIZE)
+       clib_warning ("additional debug data: %U", format_http_bytes,
+                     rx_buf + HTTP2_GOAWAY_MIN_SIZE,
+                     fh->length - HTTP2_GOAWAY_MIN_SIZE);
       /* connection error */
       h2c = http2_conn_ctx_get_w_thread (hc);
       hash_foreach (stream_id, req_index, h2c->req_by_stream_id, ({
@@ -1669,7 +1837,16 @@ http2_transport_rx_callback (http_conn_t *hc)
       http_io_ts_drain (hc, HTTP2_FRAME_HEADER_SIZE);
       to_deq -= fh.length;
 
-      HTTP_DBG (1, "frame type 0x%02x", fh.type);
+      HTTP_DBG (1, "frame type 0x%02x len %u", fh.type, fh.length);
+
+      if ((h2c->flags & HTTP2_CONN_F_EXPECT_CONTINUATION) &&
+         fh.type != HTTP2_FRAME_TYPE_CONTINUATION)
+       {
+         HTTP_DBG (1, "expected CONTINUATION frame");
+         http2_connection_error (hc, HTTP2_ERROR_PROTOCOL_ERROR, 0);
+         return;
+       }
+
       switch (fh.type)
        {
        case HTTP2_FRAME_TYPE_HEADERS:
@@ -1694,8 +1871,7 @@ http2_transport_rx_callback (http_conn_t *hc)
          rv = http2_handle_ping_frame (hc, &fh);
          break;
        case HTTP2_FRAME_TYPE_CONTINUATION:
-         /* TODO */
-         rv = HTTP2_ERROR_INTERNAL_ERROR;
+         rv = http2_handle_continuation_frame (hc, &fh);
          break;
        case HTTP2_FRAME_TYPE_PUSH_PROMISE:
          rv = http2_handle_push_promise (hc, &fh);
@@ -1867,6 +2043,7 @@ http2_enable_callback (void)
     {
       wrk = &h2m->wrk_ctx[i];
       wrk->sched_head = clib_llist_make_head (wrk->conn_pool, sched_list);
+      vec_validate (wrk->header_list, h2m->settings.max_header_list_size - 1);
     }
 }
 
@@ -1957,6 +2134,7 @@ http2_init (vlib_main_t *vm)
   clib_warning ("http/2 enabled");
   h2m->settings = http2_default_conn_settings;
   h2m->settings.max_concurrent_streams = 100; /* by default unlimited */
+  h2m->settings.max_header_list_size = 1 << 14; /* by default unlimited */
   http_register_engine (&http2_engine, HTTP_VERSION_2);
 
   return 0;