You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/09/03 19:08:59 UTC

[trafficserver] 02/05: Optimize HTTPHdr conversion of HTTP/1.1 to HTTP/2

This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 53ddfaf8e0e3e0efb0151b28f4fe218bae7d4963
Author: Masaori Koshiba <ma...@apache.org>
AuthorDate: Tue Mar 24 08:45:01 2020 +0900

    Optimize HTTPHdr conversion of HTTP/1.1 to HTTP/2
    
    Avoid extra HdrHeap allocation and copy all headers.
    
    (cherry picked from commit 3b0716d9f61ad276beab2d6d7a74d804b5b52cd5)
---
 proxy/http2/HTTP2.cc                 | 231 ++++++++++++++++++++++++-----------
 proxy/http2/HTTP2.h                  |  16 ++-
 proxy/http2/Http2ConnectionState.cc  |  61 ++++-----
 proxy/http2/Http2Stream.cc           |   6 +-
 proxy/http2/Makefile.am              |   1 +
 proxy/http2/unit_tests/test_HTTP2.cc | 169 +++++++++++++++++++++++++
 proxy/http3/Http3HeaderFramer.cc     |  11 +-
 proxy/http3/Http3HeaderFramer.h      |   2 +-
 8 files changed, 385 insertions(+), 112 deletions(-)

diff --git a/proxy/http2/HTTP2.cc b/proxy/http2/HTTP2.cc
index 6616f03..bb4b4df 100644
--- a/proxy/http2/HTTP2.cc
+++ b/proxy/http2/HTTP2.cc
@@ -45,6 +45,16 @@ const unsigned HTTP2_LEN_STATUS    = countof(":status") - 1;
 static size_t HTTP2_LEN_STATUS_VALUE_STR         = 3;
 static const uint32_t HTTP2_MAX_TABLE_SIZE_LIMIT = 64 * 1024;
 
+namespace
+{
+struct Http2HeaderName {
+  const char *name = nullptr;
+  int name_len     = 0;
+};
+
+Http2HeaderName http2_connection_specific_headers[5] = {};
+} // namespace
+
 // Statistics
 RecRawStatBlock *http2_rsb;
 static const char *const HTTP2_STAT_CURRENT_CLIENT_CONNECTION_NAME        = "proxy.process.http2.current_client_connections";
@@ -501,87 +511,146 @@ http2_convert_header_from_2_to_1_1(HTTPHdr *headers)
   return PARSE_RESULT_DONE;
 }
 
+/**
+  Initialize HTTPHdr for HTTP/2
+
+  Reserve HTTP/2 Pseudo-Header Fields in front of HTTPHdr. Value of these header fields will be set by
+  `http2_convert_header_from_1_1_to_2()`. When a HTTPHdr for HTTP/2 headers is created, this should be called immediately.
+  Because all pseudo-header fields MUST appear in the header block before regular header fields.
+ */
 void
-http2_generate_h2_header_from_1_1(HTTPHdr *headers, HTTPHdr *h2_headers)
+http2_init_pseudo_headers(HTTPHdr &hdr)
 {
-  h2_headers->create(http_hdr_type_get(headers->m_http));
+  switch (http_hdr_type_get(hdr.m_http)) {
+  case HTTP_TYPE_REQUEST: {
+    MIMEField *method = hdr.field_create(HTTP2_VALUE_METHOD, HTTP2_LEN_METHOD);
+    hdr.field_attach(method);
+
+    MIMEField *scheme = hdr.field_create(HTTP2_VALUE_SCHEME, HTTP2_LEN_SCHEME);
+    hdr.field_attach(scheme);
+
+    MIMEField *authority = hdr.field_create(HTTP2_VALUE_AUTHORITY, HTTP2_LEN_AUTHORITY);
+    hdr.field_attach(authority);
+
+    MIMEField *path = hdr.field_create(HTTP2_VALUE_PATH, HTTP2_LEN_PATH);
+    hdr.field_attach(path);
+
+    break;
+  }
+  case HTTP_TYPE_RESPONSE: {
+    MIMEField *status = hdr.field_create(HTTP2_VALUE_STATUS, HTTP2_LEN_STATUS);
+    hdr.field_attach(status);
+
+    break;
+  }
+  default:
+    ink_abort("HTTP_TYPE_UNKNOWN");
+  }
+}
+
+/**
+  Convert HTTP/1.1 HTTPHdr to HTTP/2
+
+  Assuming HTTP/2 Pseudo-Header Fields are reserved by `http2_init_pseudo_headers()`.
+ */
+ParseResult
+http2_convert_header_from_1_1_to_2(HTTPHdr *headers)
+{
+  switch (http_hdr_type_get(headers->m_http)) {
+  case HTTP_TYPE_REQUEST: {
+    // :method
+    if (MIMEField *field = headers->field_find(HTTP2_VALUE_METHOD, HTTP2_LEN_METHOD); field != nullptr) {
+      int value_len;
+      const char *value = headers->method_get(&value_len);
+
+      field->value_set(headers->m_heap, headers->m_mime, value, value_len);
+    } else {
+      ink_abort("initialize HTTP/2 pseudo-headers");
+      return PARSE_RESULT_ERROR;
+    }
+
+    // :scheme
+    if (MIMEField *field = headers->field_find(HTTP2_VALUE_SCHEME, HTTP2_LEN_SCHEME); field != nullptr) {
+      int value_len;
+      const char *value = headers->scheme_get(&value_len);
+
+      if (value != nullptr) {
+        field->value_set(headers->m_heap, headers->m_mime, value, value_len);
+      } else {
+        field->value_set(headers->m_heap, headers->m_mime, URL_SCHEME_HTTPS, URL_LEN_HTTPS);
+      }
+    } else {
+      ink_abort("initialize HTTP/2 pseudo-headers");
+      return PARSE_RESULT_ERROR;
+    }
+
+    // :authority
+    if (MIMEField *field = headers->field_find(HTTP2_VALUE_AUTHORITY, HTTP2_LEN_AUTHORITY); field != nullptr) {
+      int value_len;
+      const char *value = headers->host_get(&value_len);
+
+      if (headers->is_port_in_header()) {
+        int port            = headers->port_get();
+        char *host_and_port = static_cast<char *>(ats_malloc(value_len + 8));
+        value_len           = snprintf(host_and_port, value_len + 8, "%.*s:%d", value_len, value, port);
+
+        field->value_set(headers->m_heap, headers->m_mime, host_and_port, value_len);
+        ats_free(host_and_port);
+      } else {
+        field->value_set(headers->m_heap, headers->m_mime, value, value_len);
+      }
+    } else {
+      ink_abort("initialize HTTP/2 pseudo-headers");
+      return PARSE_RESULT_ERROR;
+    }
 
-  if (http_hdr_type_get(headers->m_http) == HTTP_TYPE_RESPONSE) {
-    // Add ':status' header field
-    char status_str[HTTP2_LEN_STATUS_VALUE_STR + 1];
-    snprintf(status_str, sizeof(status_str), "%d", headers->status_get());
-    MIMEField *status_field = h2_headers->field_create(HTTP2_VALUE_STATUS, HTTP2_LEN_STATUS);
-    status_field->value_set(h2_headers->m_heap, h2_headers->m_mime, status_str, HTTP2_LEN_STATUS_VALUE_STR);
-    h2_headers->field_attach(status_field);
+    // :path
+    if (MIMEField *field = headers->field_find(HTTP2_VALUE_PATH, HTTP2_LEN_PATH); field != nullptr) {
+      int value_len;
+      const char *value = headers->path_get(&value_len);
+      char *path        = static_cast<char *>(ats_malloc(value_len + 1));
+      path[0]           = '/';
+      memcpy(path + 1, value, value_len);
 
-  } else if (http_hdr_type_get(headers->m_http) == HTTP_TYPE_REQUEST) {
-    MIMEField *field;
-    const char *value;
-    int value_len;
+      field->value_set(headers->m_heap, headers->m_mime, path, value_len + 1);
+      ats_free(path);
+    } else {
+      ink_abort("initialize HTTP/2 pseudo-headers");
+      return PARSE_RESULT_ERROR;
+    }
 
-    // Add ':authority' header field
     // TODO: remove host/Host header
-    // [RFC 7540] 8.1.2.3. Clients that generate HTTP/2 requests directly SHOULD use the ":authority" pseudo-header field instead of
-    // the Host header field.
-    field = h2_headers->field_create(HTTP2_VALUE_AUTHORITY, HTTP2_LEN_AUTHORITY);
-    value = headers->host_get(&value_len);
-    if (headers->is_port_in_header()) {
-      int port            = headers->port_get();
-      char *host_and_port = static_cast<char *>(ats_malloc(value_len + 8));
-      value_len           = snprintf(host_and_port, value_len + 8, "%.*s:%d", value_len, value, port);
-      field->value_set(h2_headers->m_heap, h2_headers->m_mime, host_and_port, value_len);
-      ats_free(host_and_port);
+    // [RFC 7540] 8.1.2.3. Clients that generate HTTP/2 requests directly SHOULD use the ":authority" pseudo-header field instead
+    // of the Host header field.
+
+    break;
+  }
+  case HTTP_TYPE_RESPONSE: {
+    // :status
+    if (MIMEField *field = headers->field_find(HTTP2_VALUE_STATUS, HTTP2_LEN_STATUS); field != nullptr) {
+      // ink_small_itoa() requires 5+ buffer length
+      char status_str[HTTP2_LEN_STATUS_VALUE_STR + 3];
+      mime_format_int(status_str, headers->status_get(), sizeof(status_str));
+
+      field->value_set(headers->m_heap, headers->m_mime, status_str, HTTP2_LEN_STATUS_VALUE_STR);
     } else {
-      field->value_set(h2_headers->m_heap, h2_headers->m_mime, value, value_len);
+      ink_abort("initialize HTTP/2 pseudo-headers");
+      return PARSE_RESULT_ERROR;
     }
-    h2_headers->field_attach(field);
-
-    // Add ':method' header field
-    field = h2_headers->field_create(HTTP2_VALUE_METHOD, HTTP2_LEN_METHOD);
-    value = headers->method_get(&value_len);
-    field->value_set(h2_headers->m_heap, h2_headers->m_mime, value, value_len);
-    h2_headers->field_attach(field);
-
-    // Add ':path' header field
-    field      = h2_headers->field_create(HTTP2_VALUE_PATH, HTTP2_LEN_PATH);
-    value      = headers->path_get(&value_len);
-    char *path = static_cast<char *>(ats_malloc(value_len + 1));
-    path[0]    = '/';
-    memcpy(path + 1, value, value_len);
-    field->value_set(h2_headers->m_heap, h2_headers->m_mime, path, value_len + 1);
-    ats_free(path);
-    h2_headers->field_attach(field);
-
-    // Add ':scheme' header field
-    field = h2_headers->field_create(HTTP2_VALUE_SCHEME, HTTP2_LEN_SCHEME);
-    value = headers->scheme_get(&value_len);
-    field->value_set(h2_headers->m_heap, h2_headers->m_mime, value, value_len);
-    h2_headers->field_attach(field);
-  }
-
-  // Copy headers
+    break;
+  }
+  default:
+    ink_abort("HTTP_TYPE_UNKNOWN");
+  }
+
   // Intermediaries SHOULD remove connection-specific header fields.
-  MIMEFieldIter field_iter;
-  for (MIMEField *field = headers->iter_get_first(&field_iter); field != nullptr; field = headers->iter_get_next(&field_iter)) {
-    const char *name;
-    int name_len;
-    const char *value;
-    int value_len;
-    name = field->name_get(&name_len);
-    if ((name_len == MIME_LEN_CONNECTION && strncasecmp(name, MIME_FIELD_CONNECTION, name_len) == 0) ||
-        (name_len == MIME_LEN_KEEP_ALIVE && strncasecmp(name, MIME_FIELD_KEEP_ALIVE, name_len) == 0) ||
-        (name_len == MIME_LEN_PROXY_CONNECTION && strncasecmp(name, MIME_FIELD_PROXY_CONNECTION, name_len) == 0) ||
-        (name_len == MIME_LEN_TRANSFER_ENCODING && strncasecmp(name, MIME_FIELD_TRANSFER_ENCODING, name_len) == 0) ||
-        (name_len == MIME_LEN_UPGRADE && strncasecmp(name, MIME_FIELD_UPGRADE, name_len) == 0)) {
-      continue;
+  for (auto &h : http2_connection_specific_headers) {
+    if (MIMEField *field = headers->field_find(h.name, h.name_len); field != nullptr) {
+      headers->field_delete(field);
     }
-    MIMEField *newfield;
-    name     = field->name_get(&name_len);
-    newfield = h2_headers->field_create(name, name_len);
-    value    = field->value_get(&value_len);
-    newfield->value_set(h2_headers->m_heap, h2_headers->m_mime, value, value_len);
-    h2_headers->field_attach(newfield);
   }
+
+  return PARSE_RESULT_DONE;
 }
 
 Http2ErrorCode
@@ -595,6 +664,7 @@ http2_encode_header_blocks(HTTPHdr *in, uint8_t *out, uint32_t out_len, uint32_t
   if (maximum_table_size == hpack_get_maximum_table_size(handle)) {
     maximum_table_size = -1;
   }
+
   // TODO: It would be better to split Cookie header value
   int64_t result = hpack_encode_header_block(handle, out, out_len, in, maximum_table_size);
   if (result < 0) {
@@ -826,6 +896,27 @@ Http2::init()
                      static_cast<int>(HTTP2_STAT_MAX_PRIORITY_FRAMES_PER_MINUTE_EXCEEDED), RecRawStatSyncSum);
   RecRegisterRawStat(http2_rsb, RECT_PROCESS, HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE_NAME, RECD_INT, RECP_PERSISTENT,
                      static_cast<int>(HTTP2_STAT_INSUFFICIENT_AVG_WINDOW_UPDATE), RecRawStatSyncSum);
+
+  http2_init();
+}
+
+/**
+  mime_init() needs to be called
+ */
+void
+http2_init()
+{
+  ink_assert(MIME_FIELD_CONNECTION != nullptr);
+  ink_assert(MIME_FIELD_KEEP_ALIVE != nullptr);
+  ink_assert(MIME_FIELD_PROXY_CONNECTION != nullptr);
+  ink_assert(MIME_FIELD_TRANSFER_ENCODING != nullptr);
+  ink_assert(MIME_FIELD_UPGRADE != nullptr);
+
+  http2_connection_specific_headers[0] = {MIME_FIELD_CONNECTION, MIME_LEN_CONNECTION};
+  http2_connection_specific_headers[1] = {MIME_FIELD_KEEP_ALIVE, MIME_LEN_KEEP_ALIVE};
+  http2_connection_specific_headers[2] = {MIME_FIELD_PROXY_CONNECTION, MIME_LEN_PROXY_CONNECTION};
+  http2_connection_specific_headers[3] = {MIME_FIELD_TRANSFER_ENCODING, MIME_LEN_TRANSFER_ENCODING};
+  http2_connection_specific_headers[4] = {MIME_FIELD_UPGRADE, MIME_LEN_UPGRADE};
 }
 
 #if TS_HAS_TESTS
diff --git a/proxy/http2/HTTP2.h b/proxy/http2/HTTP2.h
index 7bcc75f..9661645 100644
--- a/proxy/http2/HTTP2.h
+++ b/proxy/http2/HTTP2.h
@@ -43,6 +43,18 @@ typedef int32_t Http2WindowSize;
 extern const char *const HTTP2_CONNECTION_PREFACE;
 const size_t HTTP2_CONNECTION_PREFACE_LEN = 24;
 
+extern const char *HTTP2_VALUE_SCHEME;
+extern const char *HTTP2_VALUE_METHOD;
+extern const char *HTTP2_VALUE_AUTHORITY;
+extern const char *HTTP2_VALUE_PATH;
+extern const char *HTTP2_VALUE_STATUS;
+
+extern const unsigned HTTP2_LEN_SCHEME;
+extern const unsigned HTTP2_LEN_METHOD;
+extern const unsigned HTTP2_LEN_AUTHORITY;
+extern const unsigned HTTP2_LEN_PATH;
+extern const unsigned HTTP2_LEN_STATUS;
+
 const size_t HTTP2_FRAME_HEADER_LEN       = 9;
 const size_t HTTP2_DATA_PADLEN_LEN        = 1;
 const size_t HTTP2_HEADERS_PADLEN_LEN     = 1;
@@ -355,7 +367,9 @@ Http2ErrorCode http2_decode_header_blocks(HTTPHdr *, const uint8_t *, const uint
 Http2ErrorCode http2_encode_header_blocks(HTTPHdr *, uint8_t *, uint32_t, uint32_t *, HpackHandle &, int32_t);
 
 ParseResult http2_convert_header_from_2_to_1_1(HTTPHdr *);
-void http2_generate_h2_header_from_1_1(HTTPHdr *headers, HTTPHdr *h2_headers);
+ParseResult http2_convert_header_from_1_1_to_2(HTTPHdr *);
+void http2_init_pseudo_headers(HTTPHdr &);
+void http2_init();
 
 // Not sure where else to put this, but figure this is as good of a start as
 // anything else.
diff --git a/proxy/http2/Http2ConnectionState.cc b/proxy/http2/Http2ConnectionState.cc
index e295318..23beaff 100644
--- a/proxy/http2/Http2ConnectionState.cc
+++ b/proxy/http2/Http2ConnectionState.cc
@@ -28,6 +28,9 @@
 #include "Http2Frame.h"
 #include "Http2DebugNames.h"
 #include "HttpDebugNames.h"
+
+#include "tscpp/util/PostScript.h"
+
 #include <sstream>
 #include <numeric>
 
@@ -1633,25 +1636,22 @@ Http2ConnectionState::send_headers_frame(Http2Stream *stream)
   uint32_t header_blocks_size = 0;
   int payload_length          = 0;
   uint8_t flags               = 0x00;
-  HTTPHdr *resp_header        = &stream->response_header;
 
   Http2StreamDebug(ua_session, stream->get_id(), "Send HEADERS frame");
 
-  HTTPHdr h2_hdr;
-  http2_generate_h2_header_from_1_1(resp_header, &h2_hdr);
+  HTTPHdr *resp_hdr = &stream->response_header;
+  http2_convert_header_from_1_1_to_2(resp_hdr);
 
-  buf_len = resp_header->length_get() * 2; // Make it double just in case
+  buf_len = resp_hdr->length_get() * 2; // Make it double just in case
   buf     = static_cast<uint8_t *>(ats_malloc(buf_len));
   if (buf == nullptr) {
-    h2_hdr.destroy();
     return;
   }
 
   stream->mark_milestone(Http2StreamMilestone::START_ENCODE_HEADERS);
-  Http2ErrorCode result = http2_encode_header_blocks(&h2_hdr, buf, buf_len, &header_blocks_size, *(this->remote_hpack_handle),
+  Http2ErrorCode result = http2_encode_header_blocks(resp_hdr, buf, buf_len, &header_blocks_size, *(this->remote_hpack_handle),
                                                      client_settings.get(HTTP2_SETTINGS_HEADER_TABLE_SIZE));
   if (result != Http2ErrorCode::HTTP2_ERROR_NO_ERROR) {
-    h2_hdr.destroy();
     ats_free(buf);
     return;
   }
@@ -1660,8 +1660,8 @@ Http2ConnectionState::send_headers_frame(Http2Stream *stream)
   if (header_blocks_size <= static_cast<uint32_t>(BUFFER_SIZE_FOR_INDEX(buffer_size_index[HTTP2_FRAME_TYPE_HEADERS]))) {
     payload_length = header_blocks_size;
     flags |= HTTP2_FLAGS_HEADERS_END_HEADERS;
-    if ((h2_hdr.presence(MIME_PRESENCE_CONTENT_LENGTH) && h2_hdr.get_content_length() == 0) ||
-        (!resp_header->expect_final_response() && stream->is_write_vio_done())) {
+    if ((resp_hdr->presence(MIME_PRESENCE_CONTENT_LENGTH) && resp_hdr->get_content_length() == 0) ||
+        (!resp_hdr->expect_final_response() && stream->is_write_vio_done())) {
       Http2StreamDebug(ua_session, stream->get_id(), "END_STREAM");
       flags |= HTTP2_FLAGS_HEADERS_END_STREAM;
       stream->send_end_stream = true;
@@ -1679,7 +1679,6 @@ Http2ConnectionState::send_headers_frame(Http2Stream *stream)
       fini_event = this_ethread()->schedule_imm_local((Continuation *)this, HTTP2_SESSION_EVENT_FINI);
     }
 
-    h2_hdr.destroy();
     ats_free(buf);
     return;
   }
@@ -1704,14 +1703,12 @@ Http2ConnectionState::send_headers_frame(Http2Stream *stream)
     sent += payload_length;
   }
 
-  h2_hdr.destroy();
   ats_free(buf);
 }
 
 bool
 Http2ConnectionState::send_push_promise_frame(Http2Stream *stream, URL &url, const MIMEField *accept_encoding)
 {
-  HTTPHdr h1_hdr, h2_hdr;
   uint8_t *buf                = nullptr;
   uint32_t buf_len            = 0;
   uint32_t header_blocks_size = 0;
@@ -1724,37 +1721,35 @@ Http2ConnectionState::send_push_promise_frame(Http2Stream *stream, URL &url, con
 
   Http2StreamDebug(ua_session, stream->get_id(), "Send PUSH_PROMISE frame");
 
-  h1_hdr.create(HTTP_TYPE_REQUEST);
-  h1_hdr.url_set(&url);
-  h1_hdr.method_set("GET", 3);
+  HTTPHdr hdr;
+  ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); });
+  hdr.create(HTTP_TYPE_REQUEST);
+  http2_init_pseudo_headers(hdr);
+  hdr.url_set(&url);
+  hdr.method_set(HTTP_METHOD_GET, HTTP_LEN_GET);
+
   if (accept_encoding != nullptr) {
-    MIMEField *f;
-    const char *name;
     int name_len;
-    const char *value;
-    int value_len;
+    const char *name = accept_encoding->name_get(&name_len);
+    MIMEField *f     = hdr.field_create(name, name_len);
 
-    name  = accept_encoding->name_get(&name_len);
-    f     = h1_hdr.field_create(name, name_len);
-    value = accept_encoding->value_get(&value_len);
-    f->value_set(h1_hdr.m_heap, h1_hdr.m_mime, value, value_len);
+    int value_len;
+    const char *value = accept_encoding->value_get(&value_len);
+    f->value_set(hdr.m_heap, hdr.m_mime, value, value_len);
 
-    h1_hdr.field_attach(f);
+    hdr.field_attach(f);
   }
 
-  http2_generate_h2_header_from_1_1(&h1_hdr, &h2_hdr);
+  http2_convert_header_from_1_1_to_2(&hdr);
 
-  buf_len = h1_hdr.length_get() * 2; // Make it double just in case
-  h1_hdr.destroy();
-  buf = static_cast<uint8_t *>(ats_malloc(buf_len));
+  buf_len = hdr.length_get() * 2; // Make it double just in case
+  buf     = static_cast<uint8_t *>(ats_malloc(buf_len));
   if (buf == nullptr) {
-    h2_hdr.destroy();
     return false;
   }
-  Http2ErrorCode result = http2_encode_header_blocks(&h2_hdr, buf, buf_len, &header_blocks_size, *(this->remote_hpack_handle),
+  Http2ErrorCode result = http2_encode_header_blocks(&hdr, buf, buf_len, &header_blocks_size, *(this->remote_hpack_handle),
                                                      client_settings.get(HTTP2_SETTINGS_HEADER_TABLE_SIZE));
   if (result != Http2ErrorCode::HTTP2_ERROR_NO_ERROR) {
-    h2_hdr.destroy();
     ats_free(buf);
     return false;
   }
@@ -1796,7 +1791,6 @@ Http2ConnectionState::send_push_promise_frame(Http2Stream *stream, URL &url, con
   Http2Error error(Http2ErrorClass::HTTP2_ERROR_CLASS_NONE);
   stream = this->create_stream(id, error);
   if (!stream) {
-    h2_hdr.destroy();
     return false;
   }
 
@@ -1815,11 +1809,10 @@ Http2ConnectionState::send_push_promise_frame(Http2Stream *stream, URL &url, con
     }
   }
   stream->change_state(HTTP2_FRAME_TYPE_PUSH_PROMISE, HTTP2_FLAGS_PUSH_PROMISE_END_HEADERS);
-  stream->set_request_headers(h2_hdr);
+  stream->set_request_headers(hdr);
   stream->new_transaction();
   stream->send_request(*this);
 
-  h2_hdr.destroy();
   return true;
 }
 
diff --git a/proxy/http2/Http2Stream.cc b/proxy/http2/Http2Stream.cc
index ddf887c..eef4460 100644
--- a/proxy/http2/Http2Stream.cc
+++ b/proxy/http2/Http2Stream.cc
@@ -54,9 +54,12 @@ Http2Stream::init(Http2StreamId sid, ssize_t initial_rwnd)
   this->_server_rwnd = Http2::initial_window_size;
 
   this->_reader = this->_request_buffer.alloc_reader();
-  // FIXME: Are you sure? every "stream" needs request_header?
+
   _req_header.create(HTTP_TYPE_REQUEST);
   response_header.create(HTTP_TYPE_RESPONSE);
+  // TODO: init _req_header instead of response_header if this Http2Stream is outgoing
+  http2_init_pseudo_headers(response_header);
+
   http_parser_init(&http_parser);
 }
 
@@ -624,6 +627,7 @@ Http2Stream::update_write_request(IOBufferReader *buf_reader, int64_t write_len,
         this->response_header_done = false;
         response_header.destroy();
         response_header.create(HTTP_TYPE_RESPONSE);
+        http2_init_pseudo_headers(response_header);
         http_parser_clear(&http_parser);
         http_parser_init(&http_parser);
       }
diff --git a/proxy/http2/Makefile.am b/proxy/http2/Makefile.am
index 7f77e90..100909b 100644
--- a/proxy/http2/Makefile.am
+++ b/proxy/http2/Makefile.am
@@ -84,6 +84,7 @@ test_libhttp2_CPPFLAGS = $(AM_CPPFLAGS)\
 	-I$(abs_top_srcdir)/tests/include
 
 test_libhttp2_SOURCES = \
+	unit_tests/test_HTTP2.cc \
 	unit_tests/test_Http2Frame.cc \
 	unit_tests/main.cc
 
diff --git a/proxy/http2/unit_tests/test_HTTP2.cc b/proxy/http2/unit_tests/test_HTTP2.cc
new file mode 100644
index 0000000..e67f50e
--- /dev/null
+++ b/proxy/http2/unit_tests/test_HTTP2.cc
@@ -0,0 +1,169 @@
+/** @file
+
+    Unit tests for HTTP2
+
+    @section license License
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+*/
+
+#include "catch.hpp"
+
+#include "HTTP2.h"
+
+#include "tscpp/util/PostScript.h"
+
+TEST_CASE("Convert HTTPHdr", "[HTTP2]")
+{
+  url_init();
+  mime_init();
+  http_init();
+  http2_init();
+
+  HTTPParser parser;
+  ts::PostScript parser_defer([&]() -> void { http_parser_clear(&parser); });
+  http_parser_init(&parser);
+
+  SECTION("request")
+  {
+    const char request[] = "GET /index.html HTTP/1.1\r\n"
+                           "Host: trafficserver.apache.org\r\n"
+                           "User-Agent: foobar\r\n"
+                           "\r\n";
+
+    HTTPHdr hdr_1;
+    ts::PostScript hdr_1_defer([&]() -> void { hdr_1.destroy(); });
+    hdr_1.create(HTTP_TYPE_REQUEST);
+    http2_init_pseudo_headers(hdr_1);
+
+    // parse
+    const char *start = request;
+    const char *end   = request + sizeof(request) - 1;
+    hdr_1.parse_req(&parser, &start, end, true);
+
+    // convert to HTTP/2
+    http2_convert_header_from_1_1_to_2(&hdr_1);
+
+    // check pseudo headers
+    // :method
+    {
+      MIMEField *f = hdr_1.field_find(HTTP2_VALUE_METHOD, HTTP2_LEN_METHOD);
+      REQUIRE(f != nullptr);
+      std::string_view v = f->value_get();
+      CHECK(v.compare("GET") == 0);
+    }
+
+    // :scheme
+    {
+      MIMEField *f = hdr_1.field_find(HTTP2_VALUE_SCHEME, HTTP2_LEN_SCHEME);
+      REQUIRE(f != nullptr);
+      std::string_view v = f->value_get();
+      CHECK(v.compare("https") == 0);
+    }
+
+    // :authority
+    {
+      MIMEField *f = hdr_1.field_find(HTTP2_VALUE_AUTHORITY, HTTP2_LEN_AUTHORITY);
+      REQUIRE(f != nullptr);
+      std::string_view v = f->value_get();
+      CHECK(v.compare("trafficserver.apache.org") == 0);
+    }
+
+    // :path
+    {
+      MIMEField *f = hdr_1.field_find(HTTP2_VALUE_PATH, HTTP2_LEN_PATH);
+      REQUIRE(f != nullptr);
+      std::string_view v = f->value_get();
+      CHECK(v.compare("/index.html") == 0);
+    }
+
+    // convert to HTTP/1.1
+    HTTPHdr hdr_2;
+    ts::PostScript hdr_2_defer([&]() -> void { hdr_2.destroy(); });
+    hdr_2.create(HTTP_TYPE_REQUEST);
+    hdr_2.copy(&hdr_1);
+
+    http2_convert_header_from_2_to_1_1(&hdr_2);
+
+    // dump
+    char buf[128]  = {0};
+    int bufindex   = 0;
+    int dumpoffset = 0;
+
+    hdr_2.print(buf, sizeof(buf), &bufindex, &dumpoffset);
+
+    // check
+    CHECK_THAT(buf, Catch::StartsWith("GET https://trafficserver.apache.org/index.html HTTP/1.1\r\n"
+                                      "Host: trafficserver.apache.org\r\n"
+                                      "User-Agent: foobar\r\n"
+                                      "\r\n"));
+  }
+
+  SECTION("response")
+  {
+    const char response[] = "HTTP/1.1 200 OK\r\n"
+                            "Connection: close\r\n"
+                            "\r\n";
+
+    HTTPHdr hdr_1;
+    ts::PostScript hdr_1_defer([&]() -> void { hdr_1.destroy(); });
+    hdr_1.create(HTTP_TYPE_RESPONSE);
+    http2_init_pseudo_headers(hdr_1);
+
+    // parse
+    const char *start = response;
+    const char *end   = response + sizeof(response) - 1;
+    hdr_1.parse_resp(&parser, &start, end, true);
+
+    // convert to HTTP/2
+    http2_convert_header_from_1_1_to_2(&hdr_1);
+
+    // check pseudo headers
+    // :status
+    {
+      MIMEField *f = hdr_1.field_find(HTTP2_VALUE_STATUS, HTTP2_LEN_STATUS);
+      REQUIRE(f != nullptr);
+      std::string_view v = f->value_get();
+      CHECK(v.compare("200") == 0);
+    }
+
+    // no connection header
+    {
+      MIMEField *f = hdr_1.field_find(MIME_FIELD_CONNECTION, MIME_LEN_CONNECTION);
+      CHECK(f == nullptr);
+    }
+
+    // convert to HTTP/1.1
+    HTTPHdr hdr_2;
+    ts::PostScript hdr_2_defer([&]() -> void { hdr_2.destroy(); });
+    hdr_2.create(HTTP_TYPE_REQUEST);
+    hdr_2.copy(&hdr_1);
+
+    http2_convert_header_from_2_to_1_1(&hdr_2);
+
+    // dump
+    char buf[128]  = {0};
+    int bufindex   = 0;
+    int dumpoffset = 0;
+
+    hdr_2.print(buf, sizeof(buf), &bufindex, &dumpoffset);
+
+    // check
+    REQUIRE(bufindex > 0);
+    CHECK_THAT(buf, Catch::StartsWith("HTTP/1.1 200 OK\r\n\r\n"));
+  }
+}
diff --git a/proxy/http3/Http3HeaderFramer.cc b/proxy/http3/Http3HeaderFramer.cc
index b3ac791..b69f298 100644
--- a/proxy/http3/Http3HeaderFramer.cc
+++ b/proxy/http3/Http3HeaderFramer.cc
@@ -71,9 +71,9 @@ Http3HeaderFramer::is_done() const
 }
 
 void
-Http3HeaderFramer::_convert_header_from_1_1_to_3(HTTPHdr *h3_hdrs, HTTPHdr *h1_hdrs)
+Http3HeaderFramer::_convert_header_from_1_1_to_3(HTTPHdr *hdrs)
 {
-  http2_generate_h2_header_from_1_1(h1_hdrs, h3_hdrs);
+  http2_convert_header_from_1_1_to_2(hdrs);
 }
 
 void
@@ -85,22 +85,23 @@ Http3HeaderFramer::_generate_header_block()
 
   if (this->_transaction->direction() == NET_VCONNECTION_OUT) {
     this->_header.create(HTTP_TYPE_REQUEST);
+    http2_init_pseudo_headers(this->_header);
     parse_result = this->_header.parse_req(&this->_http_parser, this->_source_vio->get_reader(), &bytes_used, false);
   } else {
     this->_header.create(HTTP_TYPE_RESPONSE);
+    http2_init_pseudo_headers(this->_header);
     parse_result = this->_header.parse_resp(&this->_http_parser, this->_source_vio->get_reader(), &bytes_used, false);
   }
   this->_source_vio->ndone += bytes_used;
 
   switch (parse_result) {
   case PARSE_RESULT_DONE: {
-    HTTPHdr h3_hdr;
-    this->_convert_header_from_1_1_to_3(&h3_hdr, &this->_header);
+    this->_convert_header_from_1_1_to_3(&this->_header);
 
     this->_header_block        = new_MIOBuffer(BUFFER_SIZE_INDEX_32K);
     this->_header_block_reader = this->_header_block->alloc_reader();
 
-    this->_qpack->encode(this->_stream_id, h3_hdr, this->_header_block, this->_header_block_len);
+    this->_qpack->encode(this->_stream_id, this->_header, this->_header_block, this->_header_block_len);
     break;
   }
   case PARSE_RESULT_CONT:
diff --git a/proxy/http3/Http3HeaderFramer.h b/proxy/http3/Http3HeaderFramer.h
index 55ce586..2e70338 100644
--- a/proxy/http3/Http3HeaderFramer.h
+++ b/proxy/http3/Http3HeaderFramer.h
@@ -55,6 +55,6 @@ private:
   HTTPParser _http_parser;
   HTTPHdr _header;
 
-  void _convert_header_from_1_1_to_3(HTTPHdr *h3_hdrs, HTTPHdr *h1_hdrs);
+  void _convert_header_from_1_1_to_3(HTTPHdr *hdrs);
   void _generate_header_block();
 };