You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ma...@apache.org on 2021/07/30 00:33:23 UTC

[trafficserver] branch master updated: Merge quic-latest into master (#8010)

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

maskit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 9590447  Merge quic-latest into master (#8010)
9590447 is described below

commit 95904475eaaaa1af4b3566dd6e3fb80256d842e6
Author: Masakazu Kitajo <ma...@apache.org>
AuthorDate: Fri Jul 30 09:33:15 2021 +0900

    Merge quic-latest into master (#8010)
    
    Squashed commit of the following:
    
    commit d232a12ec5ae461235f4a4d6f7c7644d05651aed
    Merge: 837bd0e41 2edeae477
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Tue Jun 29 15:41:34 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          reuse multiple times (#7992)
          Test bad request behavior (#7884)
          Fix BoringSSL build (#8001)
          Update TSHttpTxnAborted API to distinguish client/server aborts (#7901)
          Enforce case for well known methods (#7886)
          Add null checks for http_load (#7995)
    
    commit 837bd0e413c27b4f2132864c9a0a377a45fabaf5
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Mon Jun 28 15:11:03 2021 +0900
    
        Fix unit tests for QUICStreamState
    
    commit c5bb9e0dd41cba2198c5632f85f89f9b343eee34
    Merge: 0a63fa977 202b2505c
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Mon Jun 28 10:02:54 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Implement TLSBasicSupport for QUICNetVC (#7959)
          Reload server session inactivity timeout before placing a session into the pool (#7618)
          Use OpeSSL EVP API if SHA1 API is unavailable  (cache_promote) (#7447)
          Cleanup: Get rid of HTTP2_SESSION_EVENT_RECV (#7879)
          Timing and permissions update for regex_revalidate test (#7998)
          limit m_current_range to max value in RangeTransform (#4843)
          Allow to TLS handshake to error out on TSVConnReenable (#7994)
          Cleanup: Get rid of HTTP2_SESSION_EVENT_INIT (#7878)
          Add hook for loading certificate and key data from plugin  (#6609)
          Doc: Now's Minute invocation error (#7990)
          Fix typo in configure.ac (#7993)
    
    commit 0a63fa977f97919143f45508f1f6c5b656324d80
    Merge: 312cf393c bd93f2a40
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Fri Jun 25 14:34:55 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Don't rely on SSLNetVC when HttpSM gathers info about SSL (#7961)
          conf_remap: demote 'Invalid configuration' to warning (#7991)
          Cleans up the code bit, including milliseconds consistency (#7989)
          Pass through expect header and handle 100-continue response (#7962)
          Treat TRACE with body as bad request (#7905)
          Thread safe Mersenne Twister 64 using c++11 (#7859)
          ESI plugin documentation updates. (#7970)
          Add log name configuration and stderr/stdout support. (#7937)
          Cleanup: Constify MIMEHdr (#7949)
          Fixed compile error with Linux AIO unit test (#7958)
          Note YAML parser library bug, and work-around, in documentation. (#7963)
          Ensure that the content-length value is only digits (#7964)
          String the url fragment for outgoing requests (#7966)
          Fix for HTTP/2 frames (#7965)
          Improve parsing error messages for strategies.yaml. (#7948)
          fix the scheme of h2 0rtt tests (#7957)
          Fix double test flakiness due to EOS/TXN_CLOSE race (#7956)
          Use proxy.config.log.hostname for rotated log filenames (#7943)
          Fixed memory leak in the QUIC stream manager (#7951)
          Fixup TS_USE_LINUX_NATIVE_AIO AIO_MODE_NATIVE (#7832)
          Update GitHub stale action to auto close old PRs (#7952)
          Revert "Do not invalidate cached resources upon error responses to unsafe methods (#7864)" (#7954)
          regex_revalidate: add stats for miss/stale counts (#7950)
          Do not invalidate cached resources upon error responses to unsafe methods (#7864)
          Add an HTTP/2 304 "Not Modified" AuTest. (#7882)
          regex_revalidate: optionally retain rule epoch state across restarts (#7939)
          Fixed memory leak in QUIC ack frame unit test (#7947)
          cache_promote: Don't promote on uncacheable requests (#7942)
          Fix dynamic-stack-buffer-overflow of cachekey plugin (#7945)
          Compilation error fixes for QUIC unit tests (#7944)
          Adds bytes counting as a trigger to the cache_promote LRU (#7765)
          Add a JSON schema for strategies.yaml (#7932)
          Remove second call to TRANSACT_RETURN while handling cache write lock (#7873)
          Close connection after every bad request for HTTP/1.1 (#7885)
          Pin Sphinx to 3.x to unblock `make html` (#7940)
          Add support for Remap rule hit stats (#7936)
          Remove scrap log object dead code (#7935)
          Add STL forward iterators to DLL container. (#7934)
          Add log SQUID code testing to redirect.test.py Au test. (#7870)
          Fix race condition on server session state (#7921)
          regex_reval: bug where rule type is always reported as the first (#7928)
          Remove duplicate entry in overridable txn vars. (#7930)
          Satisfy ci/jenkins/bin/clang-format.sh (#7929)
          Add a basic Au test using strategies.yaml, with consistent hashing. (#7911)
          Add a chunked negative revalidating test. (#7907)
          Ensure that URL components are valid when alternate eviction is logged (#7924)
          fix grammar (#7927)
          AuTest: Enable h2spec generic test cases (#7926)
          Adjust vc read errors (#7923)
          Remove bucket search from IntrusiveHashMap::erase (#7848)
          Ensure TS_VCONN_CLOSE_HOOK hook is called during TS_EVENT_VCONN_CLOSE. (#7913)
          Update docs languages file to add 9.1.x for en and ja (#7917)
          * Adds a new peering ring mode to next hop selection strategies. (#7897)
          Add Au test for strategies.yaml, with consistent hashing, with fallover. (#7914)
          Make HttpSM server reference a Transaction instead of a Session (#7849)
          Set accept_options of Http1Transaction in Http1ClientSession::new_connection() (#7894)
          Reset Http1Transaction before adding vc to keep_alive_queue (#7892)
          Add dead server policy control and metric. Improve messages. (#7757)
          Ensure the HTTP protion of the protocol string is upper case (#7904)
          Fixed spelling mistakes in the docs (#7896)
          add MISS capability to the regex_revalidate plugin (#7899)
          docs: fix capitalization of Linux (#7898)
          Redirect - Make TS to honour the number_of_redirections configuration value (#7867)
          Clean up producer more regularly (#7386)
          Fix crash in open_close_h2 (#7586)
          Cleanup Http2ClientSession SessionHandler (#7876)
          Enforce HTTP parsing restrictions on HTTP versions supported (#7875)
          Do not delete the continuation twice (#7862)
          Cleanup: refer Http2ClientSession::mutex (#7853)
          Autest - Proxy Verifier Extension, add context template $-base string substitution in the replay file. (#7866)
          Fixed some spelling mistakes in comments (#7869)
          Fixed ASAN issues with MMH test (#7868)
          Cleanup: Move member functions defined inside of class definitions of Http2ConnectionState & Http2ConnectionSettings (#7854)
          Add URI Signing cdnistd Claim Implementation (#7822)
          Adds a new --enable-all-asserts configure option (#7858)
          Unifdef test code for MMH and moved it into its own test file (#7841)
          Clean up lua plugin doc for overridable configurations (#7844)
          Save and propagate epoll network error (#7809)
          Add method to write an IpAddr value to a sockaddr. (#7821)
          Add proxy.config.http.max_proxy_cycles (#7657)
          Update NextHop strategies so that unavailable server retry codes (#7837)
          generator: allow for POST requests (#7635)
          Fixed double declaration types for log buffer tracking (#7847)
          Extra braces for clang 5 / ubuntu 16.04 on array initialization (#7842)
    
         Conflicts:
        	iocore/net/quic/QUICStreamFactory.cc
    
    commit 312cf393c170bbf1ee6c945907868137197afdfa
    Merge: f90e8dde9 5cdc1459f
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Mon May 17 10:07:42 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Get rid of code for OpenSSL that has old QUIC API (#7599)
          Fixed warning in gcc 11 about array not being initalized (#7840)
          Don't call next next dup on destroyed mime field mloc. (#7833)
          build_h3_tools: use OpenSSL_1_1_1k+quic (#7836)
          Address assert on captive_action (#7807)
          Fix so EOS are delivered to sessions in the pool (#7828)
          Fix a format specifier for size_t (#7830)
          Fix stall on sending response for request with trailer header (#7831)
          Simplification dir_init_done (#7817)
          Remove unused member from HttpSM (#7835)
          AuTest: use exteneded help output to determin curl feature support (#7834)
          Apply fmt compile time argument checking to log functions (#7829)
          Adds new X-Cache-Info header to the xdebug plugin (#7784)
          Cleanup: Remove unused members of Http2Stream (#7813)
          Cleanup: unused functions of Http2ClientSession (#7812)
          Cancel cross_thread_event on clear_io_events (#7815)
          Cleanup: Remove a meaningless Http2Stream::do_io_close() call (#7814)
          Eliminate next dup call using stale mime field mloc is s3_auth plugin. (#7825)
          NetEvent cleanup - replace #define with constexpr (#7804)
          fix origin session related crashes (#7808)
          Update HTTP version info in HostDB on new outbound connection (#7816)
          Remove a redundant argument (#7811)
          SSL Cert lookup using PP dest ip when ProxyProtocol is enabled (#7802)
          Fix MLoc assert caused by s3auth (#7790)
          Fix cpu utilization problem in session cache (#7719)
          Fix to cookie_remap.cc tp avoid Intel compiler warning. (#7792)
          TSHttpTxnCacheDiskPathGet - tighten up the code a bit. (#7806)
          Doc: tcpinfo plugin table formatting (#7805)
          fix DNS spike issue for TCP_RETRY mode (#7307)
          Adds new TS API TSHttpTxnCacheDiskPathGet (#7783)
          tests: Fixes spelling (#7789)
          Traffic Dump: Add an HTTP/3 AuTest (#7758)
          use sendmsg and recvmsg (#7793)
          HTTP: clean up the http_hdr_describe format error (#7797)
          Fixes an issue where next hop unit tests crash when run on macOS. (#7787)
          Apply log throttling to HTTP/2 session error rate messages (#7772)
          Cleans up uninitialized warning in LogMessage.cc (#7788)
          Short circuit remap reload when a valid remap file is not specified (#7782)
          DNS: Clean up argument passing to DNS queries. (#7778)
          Remove extra verify-callback (#7540)
          Augment test cases for tls_verify_override test (#7736)
          Make when_to_revalidate setting available on HTTPS (#7753)
          Add traffic_server command line option for debugging in Au test. (#7762)
          Test: Update tls_partial_blind_tunnel to have a nameserver. (#7773)
          Test: update tls_forward_nonhttp to have a nameserver. (#7774)
          Test: add nameserver to log-filter test. (#7776)
          BWF: Add support for std::error_code. (#7777)
          Test: add nameserver to log-field test. (#7779)
          Test: add nameserver to regex_remap test. (#7775)
          Elevate privileges for traffic_manager during SSL cert reload (#7770)
          Clean up HTTP version processing (#7766)
          Remove proxy.config.http.down_server.abort_threshold (#7748)
          Remove undocumented keepalive_internal_vc setting (#7693)
          doc: header_rewrite random function not inclusive (#7760)
          Experimental Cache fill plugin (#7470)
          Remove references to removed options (#7756)
          Propagate TLS errors (#7714)
          AuTest extension: check for unrecognized configurations (#7752)
          Fixes errors in the strategies.yaml documentation. (#7745)
          Updates to Nexthop strategies to limit the number of simultaneous (#7744)
          Fixes Issue #7739 - Next hop strategy with bad 'to' URL causes TS crash. (#7749)
          header_rewrite: Various fixes for MaxMind support (#7746)
          Remove unused variable is_revalidation_necessary (#7747)
          Fix simple remapping in regex_remap plugin. (#7718)
          Adding DNS TTL AuTests. (#7742)
          Add a chunked disabled test. (#7743)
          Fix monitor threads in lib records to exit on system shutdown. (#7731)
          Add overload for memcpy to take a destination buffer and source string_view / TextView (#7732)
          Test: Add nameserver to TLS tunnel forward test. (#7733)
          AIO_NOT_IN_PROGRESS should not be 0 (#7734)
          if transaction status non-success, bypass intercept plugin (#7724)
          ink_utf8_to_latin1 is not defined, removing declaration (#7737)
          Fix build on FreeBSD 13 (#7730)
          Update VSCode CPP Standard (#7723)
          Updating to use Proxy Verifier 2.2.0 (#7729)
          header_rewrite: Allow for relative path to geo database files (#7727)
          Override proxy.config.ssl.client.sni_policy from sni.yaml (#7703)
          compress.test.py: Reference config file from Test.RunDirectory (#7725)
          Ran clang-tidy over the code (#7708)
          Deny unknown transfer encoding values (#7694)
          Fix doc for http2.no_activity_timeout_in (#7721)
          Add DynamicStats (#7704)
          header_rewrite: allow for use of maxminddb as source of geo truth (#7695)
          Include in parentselectdefs.h in install target (#7713)
          uri_signing: fix warning which affects ubuntu:20.04 builds (#7717)
          Increase the maximum slice block size from 32MB to 128MB (#7709)
    
    commit f90e8dde99564ff4270f2ae63e8592b0948b6130
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Tue Jan 12 12:21:51 2021 +0900
    
        Add QUICStreamStateListener
    
    commit f66646cb1907f7079eaf41aa81da9705934eff18
    Merge: be9837c03 9f9594fd3
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Sat Apr 17 13:57:50 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Fix ALPN support on QUIC connections (#7593)
          fix mem leak in session cache (#7707)
          Parent Select Plugin (#7467)
          Add new TS API function TSUrlRawPortGet. (#7568)
          Add NixOS support (#7697)
          Remove support for --enable-remote-cov-commit (#7700)
          Remove configure-time loopback interface detection (#7702)
          Add sqpv log field for server protocol (#7680)
          Call do_io_close instead of HTTP2_SESSION_EVENT_FINI handler (#7594)
          Fix a bug in tspush that pushes corrupted content to cache (#7696)
          Automatically marks PRs and issues stale (#7675)
          New rate_limit plugin for simple resource limitations (#7623)
          Remove undefined method HttpSM::perform_nca_cache_action (#7692)
          Remove undefined method HttpSM::setup_client_header_nca (#7691)
          Scalar; Move "tag" struct to be inside the "ts" namespace to avoid collisions. (#7690)
          Rollback LAZY_BUF_ALLOC remove in HttpTunnel (#7583)
          Add class to normalize handling of pending action (#7667)
          Make HTTP/2 Curl AuTest gold files case insensitive (#7683)
          Add STL compliant field iteration to MIMEHdr. - rebase. (#7476)
          Fix use of -mcx16 flag - only use if it compiles cleanly. (#7684)
          Refine connection failure logging and messages and eliminate suprious connection errors (#7580)
          Add close header normalize openclose test (#7679)
          Fix has_consumer_besides_client to deal with no clients (#7685)
          create a new cache status RWW_HIT (#7670)
          Updating to AuTest 1.10.0 (#7682)
          sslheaders AuTest: Skip if plugin does not exist (#7678)
          Add AuTest for Background Fill (#7613)
          Do NOT kill tunnel if it has any consumer besides HT_HTTP_CLIENT (#7641)
          AuTest: address various permissions issues (#7668)
          Adding TCP Info header support to header rewrite (#7516)
          Refine Inline.cc carveout for arm64 darwin builds (#7662)
          Comment why log eviction isn't implemented via a log field. (#7648)
          Fixing Throttler.h for older clang and gcc compilers (#7651)
          Update -with-profile and add some profiling documentation (#7601)
          Use correct default value for verify.server.policy (#7636)
          Update server_response_body_bytes when background fill worked (#7621)
          Remove erroneous manager.log mesg with remap include file reload (#7646)
          Change ROUNDUP from function-like macro to function template. (#7614)
          Document http.default_buffer_water_mark (#7612)
          Add proxy.config.cache.log.alternate.eviction (#7629)
          Fix HttpSessionManager::acquireSession from previous rebase error (#7631)
          Fix tls_client_versions and tls_hooks18 tests (#7645)
          Updating documentation for negative_revalidating_lifetime (#7633)
          Remove reference to client.verify.server from tests and other bits (#7639)
          Add pooled_server_connections metric (#7627)
          Expose URL element methods through HTTPHdr (#7628)
          Add default implementation for allow_half_open (#7630)
          Add thread yeield to avoid busy waiting in LogObject::_checkout_write(). (#7576)
          Add proxy.process.http.background_fill_total_count (#7625)
          statichit: misc. fixes (#7634)
          Remove unused variables (#7626)
          Adding negative revalidating AuTests. (#7620)
          Add failed state to hostdb to better track failing origins (#7291)
          Use standard isdigit library function (#7619)
          Typo in output when forcing kqueue for configure (#7617)
          Implement log throttling (#7279)
          Increase Proxy Verifier caching delay. (#7616)
          Set pcre_malloc/free function pointers in core main() only. (#7608)
    
    commit be9837c03219f2cb9efbd4981d13dbc78294ce51
    Merge: 99ff68fa3 d4fc16f64
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Wed Mar 17 09:38:59 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Fix the connection limit crash while using parents (#7604)
          Remove inline for detail::cache::CacheData::idAddr (#7592)
          Remove UnixNetVConnection::startEvent - not actually called. (#7596)
          Use return values to fix ubuntu release build error (#7591)
          Fix double destuct on Http2Stream termination (#7600)
          Add pointer/reference upcast function that is checked in debug builds. (#7582)
          Call constructors and destructors for H1/2 Session/Transaction via ClassAllocator (#7584)
          Add gold test for remap config .include directive. (#7589)
          Change the default value for verify.server.policy (#7587)
          Build the test library for tls_engine consistently (#7588)
          Generalize ALPN logic (#7555)
          Fix the final consumer write size from unchunked to chunked tunnel (#7577)
          Reactivate accept_no_activity_timeout (#7408)
          Tidy up session/transaction destruction process (#7571)
          Remove ProxyTransaction::set_proxy_ssn (#7567)
          Introduce TLSBasicSupport interface (#7556)
          Cleanup: Rename IOBufferReader of Http2ClientSession (#7569)
          Add a check for compress response, if from server and 304, then check cache for headers instead of the 304 response (#7564)
          Updates the STATUS file with all recent releases (#7566)
          Make Allocator.h less silly (no creepy "proto" object). (#6241)
          Cleanup: Remove unused member of Http2ClientSession (#7570)
          enable origin server session cache by default (#7537)
          Add tscontdestroy when transaction is closed and pacing rate is reset (#7572)
          Remove reference to CoreUtils (#7557)
          Remove unused enums from YamlSNIConfig struct. (#7565)
          Removes deprecated sni.yaml option: disable_h2 (#7547)
          This PR updates parent selection to limit the number of simultaneous (#7485)
          Fix KA header not checking strategy (#7483)
          Get rid of kruft LogObject copy constructor. (#7553)
          For TSHttpHdrEffectiveUrlBufGet(), include scheme for request to server URL. (#7545)
          Adding lower_ support to stats and bonding_slave data points for port status (#7560)
          Change cookie_remap plugin to allow use of pre-remap URL (and components). (#7519)
          check verify policy and properties (#7559)
          Fix parent.config to 504 not 502 on timeout (#7558)
          use SSL_CTX address as part of the lookup key (#7552)
          Add ALPN support on TLS Partial Blind Tunnel (#7511)
          Add server_name option to proxy.config.ssl.client.sni_policy (#7533)
          Fix a crash on origin session reuse (#7543)
          Removes the test plugins from the .spec file / RPM (#7551)
          Convert the inactive_client_timeout test to use Proxy Verifier (#7535)
          Fix ja3_fingerprint configure syntax (#7550)
          Fix asserts in multiplexer plugin. (#7532)
          parse expiration time and reload config at time out (#7281)
          Fix origin_session_reuse test (#7542)
          Fix tls_session_reuse test (#7541)
          Split SSL_CTX initialization logic into small functions (#7434)
          Remove dependency for SSL stuff from P_Net.h (#7531)
          Unify all the connect timeouts into one (#7335)
          Fix lua_states_stats Au test. (#7232)
          origin session reuse (#7479)
          Updating to use Proxy Verifier 2.1.0 (#7534)
          update the session reuse tests (#7529)
    
    commit 99ff68fa395a6e3f8ab2e27e98049ceb00a0a7c8
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Wed Feb 17 11:14:40 2021 +0900
    
        Fix link error
    
    commit c4ad0c071d53fb888d8b2ffac34b848524f4fe68
    Merge: c40d95a91 cd33010ff
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Wed Feb 17 09:56:25 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Select lua context per thread (#7465)
          Fix out of bounds access error in jtest (#7526)
          Disable compiling Inline.cc on macOS (#7389)
          Makes sure the types are correct, avoiding compiler warnings (#7523)
          Move has_request_body to ProxyTransaction (#7499)
          Make the H3 build script work properly on Debian platforms (#7522)
          slice/handleFirstServerHeader: return sooner on requested range errors (#7486)
          Add new log field for negotiated ALPN Protocol ID with the client (#7491)
          Add Outbound PROXY Protocol (v1/v2) Support (#7446)
          Updates the Dockerfile for debian (#7518)
          Disable client inactivity timeout while server is processing POST request (#7309)
          Upgrade Catch.hpp to v2.13.4 (#7464)
          Move reopen_moved_log_files to log flushing thread (#7450)
          replace psutil.pid() with psutil.process_iter() for safer execution (#7515)
          Fix spacing in clang-analyzer.sh script (#7480)
          Fix out of bounds access error in ats_base64_decode (#7490)
          Updated to build lastest versions of Fedora and CentOS docker images (#7505)
          Fix QUIC unit tests build issue on GNU ld (#7496)
          Fix QUIC unit test failures (#7497)
          Fixed build issues with Fedora 34 (#7506)
          Fixing DNS local_ipv* config option (#7507)
          traffic_dump: AuTests to use Proxy Verifier. (#7502)
          Disable ja3 plugin when building with boringssl (#7500)
          Avoid -Warray-bounds on PROXY Protocol Builder (#7488)
          AuTest: Upgrade to Proxy Verifier 2.0.2 (#7493)
          fix certs (#7494)
          Add zlib1g-dev to Debian dependencies in README (#7495)
          Unit Test -  Increase openssl's key size. Place test certs into a common test folder. (#7451)
          Add basic type aliases for std::chrono types to ink_time.h for future use. (#7482)
          traffic_ctl - Fix lookup key for run-root option (#7484)
          update thread config tests (#7370)
          Perf: Replace casecmp with memcmp in HPACK static table lookup (#6521)
          Add PROXY Protocol Builder (#7445)
          Adjust so transfer-encoding header can be treated hop-by-hop (#7473)
          Convert auxkey form 2 uint32_t to 1 uint64_t. (#7350)
          Remove the queuing option from proxy.config.http.per_server.connection (#7302)
          Remove unused function ink_microseconds. (#7481)
          use std::unordered_map to store sessions (#7405)
          drop use of BIO_f_base64 and EVP_PKEY_new_mac_key (#7106)
          Do not write to the cache if the plugin decides not to write to the cache (#7461)
          API to retrieve NoStore set by plugins (#7439)
          Update AuTest version update directions for pipenv (#7469)
          Add command line utility to help convert remap plugin usage to ATS9. (#7426)
          Cleanup: Get rid of MIMEFieldWrapper from HPACK encoding (#6520)
          Proxy Verifier: Making use of delay directives for caching tests. (#7468)
          Cleanup: Add SNIRoutingType (#7453)
          Updating to Proxy Verifier v2.0.0 (#7454)
          Adjust to actually try a server address more than once (#7288)
          Change atoi to atol, causing obvious issues on what needs to be int64's (#7466)
          Cleans up duplicated TSOutboundConnectionMatchType definition (#7090)
          Fixing compress expectation for new microserver (#7463)
          Update to the new MicroServer 1.0.6 release (#7460)
          CacheRead: clear dir entry if doc is found to be truncated (#7064)
          Do not provide a stale negative cache (#7422)
          Generalize SNI support (#6870)
          Add synchronization between UDPNetProcessor::UDPBind in main Thread and initialize_thread_for_udp_net in ET_UDP Thread (#7407)
          Fix heap use after free in DNSProcessor::getby() (#3871)
          Fix comment in include/tscore/Filenames.h. (#7457)
          Fix Makefile target for creating changelogs (#7455)
          Change squid log code for self looping (#7443)
          Enhancements for compress plugin (#7416)
          Add incoming PROXY Protocol v2 support (#7340)
          Cleanup: Remove unused members of NextHopProperty (#7436)
          Small fix to regex_remap PR # 7347. (#7437)
          PoolableSession (#6828)
          option to disable compression for range request's response (#7287)
          Make TSUrlSchemeGet() return scheme implied by URL type when there is no explicit scheme. (#7262)
    
    commit c40d95a912166224e517b07d6dc3ffd273907fc9
    Merge: 573035c60 ecd70df36
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Wed Jan 20 09:39:34 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Fix a link error on traffi_quic command (#7433)
          Fix stall on outbound TLS handshake (#7432)
          Fix the Proxy Verifier AuTest extension to handle cert paths correctly (#7415)
          Update documentation for TSSslSessionInsert (#7420)
          Improve zlib detection logic (#7430)
          Fix parent connect fail segfault (#7429)
    
    commit 573035c606fa088349420de35f0cdabe38649f5e
    Merge: 5704095ba 95b8d575a
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Fri Jan 15 23:24:29 2021 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Doc: Fix typo in negative_revalidating_lifetime (#7427)
          Change comment handling for long lines in url_sig plugin (#7421)
          Add unit tests for PROXY Protocol v1 parser (#7332)
          LGTM: Remove superfluous const qualifier in return type (#7412)
          Fix issue with unavailable server retry codes (#7410)
          Remove the warning statement (#7414)
          default to throttling and subsequently simplify the transfer code (#7257)
          Improvement to lua plugin (#7413)
          Make places to bind/unbind SSL object with/from NetVC (#7399)
          traffic_ctl - plugin msg  now require only the tag as mandatory field data field is now optional. (#7364)
          API - Add new api function TSHttpTxnServerSsnTransactionCount() to retrieve the number of transactions between TS proxy and the origin server from a single session. (#7387)
          Fix clang compiler complaint about an unused parameter in SNIAction. (#7409)
          Add compression support to stats_over_http (#7393)
          Doc: Fix INPUT tag of Doxyfile (#7404)
          Remove unneeded variables in UnixNetVConnection (#7403)
          Correctly pass back errno to HttpSM (#7402)
          Reverting to old negative_caching conditional behavior (#7401)
          Remove unused MAYBE_ABORT state (#7400)
          traffic_manager should not retry on disk failure (#7397)
          Eliminate dangling pointer into stack space. (#7392)
          This PR aims to address some of the lock contention found and (#7377)
          Remove a special treatment for SSLNetVC in migrateToCurrentThread() (#7384)
          Replace ::exit() with _exit() to avoid secondary cleanup cores (#7395)
          [Doc] Fix build warnings (#7391)
          Clear call_sm on tunnel reset (#7352)
          Unused code: HostDBContinuation::removeEvent (#7383)
          Traffic Dump: Fix stream-id printing after first transaction. (#7311)
          Add comments to ink_queue.h. (#7376)
          Cleanup incoming PROXY Protocol v1 (#7331)
          In CI, only run autopep8 on branches that enforce autopep8 (#7270)
          Fix FreeBSD 12 link issue in test_libhttp2. (#7367)
          Adjust flags to ensure tunnel producer is cleaned up (#7336)
          Cleanup: Remove SSL Wire Trace releated code in UnixNetVConnection (#7368)
          Use EVP MAC API if available (#7363)
          Use EVP API instead of MD5_Init/Update/Final (secure_link plugin) (#7355)
          Use ERR_get_error_all if available (#7354)
          Use OpeSSL EVP API instead of SHA256_Init/Update/Final (#7342)
          Cleanup: Get rid of NetVConnection::outstanding() (#7366)
          Cleanup: Remove unused functions (#7365)
          Add a post case to the conn_timeout test (#7334)
          Fix sni ip_allow and host_sni_policy (#7349)
          AuTest for Split DNS (#7325)
          Make reloading client certificate configuration more reliable (#7313)
          Add negative caching tests and fixes. (#7361)
          ESI: Ensure gzip header is always initialized (#7360)
          Allow for regex_remap of pristine URL. (#7347)
          Set thread mutex to the DNSHandler mutex of SplitDNS (#7321)
          Fix lookup split dns rule with fast path (#7320)
          Add note to background fetch about include/exclude (#7343)
          AuTest for incoming PROXY Protocol v1 (#7326)
          Fix vc close migration race condition (#7337)
          TLS Session Reuse: Downgrade add_session messages to debug (#7345)
          TLS Session Reuse: Downgrade noisy log to debug (#7344)
          Remove the last remnants of the enable_url_expandomatic (#7276)
          Remove unnecessary cast from ReverseProxy. (#7329)
          Updates the Dockerfile with more packages (#7323)
          fixup in HttpSM to only set [TS_MILESTONE_SERVER_CLOSE if TS_MILESTONE_SERVER_CONNECT has been set (#7259)
          Add option for hybrid global and thread session pools (#6978)
          Get appropriate locks on SSN_START hook delays (#7295)
          s3_auth: demote noisy errors around configuration that doesn't affect plugin usability (#7306)
          Follow the comments in I_Thread.h, add an independent ink_thread_key for EThread. (#6288)
          Reduce the number of write operation on H2 (#7282)
    
    commit 5704095ba63316e672d9fae67d2757fff084e03c
    Merge: 882a79d87 0c88b24a0
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Wed Oct 28 21:06:11 2020 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Adds a shell script to help build the H3 toolchains (#7299)
          Remove unfinished h2c support (#7286)
          Allow disabling SO_MARK and IP_TOS usage (#7292)
          Enable all h2spec test (#7289)
          Fix bad HTTP/2 post client causing stuck HttpSM (#7237)
          Sticky server does not work with H2 client (#7261)
          7096: Synchronize Server Session Management and Network I/O (#7278)
          HostDB: remove unused field in HostDBApplicationInfo, and update remaining types in http_data to fix broken padding. (#7264)
          Add support for a new (TSMgmtDataTypeGet) mgmt API function to retrieve the record data type (#7221)
          Fix example in default sni.yaml configuration. (#7277)
          Fix proxy.process.http.current_client_transactions (#7258)
          Add AuTest for HTTP/2 Graceful Shutdown (#7271)
          Fix truncated reponse on HTTP/2 graceful shutdown (#7267)
          url_sig add 'ignore_expiry = true' option for log replay testing (#7231)
          Respecting default rolling_enabled in plugins. (#7275)
          gracefully handle TSReleaseAsserts in statichit and generator plugins (#7269)
          Removes commented out code from esi plugin (#7273)
          Allow initial // in request targets. (#7266)
          Document external log rotation support via SIGUSR2 (#7265)
          Let Dedicated EThreads use `EThread::schedule` (#7228)
          HostDB: Fix cache data version checking to use full version, not major version. (#7263)
          Bugfix: set a default inactivity timeout only if a read or write I/O operation was set (#7226)
          Treat objects with negative max-age CC directives as stale. (#7260)
          Remove some usless defines, which just obsfucates code (#7252)
          Remove useless if for port set assertion. (#7250)
          Fix test_error_page_selection memory leaks and logic errors (#7248)
          [multiplexer] option to skip post/put requests (#7233)
          Incorporates the latest CI build changes (#7251)
          Add support for server protocol stack API (#7239)
          Fix for plugins ASAN suppression file (#7249)
          RolledLogDeleter: do not sort on each candidate consideration. (#7243)
          Make double Au test more reliable. (#7216)
          Ensure that ca override does not get lost (#7219)
          Stop crash on disk failure (#7218)
          Do not cache Transfer-Encoding header (#7234)
          clean up body factory tests (#7236)
          Revert "Create an explicit runroot.yaml for AuTests (#7177)" (#7235)
          New option to dead server to not retry during dead period (#7142)
          Increment ssl_error_syscall only if not EOF (#7225)
          Fix renamed setting in default config (#7224)
          Log config reload: use new config for initialization (#7215)
          Introduce proxy-verifier to AuTests (#7211)
          Follow redirection responses when refreshing stale cache objects. (#7213)
          Create an explicit runroot.yaml for AuTests (#7177)
          Support external log rotation tools via SIGUSR2 (#6806)
          Add support for TS API for Note, Status, Warning, Alert (#7208)
          If the weight is 0, the SRV record should be selected from the highest priority group (#7206)
          Cleanup: remove unnecessary memset() within dns_process() (#7209)
          Docs cleanup (#7210)
          Strip whitespaces after field-name and before the colon in headers from the origin (#7202)
          Adds new plugin: statichit (#7173)
          Add duplicate header field processing when creating outgoing response (#7207)
    
    commit 882a79d87126a27482b2d1dc5a172ef042acad6b
    Merge: 2a9887f4c bb5c39086
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Fri Sep 18 10:01:14 2020 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Rename ambiguous log variable (#7199)
          KWF useless member function HttpSM::kill_this_async_hook(). (#7198)
          Fix the active_timeout test to work without quic enabled (#7197)
          Remove obsolete cdn_ HttpTransact vars (#7182)
          Remove unused HttpUpdate mechanism (#7194)
          Updates the list of supported / linked Docs versions (#7152)
          Make custom xdebug HTTP header name available to other plugins. (#7193)
          Update sni outbound policy to allow directly setting the outbound SNI. (#7188)
    
    commit 2a9887f4c4c5a9259cdd64bf24c76b1618d78d29
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Wed Sep 16 17:54:01 2020 +0900
    
        Avoid unnecessary QUIC CID randomization
    
    commit 42e8898aafbdb8f17fefb1da99d7ae7cdc019a19
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Tue Sep 15 12:41:28 2020 +0900
    
        Simplify interface between H3 and QUIC, and remove memcopy between them
    
    commit 112fc71a324397a590c1cad6a4b2cfed27a551c2
    Merge: ac31adaa8 b09096481
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Tue Sep 15 09:21:25 2020 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Add an autest testcase for HTTP3 (#7063)
          Fix TSHttpTxnServerPacket* API's to correctly update existing server connections (#7175)
          Do not lose original inactivity timeout on disable (#7134)
          Emits log when OCSP fails to connect to server (#7183)
          autopep8: avoid running on non-tracked files. (#7186)
          TextView: Add additional constructor tests. (#7189)
          Remove duplicate code (#7180)
          TextView: add constructor size values to enable strlen even for null pointers. (#7185)
          Add virtual destructor to QUICRTTProvider. (#7184)
          AuTest: Reuse venv if it exists already (#7178)
          TS_API for Note,Status,Warning,Alert,Fatal (#7181)
          Traffic Dump: Record HTTP/2 priority. (#7149)
          leaks in logs (#7172)
          Additions to enable loading qat_engine (#7150)
          Removes references to non-existent function handle_conditional_headers (#7162)
          Fix #7164 Chaning Warning to Debug and creating a stat for inserting duplicates to pending dns (#7166)
          Fix #7167, make autopep8 failure (#7168)
          MicroDNS Extension: handle different 'default' types (#7159)
          Traffic Dump documentation for post_process.py (#7161)
          Fix memory leaks in multiplexer plugin (#7160)
          rc: fixes systemd unit file stopping (#7157)
          Fix lua plugin mem leak problem (#7158)
          Don't make an error on duplicated RETIRE_CONNECTION frames (#7131)
          URL::parse fixes for empty paths (#7119)
          Replace ACTION_RESULT_NONE with nullptr (#7135)
          Add metric tracking async job pauses (#7153)
          PluginFactory - Remove unused code that was left from last PluginFactory change(TSPluginDSOReloadEnable) (#7155)
          Fix stale pointer due to SSL config reload (#7148)
          slice: check if vio is still valid before calling TSVIODone* on shutdown (#7147)
          Deprecate cqhv field (#7143)
          Don't return QUIC frame if the size exceeds maximum frame size (#7121)
          Check VIO availability before acquiring a lock for it (#7145)
          Fix #7116, skip the insertion of the same continuation to pending dns (#7117)
          Allow override of CA certs for cert from client based on SNI server name sent by client. (#7130)
          Fix typo in cache docs (#7144)
          remove useless shortopt (#7138)
          Protect TSActionCancel from null INKContInternal actions (#7128)
          Check VIO availability before checking whether the VIO has data (#7120)
          Accept NAT rebinding on a QUIC connection (#7123)
          Fixes garbled logs when using %<vbn> log tag (#7140)
          Removes duplicated listing of files in same Makefile target (#7137)
          Updated gdb mutex script to get process file for Fedora 32 (#7133)
          SSLConfig mem leak fix (#7125)
          Replaces "smart" quotes with ASCII equivalents (#7126)
          Comment out a wrong assertion in QUIC Loss Detection logic (#7129)
          Add member initialization to the Errata class. (#7132)
          Cancel active/inactive timeout on closing Http2Stream (#7111)
          Add modsecurity lua script to example (#7105)
          Expose remap config file callback (#7073)
          Make tls_hooks tests more likely to pass (#7122)
    
    commit ac31adaa82f9271f902de2f45c071e328f620271
    Merge: 4d579f49a e904dbcef
    Author: Masakazu Kitajo <ma...@apache.org>
    Date:   Mon Aug 17 09:14:14 2020 +0900
    
        Merge branch 'master' into quic-latest
    
        * master:
          Backing out my update of our jenkin's autest file. (#7118)
          Don't send image/webp responses from cache to broswers that don't support it (#7104)
          Updating our autest suite to require Python3.6 (#7113)
          Squashed commit of the following: (#7110)
          Supporting out of source builds for AuTests. (#7109)
          Fixes uninitialized variables found by Xcode (#7100)
          Add cross references between server session sharing match and upstream connection tracking match. (#7038)
---
 iocore/net/quic/Makefile.am                        |   3 +
 iocore/net/quic/Mock.h                             |  90 +++-
 iocore/net/quic/QUICApplication.cc                 | 248 +---------
 iocore/net/quic/QUICApplication.h                  |  52 +-
 iocore/net/quic/QUICBidirectionalStream.cc         | 381 +++++----------
 iocore/net/quic/QUICBidirectionalStream.h          |  23 +-
 iocore/net/quic/QUICPacket.h                       |   2 +-
 iocore/net/quic/QUICPacketFactory.cc               |   4 +-
 iocore/net/quic/QUICStream.cc                      | 158 +-----
 iocore/net/quic/QUICStream.h                       |  49 +-
 .../net/quic/QUICStreamAdapter.cc                  |  16 +-
 .../net/quic/QUICStreamAdapter.h                   |  52 +-
 iocore/net/quic/QUICStreamFactory.cc               |   6 +-
 iocore/net/quic/QUICStreamFactory.h                |   6 +-
 iocore/net/quic/QUICStreamManager.cc               |  89 ++--
 iocore/net/quic/QUICStreamManager.h                |  11 +-
 iocore/net/quic/QUICStreamState.cc                 |  96 ++--
 iocore/net/quic/QUICStreamState.h                  |  43 +-
 iocore/net/quic/QUICStreamVCAdapter.cc             | 320 +++++++++++++
 iocore/net/quic/QUICStreamVCAdapter.h              | 105 ++++
 iocore/net/quic/QUICTransferProgressProvider.cc    |  83 ++++
 iocore/net/quic/QUICTransferProgressProvider.h     |  44 +-
 iocore/net/quic/QUICUnidirectionalStream.cc        | 533 ++++-----------------
 iocore/net/quic/QUICUnidirectionalStream.h         |  20 +-
 iocore/net/quic/test/test_QUICStream.cc            | 189 +++++---
 iocore/net/quic/test/test_QUICStreamState.cc       | 134 +++---
 proxy/http3/Http09App.cc                           |  53 +-
 proxy/http3/Http09App.h                            |   4 +
 proxy/http3/Http3App.cc                            | 157 +++---
 proxy/http3/Http3App.h                             |  28 +-
 proxy/http3/Http3DataFramer.cc                     |   8 +-
 proxy/http3/Http3DataFramer.h                      |   2 +-
 proxy/http3/Http3Frame.cc                          | 127 +++--
 proxy/http3/Http3Frame.h                           |  12 +-
 proxy/http3/Http3FrameCollector.cc                 |  22 +-
 proxy/http3/Http3FrameCollector.h                  |   4 +-
 proxy/http3/Http3FrameDispatcher.cc                |  15 +-
 proxy/http3/Http3FrameDispatcher.h                 |   4 +-
 proxy/http3/Http3FrameGenerator.h                  |   4 +-
 proxy/http3/Http3HeaderFramer.cc                   |   9 +-
 proxy/http3/Http3HeaderFramer.h                    |   2 +-
 proxy/http3/Http3HeaderVIOAdaptor.cc               | 122 ++++-
 proxy/http3/Http3HeaderVIOAdaptor.h                |  23 +-
 proxy/http3/Http3Transaction.cc                    | 178 ++-----
 proxy/http3/Http3Transaction.h                     |  19 +-
 proxy/http3/QPACK.cc                               | 159 +++---
 proxy/http3/QPACK.h                                |  33 +-
 proxy/http3/test/main.cc                           |   8 +
 proxy/http3/test/test_Http3Frame.cc                |  24 +-
 proxy/http3/test/test_QPACK.cc                     |  22 +-
 src/traffic_quic/quic_client.cc                    |  67 ++-
 src/traffic_quic/quic_client.h                     |  11 +
 src/traffic_quic/traffic_quic.cc                   |   5 +
 53 files changed, 1946 insertions(+), 1933 deletions(-)

diff --git a/iocore/net/quic/Makefile.am b/iocore/net/quic/Makefile.am
index 8a60a23..ca72382 100644
--- a/iocore/net/quic/Makefile.am
+++ b/iocore/net/quic/Makefile.am
@@ -62,6 +62,8 @@ libquic_a_SOURCES = \
   QUICNewRenoCongestionController.cc \
   QUICFlowController.cc \
   QUICStreamState.cc \
+  QUICStreamAdapter.cc \
+  QUICStreamVCAdapter.cc \
   QUICStream.cc \
   QUICHandshake.cc \
   QUICPacketHeaderProtector.cc \
@@ -92,6 +94,7 @@ libquic_a_SOURCES = \
   QUICFrameGenerator.cc \
   QUICFrameRetransmitter.cc \
   QUICAddrVerifyState.cc \
+  QUICTransferProgressProvider.cc \
   QUICBidirectionalStream.cc \
   QUICCryptoStream.cc \
   QUICUnidirectionalStream.cc \
diff --git a/iocore/net/quic/Mock.h b/iocore/net/quic/Mock.h
index eda260c..3185315 100644
--- a/iocore/net/quic/Mock.h
+++ b/iocore/net/quic/Mock.h
@@ -36,6 +36,7 @@
 #include "QUICPinger.h"
 #include "QUICPadder.h"
 #include "QUICHandshakeProtocol.h"
+#include "QUICStreamAdapter.h"
 
 class MockQUICContext;
 
@@ -717,6 +718,76 @@ private:
   MockQUICCongestionController _cc;
 };
 
+class MockQUICStreamAdapter : public QUICStreamAdapter
+{
+public:
+  MockQUICStreamAdapter(QUICStream &stream) : QUICStreamAdapter(stream) {}
+
+  void
+  write_to_stream(const uint8_t *buf, size_t len)
+  {
+    this->_total_sending_data_len += len;
+    this->_sending_data_len += len;
+  }
+
+  int64_t
+  write(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin) override
+  {
+    this->_total_receiving_data_len += data_length;
+    this->_receiving_data_len += data_length;
+    return data_length;
+  }
+  bool
+  is_eos() override
+  {
+    return false;
+  }
+  uint64_t
+  unread_len() override
+  {
+    return this->_sending_data_len;
+  }
+  uint64_t
+  read_len() override
+  {
+    return 0;
+  }
+  uint64_t
+  total_len() override
+  {
+    return this->_total_sending_data_len;
+  }
+  void
+  encourge_read() override
+  {
+  }
+  void
+  encourge_write() override
+  {
+  }
+  void
+  notify_eos() override
+  {
+  }
+
+protected:
+  Ptr<IOBufferBlock>
+  _read(size_t len) override
+  {
+    this->_sending_data_len -= len;
+    Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
+    block->alloc(iobuffer_size_to_index(len, BUFFER_SIZE_INDEX_32K));
+    block->fill(len);
+    return block;
+  }
+
+private:
+  size_t _sending_data_len         = 0;
+  size_t _total_sending_data_len   = 0;
+  size_t _receiving_data_len       = 0;
+  size_t _total_receiving_data_len = 0;
+};
+
 class MockQUICApplication : public QUICApplication
 {
 public:
@@ -726,20 +797,29 @@ public:
   main_event_handler(int event, Event *data)
   {
     if (event == 12345) {
-      QUICStreamIO *stream_io = static_cast<QUICStreamIO *>(data->cookie);
-      stream_io->write_reenable();
     }
     return EVENT_CONT;
   }
 
   void
+  on_new_stream(QUICStream &stream) override
+  {
+    auto ite                   = this->_streams.emplace(stream.id(), stream);
+    QUICStreamAdapter &adapter = ite.first->second;
+    stream.set_io_adapter(&adapter);
+  }
+
+  void
   send(const uint8_t *data, size_t size, QUICStreamId stream_id)
   {
-    QUICStreamIO *stream_io = this->_find_stream_io(stream_id);
-    stream_io->write(data, size);
+    auto ite      = this->_streams.find(stream_id);
+    auto &adapter = ite->second;
+    adapter.write_to_stream(data, size);
 
-    eventProcessor.schedule_imm(this, ET_CALL, 12345, stream_io);
+    // eventProcessor.schedule_imm(this, ET_CALL, 12345, adapter);
   }
+
+  std::unordered_map<QUICStreamId, MockQUICStreamAdapter> _streams;
 };
 
 class MockQUICPacketR : public QUICPacketR
diff --git a/iocore/net/quic/QUICApplication.cc b/iocore/net/quic/QUICApplication.cc
index a5bfd26..d6dc495 100644
--- a/iocore/net/quic/QUICApplication.cc
+++ b/iocore/net/quic/QUICApplication.cc
@@ -24,172 +24,6 @@
 #include "QUICApplication.h"
 #include "QUICStream.h"
 
-static constexpr char tag_stream_io[] = "quic_stream_io";
-static constexpr char tag_app[]       = "quic_app";
-
-#define QUICStreamIODebug(fmt, ...)                                                                                           \
-  Debug(tag_stream_io, "[%s] [%" PRIu64 "] " fmt, this->_stream_vc->connection_info()->cids().data(), this->_stream_vc->id(), \
-        ##__VA_ARGS__)
-
-//
-// QUICStreamIO
-//
-QUICStreamIO::QUICStreamIO(QUICApplication *app, QUICStreamVConnection *stream_vc) : _stream_vc(stream_vc)
-{
-  this->_read_buffer  = new_MIOBuffer(BUFFER_SIZE_INDEX_8K);
-  this->_write_buffer = new_MIOBuffer(BUFFER_SIZE_INDEX_8K);
-
-  this->_read_buffer_reader  = this->_read_buffer->alloc_reader();
-  this->_write_buffer_reader = this->_write_buffer->alloc_reader();
-
-  switch (stream_vc->direction()) {
-  case QUICStreamDirection::BIDIRECTIONAL:
-    this->_read_vio  = stream_vc->do_io_read(app, INT64_MAX, this->_read_buffer);
-    this->_write_vio = stream_vc->do_io_write(app, INT64_MAX, this->_write_buffer_reader);
-    break;
-  case QUICStreamDirection::SEND:
-    this->_write_vio = stream_vc->do_io_write(app, INT64_MAX, this->_write_buffer_reader);
-    break;
-  case QUICStreamDirection::RECEIVE:
-    this->_read_vio = stream_vc->do_io_read(app, INT64_MAX, this->_read_buffer);
-    break;
-  default:
-    ink_assert(false);
-    break;
-  }
-}
-
-QUICStreamIO::~QUICStreamIO()
-{
-  // All readers will be deallocated
-  free_MIOBuffer(this->_read_buffer);
-  free_MIOBuffer(this->_write_buffer);
-};
-
-uint32_t
-QUICStreamIO::stream_id() const
-{
-  return this->_stream_vc->id();
-}
-
-bool
-QUICStreamIO::is_bidirectional() const
-{
-  return this->_stream_vc->is_bidirectional();
-}
-
-int64_t
-QUICStreamIO::read(uint8_t *buf, int64_t len)
-{
-  if (is_debug_tag_set(tag_stream_io)) {
-    if (this->_read_vio->nbytes == INT64_MAX) {
-      QUICStreamIODebug("nbytes=- ndone=%" PRId64 " read_avail=%" PRId64 " read_len=%" PRId64, this->_read_vio->ndone,
-                        this->_read_buffer_reader->read_avail(), len);
-    } else {
-      QUICStreamIODebug("nbytes=%" PRId64 " ndone=%" PRId64 " read_avail=%" PRId64 " read_len=%" PRId64, this->_read_vio->nbytes,
-                        this->_read_vio->ndone, this->_read_buffer_reader->read_avail(), len);
-    }
-  }
-
-  int64_t nread = this->_read_buffer_reader->read(buf, len);
-  if (nread > 0) {
-    this->_read_vio->ndone += nread;
-  }
-
-  this->_stream_vc->on_read();
-
-  return nread;
-}
-
-int64_t
-QUICStreamIO::peek(uint8_t *buf, int64_t len)
-{
-  return this->_read_buffer_reader->memcpy(buf, len) - reinterpret_cast<char *>(buf);
-}
-
-void
-QUICStreamIO::consume(int64_t len)
-{
-  this->_read_buffer_reader->consume(len);
-  this->_stream_vc->on_read();
-}
-
-bool
-QUICStreamIO::is_read_done() const
-{
-  return this->_read_vio->ntodo() == 0;
-}
-
-int64_t
-QUICStreamIO::write(const uint8_t *buf, int64_t len)
-{
-  SCOPED_MUTEX_LOCK(lock, this->_write_vio->mutex, this_ethread());
-
-  int64_t nwritten = this->_write_buffer->write(buf, len);
-  if (nwritten > 0) {
-    this->_nwritten += nwritten;
-  }
-
-  return len;
-}
-
-int64_t
-QUICStreamIO::write(IOBufferReader *r, int64_t len)
-{
-  SCOPED_MUTEX_LOCK(lock, this->_write_vio->mutex, this_ethread());
-
-  int64_t bytes_avail = this->_write_buffer->write_avail();
-
-  if (bytes_avail > 0) {
-    if (is_debug_tag_set(tag_stream_io)) {
-      if (this->_write_vio->nbytes == INT64_MAX) {
-        QUICStreamIODebug("nbytes=- ndone=%" PRId64 " write_avail=%" PRId64 " write_len=%" PRId64, this->_write_vio->ndone,
-                          bytes_avail, len);
-      } else {
-        QUICStreamIODebug("nbytes=%" PRId64 " ndone=%" PRId64 " write_avail=%" PRId64 " write_len=%" PRId64,
-                          this->_write_vio->nbytes, this->_write_vio->ndone, bytes_avail, len);
-      }
-    }
-
-    int64_t bytes_len = std::min(bytes_avail, len);
-    int64_t nwritten  = this->_write_buffer->write(r, bytes_len);
-
-    if (nwritten > 0) {
-      this->_nwritten += nwritten;
-    }
-
-    return nwritten;
-  } else {
-    return 0;
-  }
-}
-
-// TODO: Similar to other "write" apis, but do not copy.
-int64_t
-QUICStreamIO::write(IOBufferBlock *b)
-{
-  ink_assert(!"not implemented yet");
-  return 0;
-}
-
-void
-QUICStreamIO::write_done()
-{
-  this->_write_vio->nbytes = this->_nwritten;
-}
-
-void
-QUICStreamIO::read_reenable()
-{
-  return this->_read_vio->reenable();
-}
-
-void
-QUICStreamIO::write_reenable()
-{
-  return this->_write_vio->reenable();
-}
-
 //
 // QUICApplication
 //
@@ -198,84 +32,4 @@ QUICApplication::QUICApplication(QUICConnection *qc) : Continuation(new_ProxyMut
   this->_qc = qc;
 }
 
-QUICApplication::~QUICApplication()
-{
-  for (auto const &kv : this->_stream_map) {
-    delete kv.second;
-  }
-}
-
-// @brief Bind stream and application
-void
-QUICApplication::set_stream(QUICStreamVConnection *stream_vc, QUICStreamIO *stream_io)
-{
-  if (stream_io == nullptr) {
-    stream_io = new QUICStreamIO(this, stream_vc);
-  }
-  this->_stream_map.insert(std::make_pair(stream_vc->id(), stream_io));
-}
-
-// @brief Bind stream and application
-void
-QUICApplication::set_stream(QUICStreamIO *stream_io)
-{
-  this->_stream_map.insert(std::make_pair(stream_io->stream_id(), stream_io));
-}
-
-bool
-QUICApplication::is_stream_set(QUICStreamVConnection *stream)
-{
-  auto result = this->_stream_map.find(stream->id());
-
-  return result != this->_stream_map.end();
-}
-
-void
-QUICApplication::reenable(QUICStreamVConnection *stream)
-{
-  QUICStreamIO *stream_io = this->_find_stream_io(stream->id());
-  if (stream_io) {
-    stream_io->read_reenable();
-    stream_io->write_reenable();
-  } else {
-    Debug(tag_app, "[%s] Unknown Stream id=%" PRIx64, this->_qc->cids().data(), stream->id());
-  }
-
-  return;
-}
-
-void
-QUICApplication::unset_stream(QUICStreamVConnection *stream)
-{
-  QUICStreamIO *stream_io = this->_find_stream_io(stream->id());
-  if (stream_io) {
-    this->_stream_map.erase(stream->id());
-  }
-}
-
-QUICStreamIO *
-QUICApplication::_find_stream_io(QUICStreamId id)
-{
-  auto result = this->_stream_map.find(id);
-
-  if (result == this->_stream_map.end()) {
-    return nullptr;
-  } else {
-    return result->second;
-  }
-}
-
-QUICStreamIO *
-QUICApplication::_find_stream_io(VIO *vio)
-{
-  if (vio == nullptr) {
-    return nullptr;
-  }
-
-  QUICStream *stream = dynamic_cast<QUICStream *>(vio->vc_server);
-  if (stream == nullptr) {
-    return nullptr;
-  }
-
-  return this->_find_stream_io(stream->id());
-}
+QUICApplication::~QUICApplication() {}
diff --git a/iocore/net/quic/QUICApplication.h b/iocore/net/quic/QUICApplication.h
index c4fb16f..a697ea2 100644
--- a/iocore/net/quic/QUICApplication.h
+++ b/iocore/net/quic/QUICApplication.h
@@ -32,48 +32,6 @@
 class QUICApplication;
 
 /**
- @brief QUICStream I/O Interface for QUICApplication
- */
-class QUICStreamIO
-{
-public:
-  QUICStreamIO(QUICApplication *app, QUICStreamVConnection *stream);
-  virtual ~QUICStreamIO();
-
-  uint32_t stream_id() const;
-  bool is_bidirectional() const;
-
-  int64_t read(uint8_t *buf, int64_t len);
-  int64_t peek(uint8_t *buf, int64_t len);
-  void consume(int64_t len);
-  bool is_read_done() const;
-  virtual void read_reenable();
-
-  int64_t write(const uint8_t *buf, int64_t len);
-  int64_t write(IOBufferReader *r, int64_t len);
-  int64_t write(IOBufferBlock *b);
-  void write_done();
-  virtual void write_reenable();
-
-protected:
-  MIOBuffer *_read_buffer  = nullptr;
-  MIOBuffer *_write_buffer = nullptr;
-
-  IOBufferReader *_read_buffer_reader  = nullptr;
-  IOBufferReader *_write_buffer_reader = nullptr;
-
-private:
-  QUICStreamVConnection *_stream_vc = nullptr;
-
-  VIO *_read_vio  = nullptr;
-  VIO *_write_vio = nullptr;
-
-  // Track how much data is written to _write_vio. When total size of data become clear,
-  // set it to _write_vio.nbytes.
-  uint64_t _nwritten = 0;
-};
-
-/**
  * @brief Abstract QUIC Application Class
  * @detail Every quic application must inherits this class
  */
@@ -83,18 +41,10 @@ public:
   QUICApplication(QUICConnection *qc);
   virtual ~QUICApplication();
 
-  void set_stream(QUICStreamVConnection *stream_vc, QUICStreamIO *stream_io = nullptr);
-  void set_stream(QUICStreamIO *stream_io);
-  bool is_stream_set(QUICStreamVConnection *stream_vc);
-  void reenable(QUICStreamVConnection *stream_vc);
-  void unset_stream(QUICStreamVConnection *stream_vc);
+  virtual void on_new_stream(QUICStream &stream) = 0;
 
 protected:
-  QUICStreamIO *_find_stream_io(QUICStreamId id);
-  QUICStreamIO *_find_stream_io(VIO *vio);
-
   QUICConnection *_qc = nullptr;
 
 private:
-  std::map<QUICStreamId, QUICStreamIO *> _stream_map;
 };
diff --git a/iocore/net/quic/QUICBidirectionalStream.cc b/iocore/net/quic/QUICBidirectionalStream.cc
index 4203ec0..e5653e7 100644
--- a/iocore/net/quic/QUICBidirectionalStream.cc
+++ b/iocore/net/quic/QUICBidirectionalStream.cc
@@ -22,118 +22,25 @@
  */
 
 #include "QUICBidirectionalStream.h"
+#include "QUICStreamAdapter.h"
 
 //
 // QUICBidirectionalStream
 //
 QUICBidirectionalStream::QUICBidirectionalStream(QUICRTTProvider *rtt_provider, QUICConnectionInfoProvider *cinfo, QUICStreamId sid,
                                                  uint64_t recv_max_stream_data, uint64_t send_max_stream_data)
-  : QUICStreamVConnection(cinfo, sid),
+  : QUICStream(cinfo, sid),
     _remote_flow_controller(send_max_stream_data, _id),
     _local_flow_controller(rtt_provider, recv_max_stream_data, _id),
     _flow_control_buffer_size(recv_max_stream_data),
-    _state(nullptr, &this->_progress_vio, this, nullptr)
+    _state(nullptr, &this->_progress_sa, this, nullptr)
 {
-  SET_HANDLER(&QUICBidirectionalStream::state_stream_open);
-
   QUICStreamFCDebug("[LOCAL] %" PRIu64 "/%" PRIu64, this->_local_flow_controller.current_offset(),
                     this->_local_flow_controller.current_limit());
   QUICStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
                     this->_remote_flow_controller.current_limit());
 }
 
-int
-QUICBidirectionalStream::state_stream_open(int event, void *data)
-{
-  QUICVStreamDebug("%s (%d)", get_vc_event_name(event), event);
-  QUICErrorUPtr error = nullptr;
-
-  switch (event) {
-  case VC_EVENT_READ_READY:
-  case VC_EVENT_READ_COMPLETE: {
-    int64_t len = this->_process_read_vio();
-    if (len > 0) {
-      this->_signal_read_event();
-    }
-
-    break;
-  }
-  case VC_EVENT_WRITE_READY:
-  case VC_EVENT_WRITE_COMPLETE: {
-    int64_t len = this->_process_write_vio();
-    if (len > 0) {
-      this->_signal_write_event();
-    }
-
-    break;
-  }
-  case VC_EVENT_EOS:
-  case VC_EVENT_ERROR:
-  case VC_EVENT_INACTIVITY_TIMEOUT:
-  case VC_EVENT_ACTIVE_TIMEOUT: {
-    // TODO
-    ink_assert(false);
-    break;
-  }
-  default:
-    QUICStreamDebug("unknown event");
-    ink_assert(false);
-  }
-
-  // FIXME error is always nullptr
-  if (error != nullptr) {
-    if (error->cls == QUICErrorClass::TRANSPORT) {
-      QUICStreamDebug("QUICError: %s (%u), %s (0x%x)", QUICDebugNames::error_class(error->cls),
-                      static_cast<unsigned int>(error->cls), QUICDebugNames::error_code(error->code),
-                      static_cast<unsigned int>(error->code));
-    } else {
-      QUICStreamDebug("QUICError: %s (%u), APPLICATION ERROR (0x%x)", QUICDebugNames::error_class(error->cls),
-                      static_cast<unsigned int>(error->cls), static_cast<unsigned int>(error->code));
-    }
-    if (dynamic_cast<QUICStreamError *>(error.get()) != nullptr) {
-      // Stream Error
-      QUICStreamErrorUPtr serror = QUICStreamErrorUPtr(static_cast<QUICStreamError *>(error.get()));
-      this->reset(std::move(serror));
-    } else {
-      // Connection Error
-      // TODO Close connection (Does this really happen?)
-    }
-  }
-
-  return EVENT_DONE;
-}
-
-int
-QUICBidirectionalStream::state_stream_closed(int event, void *data)
-{
-  QUICVStreamDebug("%s (%d)", get_vc_event_name(event), event);
-
-  switch (event) {
-  case VC_EVENT_READ_READY:
-  case VC_EVENT_READ_COMPLETE: {
-    // ignore
-    break;
-  }
-  case VC_EVENT_WRITE_READY:
-  case VC_EVENT_WRITE_COMPLETE: {
-    // ignore
-    break;
-  }
-  case VC_EVENT_EOS:
-  case VC_EVENT_ERROR:
-  case VC_EVENT_INACTIVITY_TIMEOUT:
-  case VC_EVENT_ACTIVE_TIMEOUT: {
-    // TODO
-    ink_assert(false);
-    break;
-  }
-  default:
-    ink_assert(false);
-  }
-
-  return EVENT_DONE;
-}
-
 bool
 QUICBidirectionalStream::is_transfer_goal_set() const
 {
@@ -168,7 +75,6 @@ QUICConnectionErrorUPtr
 QUICBidirectionalStream::recv(const QUICStreamFrame &frame)
 {
   ink_assert(_id == frame.stream_id());
-  ink_assert(this->_read_vio.op == VIO::READ);
 
   // Check stream state - Do this first before accept the frame
   if (!this->_state.is_allowed_to_receive(frame)) {
@@ -202,9 +108,11 @@ QUICBidirectionalStream::recv(const QUICStreamFrame &frame)
     last_offset  = stream_frame->offset();
     last_length  = stream_frame->data_length();
 
-    this->_write_to_read_vio(stream_frame->offset(), reinterpret_cast<uint8_t *>(stream_frame->data()->start()),
-                             stream_frame->data_length(), stream_frame->has_fin_flag());
-    this->_state.update_with_receiving_frame(*new_frame);
+    this->_adapter->write(stream_frame->offset(), reinterpret_cast<uint8_t *>(stream_frame->data()->start()),
+                          stream_frame->data_length(), stream_frame->has_fin_flag());
+    if (this->_state.update_with_receiving_frame(*new_frame)) {
+      this->_notify_state_change();
+    }
 
     delete new_frame;
     new_frame = this->_received_stream_frame_buffer.pop();
@@ -218,7 +126,7 @@ QUICBidirectionalStream::recv(const QUICStreamFrame &frame)
                       this->_local_flow_controller.current_limit());
   }
 
-  this->_signal_read_event();
+  this->_adapter->encourge_read();
 
   return nullptr;
 }
@@ -230,10 +138,7 @@ QUICBidirectionalStream::recv(const QUICMaxStreamDataFrame &frame)
   QUICStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
                     this->_remote_flow_controller.current_limit());
 
-  int64_t len = this->_process_write_vio();
-  if (len > 0) {
-    this->_signal_write_event();
-  }
+  this->_adapter->encourge_write();
 
   return nullptr;
 }
@@ -249,7 +154,9 @@ QUICBidirectionalStream::recv(const QUICStreamDataBlockedFrame &frame)
 QUICConnectionErrorUPtr
 QUICBidirectionalStream::recv(const QUICStopSendingFrame &frame)
 {
-  this->_state.update_with_receiving_frame(frame);
+  if (this->_state.update_with_receiving_frame(frame)) {
+    this->_notify_state_change();
+  }
   this->_reset_reason = QUICStreamErrorUPtr(new QUICStreamError(this, QUIC_APP_ERROR_CODE_STOPPING));
   // We received and processed STOP_SENDING frame, so return NO_ERROR here
   return nullptr;
@@ -258,97 +165,11 @@ QUICBidirectionalStream::recv(const QUICStopSendingFrame &frame)
 QUICConnectionErrorUPtr
 QUICBidirectionalStream::recv(const QUICRstStreamFrame &frame)
 {
-  this->_state.update_with_receiving_frame(frame);
-  this->_signal_read_eos_event();
-  return nullptr;
-}
-
-// this->_read_vio.nbytes should be INT64_MAX until receive FIN flag
-VIO *
-QUICBidirectionalStream::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
-{
-  if (buf) {
-    this->_read_vio.buffer.writer_for(buf);
-  } else {
-    this->_read_vio.buffer.clear();
-  }
-
-  this->_read_vio.mutex     = c ? c->mutex : this->mutex;
-  this->_read_vio.cont      = c;
-  this->_read_vio.nbytes    = nbytes;
-  this->_read_vio.ndone     = 0;
-  this->_read_vio.vc_server = this;
-  this->_read_vio.op        = VIO::READ;
-
-  this->_process_read_vio();
-  this->_send_tracked_event(this->_read_event, VC_EVENT_READ_READY, &this->_read_vio);
-
-  return &this->_read_vio;
-}
-
-VIO *
-QUICBidirectionalStream::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, bool owner)
-{
-  if (buf) {
-    this->_write_vio.buffer.reader_for(buf);
-  } else {
-    this->_write_vio.buffer.clear();
-  }
-
-  this->_write_vio.mutex     = c ? c->mutex : this->mutex;
-  this->_write_vio.cont      = c;
-  this->_write_vio.nbytes    = nbytes;
-  this->_write_vio.ndone     = 0;
-  this->_write_vio.vc_server = this;
-  this->_write_vio.op        = VIO::WRITE;
-
-  this->_process_write_vio();
-  this->_send_tracked_event(this->_write_event, VC_EVENT_WRITE_READY, &this->_write_vio);
-
-  return &this->_write_vio;
-}
-
-void
-QUICBidirectionalStream::do_io_close(int lerrno)
-{
-  SET_HANDLER(&QUICBidirectionalStream::state_stream_closed);
-
-  this->_read_vio.buffer.clear();
-  this->_read_vio.nbytes = 0;
-  this->_read_vio.op     = VIO::NONE;
-  this->_read_vio.cont   = nullptr;
-
-  this->_write_vio.buffer.clear();
-  this->_write_vio.nbytes = 0;
-  this->_write_vio.op     = VIO::NONE;
-  this->_write_vio.cont   = nullptr;
-}
-
-void
-QUICBidirectionalStream::do_io_shutdown(ShutdownHowTo_t howto)
-{
-  ink_assert(false); // unimplemented yet
-  return;
-}
-
-void
-QUICBidirectionalStream::reenable(VIO *vio)
-{
-  if (vio->op == VIO::READ) {
-    QUICVStreamDebug("read_vio reenabled");
-
-    int64_t len = this->_process_read_vio();
-    if (len > 0) {
-      this->_signal_read_event();
-    }
-  } else if (vio->op == VIO::WRITE) {
-    QUICVStreamDebug("write_vio reenabled");
-
-    int64_t len = this->_process_write_vio();
-    if (len > 0) {
-      this->_signal_write_event();
-    }
+  if (this->_state.update_with_receiving_frame(frame)) {
+    this->_notify_state_change();
   }
+  this->_adapter->notify_eos();
+  return nullptr;
 }
 
 bool
@@ -361,9 +182,9 @@ QUICBidirectionalStream::will_generate_frame(QUICEncryptionLevel level, size_t c
   if (!this->is_retransmited_frame_queue_empty()) {
     return true;
   }
-  if (this->_write_vio.op != VIO::NONE && this->_write_vio.get_reader()->is_read_avail_more_than(0)) {
+  if (this->_adapter && this->_adapter->unread_len() > 0) {
     return true;
-  };
+  }
   return false;
 }
 
@@ -386,7 +207,9 @@ QUICBidirectionalStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level,
       return nullptr;
     }
     this->_records_rst_stream_frame(level, *static_cast<QUICRstStreamFrame *>(frame));
-    this->_state.update_with_sending_frame(*frame);
+    if (this->_state.update_with_sending_frame(*frame)) {
+      this->_notify_state_change();
+    }
     this->_is_reset_sent = true;
     return frame;
   }
@@ -400,7 +223,9 @@ QUICBidirectionalStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level,
       return nullptr;
     }
     this->_records_stop_sending_frame(level, *static_cast<QUICStopSendingFrame *>(frame));
-    this->_state.update_with_sending_frame(*frame);
+    if (this->_state.update_with_sending_frame(*frame)) {
+      this->_notify_state_change();
+    }
     this->_is_stop_sending_sent = true;
     return frame;
   }
@@ -412,91 +237,87 @@ QUICBidirectionalStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level,
     return frame;
   }
 
-  if (this->_write_vio.op != VIO::NONE && this->_state.is_allowed_to_send(QUICFrameType::STREAM)) {
-    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+  if (!this->_adapter || !this->_state.is_allowed_to_send(QUICFrameType::STREAM)) {
+    return frame;
+  }
+
+  uint64_t maximum_data_size = 0;
+  if (maximum_frame_size <= MAX_STREAM_FRAME_OVERHEAD) {
+    return frame;
+  }
+  maximum_data_size = maximum_frame_size - MAX_STREAM_FRAME_OVERHEAD;
+
+  bool pure_fin = false;
+  bool fin      = false;
+  if (this->_adapter->is_eos()) {
+    // Pure FIN stream should be sent regardless status of remote flow controller, because the length is zero.
+    pure_fin = true;
+    fin      = true;
+  }
 
-    uint64_t maximum_data_size = 0;
-    if (maximum_frame_size <= MAX_STREAM_FRAME_OVERHEAD) {
+  uint64_t len = 0;
+  if (!pure_fin) {
+    uint64_t data_len = this->_adapter->unread_len();
+    if (data_len == 0) {
       return frame;
     }
-    maximum_data_size = maximum_frame_size - MAX_STREAM_FRAME_OVERHEAD;
-
-    bool pure_fin = false;
-    bool fin      = false;
-    if ((this->_write_vio.nbytes != 0 || this->_write_vio.nbytes != INT64_MAX) &&
-        this->_write_vio.nbytes == static_cast<int64_t>(this->_send_offset)) {
-      // Pure FIN stream should be sent regardless status of remote flow controller, because the length is zero.
-      pure_fin = true;
-      fin      = true;
-    }
 
-    uint64_t len           = 0;
-    IOBufferReader *reader = this->_write_vio.get_reader();
-    if (!pure_fin) {
-      uint64_t data_len = reader->block_read_avail();
-      if (data_len == 0) {
-        return frame;
-      }
-
-      // Check Connection/Stream level credit only if the generating STREAM frame is not pure fin
-      uint64_t stream_credit = this->_remote_flow_controller.credit();
-      if (stream_credit == 0) {
-        // STREAM_DATA_BLOCKED
-        frame =
-          this->_remote_flow_controller.generate_frame(buf, level, UINT16_MAX, maximum_frame_size, current_packet_size, seq_num);
-        return frame;
-      }
-
-      if (connection_credit == 0) {
-        // BLOCKED - BLOCKED frame will be sent by connection level remote flow controller
-        return frame;
-      }
-
-      len = std::min(data_len, std::min(maximum_data_size, std::min(stream_credit, connection_credit)));
-
-      // data_len, maximum_data_size, stream_credit and connection_credit are already checked they're larger than 0
-      ink_assert(len != 0);
-
-      if (this->_write_vio.nbytes == static_cast<int64_t>(this->_send_offset + len)) {
-        fin = true;
-      }
+    // Check Connection/Stream level credit only if the generating STREAM frame is not pure fin
+    uint64_t stream_credit = this->_remote_flow_controller.credit();
+    if (stream_credit == 0) {
+      // STREAM_DATA_BLOCKED
+      frame =
+        this->_remote_flow_controller.generate_frame(buf, level, UINT16_MAX, maximum_frame_size, current_packet_size, seq_num);
+      return frame;
     }
 
-    Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(reader->get_current_block()->clone());
-    block->consume(reader->start_offset);
-    block->_end = std::min(block->start() + len, block->_buf_end);
-    ink_assert(static_cast<uint64_t>(block->read_avail()) == len);
-
-    // STREAM - Pure FIN or data length is lager than 0
-    // FIXME has_length_flag and has_offset_flag should be configurable
-    frame = QUICFrameFactory::create_stream_frame(buf, block, this->_id, this->_send_offset, fin, true, true,
-                                                  this->_issue_frame_id(), this);
-    if (!this->_state.is_allowed_to_send(*frame)) {
-      QUICStreamDebug("Canceled sending %s frame due to the stream state", QUICDebugNames::frame_type(frame->type()));
+    if (connection_credit == 0) {
+      // BLOCKED - BLOCKED frame will be sent by connection level remote flow controller
       return frame;
     }
 
-    if (!pure_fin) {
-      int ret = this->_remote_flow_controller.update(this->_send_offset + len);
-      // We cannot cancel sending the frame after updating the flow controller
+    len = std::min(data_len, std::min(maximum_data_size, std::min(stream_credit, connection_credit)));
 
-      // Calling update always success, because len is always less than stream_credit
-      ink_assert(ret == 0);
+    // data_len, maximum_data_size, stream_credit and connection_credit are already checked they're larger than 0
+    ink_assert(len != 0);
 
-      QUICVStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
-                         this->_remote_flow_controller.current_limit());
-      if (this->_remote_flow_controller.current_offset() == this->_remote_flow_controller.current_limit()) {
-        QUICStreamDebug("Flow Controller will block sending a STREAM frame");
-      }
+    if (this->_adapter->total_len() == this->_send_offset + len) {
+      fin = true;
+    }
+  }
+
+  Ptr<IOBufferBlock> block = this->_adapter->read(len);
+  ink_assert(static_cast<uint64_t>(block->read_avail()) == len);
+
+  // STREAM - Pure FIN or data length is lager than 0
+  // FIXME has_length_flag and has_offset_flag should be configurable
+  frame = QUICFrameFactory::create_stream_frame(buf, block, this->_id, this->_send_offset, fin, true, true, this->_issue_frame_id(),
+                                                this);
+  if (!this->_state.is_allowed_to_send(*frame)) {
+    QUICStreamDebug("Canceled sending %s frame due to the stream state", QUICDebugNames::frame_type(frame->type()));
+    return frame;
+  }
 
-      reader->consume(len);
-      this->_send_offset += len;
-      this->_write_vio.ndone += len;
+  if (!pure_fin) {
+    int ret = this->_remote_flow_controller.update(this->_send_offset + len);
+    // We cannot cancel sending the frame after updating the flow controller
+
+    // Calling update always success, because len is always less than stream_credit
+    ink_assert(ret == 0);
+
+    QUICVStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
+                       this->_remote_flow_controller.current_limit());
+    if (this->_remote_flow_controller.current_offset() == this->_remote_flow_controller.current_limit()) {
+      QUICStreamDebug("Flow Controller will block sending a STREAM frame");
     }
-    this->_records_stream_frame(level, *static_cast<QUICStreamFrame *>(frame));
 
-    this->_signal_write_event();
-    this->_state.update_with_sending_frame(*frame);
+    this->_send_offset += len;
+  }
+  this->_records_stream_frame(level, *static_cast<QUICStreamFrame *>(frame));
+
+  this->_adapter->encourge_write();
+  if (this->_state.update_with_sending_frame(*frame)) {
+    this->_notify_state_change();
   }
 
   return frame;
@@ -522,7 +343,9 @@ QUICBidirectionalStream::_on_frame_acked(QUICFrameInformationUPtr &info)
     break;
   }
 
-  this->_state.update_on_ack();
+  if (this->_state.update_on_ack()) {
+    this->_notify_state_change();
+  }
 }
 
 void
@@ -564,13 +387,17 @@ QUICBidirectionalStream::reset(QUICStreamErrorUPtr error)
 void
 QUICBidirectionalStream::on_read()
 {
-  this->_state.update_on_read();
+  if (this->_state.update_on_read()) {
+    this->_notify_state_change();
+  }
 }
 
 void
 QUICBidirectionalStream::on_eos()
 {
-  this->_state.update_on_eos();
+  if (this->_state.update_on_eos()) {
+    this->_notify_state_change();
+  }
 }
 
 QUICOffset
@@ -584,3 +411,9 @@ QUICBidirectionalStream::largest_offset_sent() const
 {
   return this->_remote_flow_controller.current_offset();
 }
+
+void
+QUICBidirectionalStream::_on_adapter_updated()
+{
+  this->_progress_sa.set_stream_adapter(this->_adapter);
+}
diff --git a/iocore/net/quic/QUICBidirectionalStream.h b/iocore/net/quic/QUICBidirectionalStream.h
index d64f899..6f0118b 100644
--- a/iocore/net/quic/QUICBidirectionalStream.h
+++ b/iocore/net/quic/QUICBidirectionalStream.h
@@ -25,24 +25,18 @@
 
 #include "QUICStream.h"
 
-class QUICBidirectionalStream : public QUICStreamVConnection, public QUICTransferProgressProvider
+class QUICBidirectionalStream : public QUICStream, public QUICTransferProgressProvider
 {
 public:
   QUICBidirectionalStream(QUICRTTProvider *rtt_provider, QUICConnectionInfoProvider *cinfo, QUICStreamId sid,
                           uint64_t recv_max_stream_data, uint64_t send_max_stream_data);
   QUICBidirectionalStream()
-    : QUICStreamVConnection(),
-      _remote_flow_controller(0, 0),
-      _local_flow_controller(nullptr, 0, 0),
-      _state(nullptr, nullptr, nullptr, nullptr)
+    : QUICStream(), _remote_flow_controller(0, 0), _local_flow_controller(nullptr, 0, 0), _state(nullptr, nullptr, nullptr, nullptr)
   {
   }
 
   ~QUICBidirectionalStream() {}
 
-  int state_stream_open(int event, void *data);
-  int state_stream_closed(int event, void *data);
-
   // QUICFrameGenerator
   bool will_generate_frame(QUICEncryptionLevel level, size_t current_packet_size, bool ack_eliciting, uint32_t seq_num) override;
   QUICFrame *generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint64_t connection_credit, uint16_t maximum_frame_size,
@@ -54,13 +48,6 @@ public:
   virtual QUICConnectionErrorUPtr recv(const QUICStopSendingFrame &frame) override;
   virtual QUICConnectionErrorUPtr recv(const QUICRstStreamFrame &frame) override;
 
-  // Implement VConnection Interface.
-  VIO *do_io_read(Continuation *c, int64_t nbytes = INT64_MAX, MIOBuffer *buf = 0) override;
-  VIO *do_io_write(Continuation *c = nullptr, int64_t nbytes = INT64_MAX, IOBufferReader *buf = 0, bool owner = false) override;
-  void do_io_close(int lerrno = -1) override;
-  void do_io_shutdown(ShutdownHowTo_t howto) override;
-  void reenable(VIO *vio) override;
-
   void stop_sending(QUICStreamErrorUPtr error) override;
   void reset(QUICStreamErrorUPtr error) override;
 
@@ -79,6 +66,9 @@ public:
   QUICOffset largest_offset_received() const override;
   QUICOffset largest_offset_sent() const override;
 
+protected:
+  virtual void _on_adapter_updated() override;
+
 private:
   QUICStreamErrorUPtr _reset_reason        = nullptr;
   bool _is_reset_sent                      = false;
@@ -88,7 +78,7 @@ private:
   bool _is_transfer_complete = false;
   bool _is_reset_complete    = false;
 
-  QUICTransferProgressProviderVIO _progress_vio = {this->_write_vio};
+  QUICTransferProgressProviderSA _progress_sa;
 
   QUICRemoteStreamFlowController _remote_flow_controller;
   QUICLocalStreamFlowController _local_flow_controller;
@@ -98,7 +88,6 @@ private:
   // TODO: Consider to replace with ts/RbTree.h or other data structure
   QUICIncomingStreamFrameBuffer _received_stream_frame_buffer;
 
-  // FIXME Unidirectional streams should use either ReceiveStreamState or SendStreamState
   QUICBidirectionalStreamStateMachine _state;
 
   // QUICFrameGenerator
diff --git a/iocore/net/quic/QUICPacket.h b/iocore/net/quic/QUICPacket.h
index e9d0212..49d0b14 100644
--- a/iocore/net/quic/QUICPacket.h
+++ b/iocore/net/quic/QUICPacket.h
@@ -275,7 +275,7 @@ private:
   QUICKeyPhase _key_phase;
   QUICPacketNumber _packet_number;
   int _packet_number_len;
-  QUICConnectionId _dcid;
+  QUICConnectionId _dcid = QUICConnectionId::ZERO();
 };
 
 class QUICStatelessResetPacket : public QUICPacket
diff --git a/iocore/net/quic/QUICPacketFactory.cc b/iocore/net/quic/QUICPacketFactory.cc
index 2fcb1a2..279cf7d 100644
--- a/iocore/net/quic/QUICPacketFactory.cc
+++ b/iocore/net/quic/QUICPacketFactory.cc
@@ -75,8 +75,8 @@ QUICPacketFactory::create(uint8_t *packet_buf, UDPConnection *udp_con, IpEndpoin
 
   QUICPacketType type;
   QUICVersion version;
-  QUICConnectionId dcid;
-  QUICConnectionId scid;
+  QUICConnectionId dcid = QUICConnectionId::ZERO();
+  QUICConnectionId scid = QUICConnectionId::ZERO();
   QUICPacketNumber packet_number;
   QUICKeyPhase key_phase;
 
diff --git a/iocore/net/quic/QUICStream.cc b/iocore/net/quic/QUICStream.cc
index 93ac8f1..932d1c1 100644
--- a/iocore/net/quic/QUICStream.cc
+++ b/iocore/net/quic/QUICStream.cc
@@ -62,6 +62,13 @@ QUICStream::final_offset() const
   return 0;
 }
 
+void
+QUICStream::set_io_adapter(QUICStreamAdapter *adapter)
+{
+  this->_adapter = adapter;
+  this->_on_adapter_updated();
+}
+
 QUICOffset
 QUICStream::reordered_bytes() const
 {
@@ -154,6 +161,20 @@ QUICStream::_records_crypto_frame(QUICEncryptionLevel level, const QUICCryptoFra
 }
 
 void
+QUICStream::set_state_listener(QUICStreamStateListener *listener)
+{
+  this->_state_listener = listener;
+}
+
+void
+QUICStream::_notify_state_change()
+{
+  if (this->_state_listener) {
+    // TODO Check own state and call an appropriate callback function
+  }
+}
+
+void
 QUICStream::reset(QUICStreamErrorUPtr error)
 {
 }
@@ -184,140 +205,3 @@ void
 QUICStream::on_read()
 {
 }
-
-//
-// QUICStreamVConnection
-//
-QUICStreamVConnection::~QUICStreamVConnection()
-{
-  if (this->_read_event) {
-    this->_read_event->cancel();
-    this->_read_event = nullptr;
-  }
-
-  if (this->_write_event) {
-    this->_write_event->cancel();
-    this->_write_event = nullptr;
-  }
-}
-
-void
-QUICStreamVConnection::_write_to_read_vio(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin)
-{
-  SCOPED_MUTEX_LOCK(lock, this->_read_vio.mutex, this_ethread());
-
-  uint64_t bytes_added = this->_read_vio.buffer.writer()->write(data, data_length);
-
-  // Until receive FIN flag, keep nbytes INT64_MAX
-  if (fin && bytes_added == data_length) {
-    this->_read_vio.nbytes = offset + data_length;
-  }
-}
-
-/**
- * Replace existing event only if the new event is different than the inprogress event
- */
-Event *
-QUICStreamVConnection::_send_tracked_event(Event *event, int send_event, VIO *vio)
-{
-  if (event != nullptr) {
-    if (event->callback_event != send_event) {
-      event->cancel();
-      event = nullptr;
-    }
-  }
-
-  if (event == nullptr) {
-    event = this_ethread()->schedule_imm(this, send_event, vio);
-  }
-
-  return event;
-}
-
-/**
- * @brief Signal event to this->_read_vio.cont
- */
-void
-QUICStreamVConnection::_signal_read_event()
-{
-  if (this->_read_vio.cont == nullptr || this->_read_vio.op == VIO::NONE) {
-    return;
-  }
-  MUTEX_TRY_LOCK(lock, this->_read_vio.mutex, this_ethread());
-
-  int event = this->_read_vio.nbytes == INT64_MAX ? VC_EVENT_READ_READY : VC_EVENT_READ_COMPLETE;
-
-  if (lock.is_locked()) {
-    this->_read_vio.cont->handleEvent(event, &this->_read_vio);
-  } else {
-    this_ethread()->schedule_imm(this->_read_vio.cont, event, &this->_read_vio);
-  }
-}
-
-/**
- * @brief Signal event to this->_write_vio.cont
- */
-void
-QUICStreamVConnection::_signal_write_event()
-{
-  if (this->_write_vio.cont == nullptr || this->_write_vio.op == VIO::NONE) {
-    return;
-  }
-  MUTEX_TRY_LOCK(lock, this->_write_vio.mutex, this_ethread());
-
-  int event = this->_write_vio.ntodo() ? VC_EVENT_WRITE_READY : VC_EVENT_WRITE_COMPLETE;
-
-  if (lock.is_locked()) {
-    this->_write_vio.cont->handleEvent(event, &this->_write_vio);
-  } else {
-    this_ethread()->schedule_imm(this->_write_vio.cont, event, &this->_write_vio);
-  }
-}
-
-/**
- * @brief Signal event to this->_write_vio.cont
- */
-void
-QUICStreamVConnection::_signal_read_eos_event()
-{
-  if (this->_read_vio.cont == nullptr || this->_read_vio.op == VIO::NONE) {
-    return;
-  }
-  MUTEX_TRY_LOCK(lock, this->_read_vio.mutex, this_ethread());
-
-  int event = VC_EVENT_EOS;
-
-  if (lock.is_locked()) {
-    this->_write_vio.cont->handleEvent(event, &this->_write_vio);
-  } else {
-    this_ethread()->schedule_imm(this->_read_vio.cont, event, &this->_read_vio);
-  }
-}
-
-int64_t
-QUICStreamVConnection::_process_read_vio()
-{
-  if (this->_read_vio.cont == nullptr || this->_read_vio.op == VIO::NONE) {
-    return 0;
-  }
-
-  // Pass through. Read operation is done by QUICStream::recv(const std::shared_ptr<const QUICStreamFrame> frame)
-  // TODO: 1. pop frame from _received_stream_frame_buffer
-  //       2. write data to _read_vio
-
-  return 0;
-}
-
-/**
- * @brief Send STREAM DATA from _response_buffer
- * @detail Call _signal_write_event() to indicate event upper layer
- */
-int64_t
-QUICStreamVConnection::_process_write_vio()
-{
-  if (this->_write_vio.cont == nullptr || this->_write_vio.op == VIO::NONE) {
-    return 0;
-  }
-
-  return 0;
-}
diff --git a/iocore/net/quic/QUICStream.h b/iocore/net/quic/QUICStream.h
index afeb03c..d6cdf06 100644
--- a/iocore/net/quic/QUICStream.h
+++ b/iocore/net/quic/QUICStream.h
@@ -37,6 +37,9 @@
 #include "QUICFrameRetransmitter.h"
 #include "QUICDebugNames.h"
 
+class QUICStreamAdapter;
+class QUICStreamStateListener;
+
 /**
  * @brief QUIC Stream
  * TODO: This is similar to Http2Stream. Need to think some integration.
@@ -54,6 +57,14 @@ public:
   bool is_bidirectional() const;
   QUICOffset final_offset() const;
 
+  /**
+   * Set an adapter to read/write data from/to this stream
+   *
+   * This is an interface for QUICApplication. An application can set an adapter
+   * to access data in the  way the applications wants.
+   */
+  void set_io_adapter(QUICStreamAdapter *adapter);
+
   /*
    * QUICApplication need to call one of these functions when it process VC_EVENT_*
    */
@@ -74,6 +85,8 @@ public:
   virtual void stop_sending(QUICStreamErrorUPtr error);
   virtual void reset(QUICStreamErrorUPtr error);
 
+  void set_state_listener(QUICStreamStateListener *listener);
+
   LINK(QUICStream, link);
 
 protected:
@@ -82,41 +95,23 @@ protected:
   QUICOffset _send_offset                      = 0;
   QUICOffset _reordered_bytes                  = 0;
 
+  QUICStreamAdapter *_adapter              = nullptr;
+  QUICStreamStateListener *_state_listener = nullptr;
+
+  virtual void _on_adapter_updated(){};
+
+  void _notify_state_change();
+
   void _records_rst_stream_frame(QUICEncryptionLevel level, const QUICRstStreamFrame &frame);
   void _records_stream_frame(QUICEncryptionLevel level, const QUICStreamFrame &frame);
   void _records_stop_sending_frame(QUICEncryptionLevel level, const QUICStopSendingFrame &frame);
   void _records_crypto_frame(QUICEncryptionLevel level, const QUICCryptoFrame &frame);
 };
 
-// This is VConnection class for VIO operation.
-class QUICStreamVConnection : public VConnection, public QUICStream
+class QUICStreamStateListener
 {
 public:
-  QUICStreamVConnection(QUICConnectionInfoProvider *cinfo, QUICStreamId sid) : VConnection(nullptr), QUICStream(cinfo, sid)
-  {
-    mutex = new_ProxyMutex();
-  }
-
-  QUICStreamVConnection() : VConnection(nullptr) {}
-  virtual ~QUICStreamVConnection();
-
-  LINK(QUICStreamVConnection, link);
-
-protected:
-  virtual int64_t _process_read_vio();
-  virtual int64_t _process_write_vio();
-  void _signal_read_event();
-  void _signal_write_event();
-  void _signal_read_eos_event();
-  Event *_send_tracked_event(Event *, int, VIO *);
-
-  void _write_to_read_vio(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin);
-
-  VIO _read_vio;
-  VIO _write_vio;
-
-  Event *_read_event  = nullptr;
-  Event *_write_event = nullptr;
+  virtual void on_stream_state_close(const QUICStream *stream) = 0;
 };
 
 #define QUICStreamDebug(fmt, ...)                                                                        \
diff --git a/proxy/http3/Http3FrameGenerator.h b/iocore/net/quic/QUICStreamAdapter.cc
similarity index 78%
copy from proxy/http3/Http3FrameGenerator.h
copy to iocore/net/quic/QUICStreamAdapter.cc
index 6da188f..8534781 100644
--- a/proxy/http3/Http3FrameGenerator.h
+++ b/iocore/net/quic/QUICStreamAdapter.cc
@@ -21,14 +21,12 @@
  *  limitations under the License.
  */
 
-#pragma once
+#include "QUICStreamAdapter.h"
 
-#include "Http3Frame.h"
-
-class Http3FrameGenerator
+Ptr<IOBufferBlock>
+QUICStreamAdapter::read(size_t len)
 {
-public:
-  virtual ~Http3FrameGenerator(){};
-  virtual Http3FrameUPtr generate_frame(uint16_t max_size) = 0;
-  virtual bool is_done() const                             = 0;
-};
+  auto ret = this->_read(len);
+  this->_stream.on_read();
+  return ret;
+}
diff --git a/proxy/http3/Http09App.h b/iocore/net/quic/QUICStreamAdapter.h
similarity index 50%
copy from proxy/http3/Http09App.h
copy to iocore/net/quic/QUICStreamAdapter.h
index ab35399..3c3a0e3 100644
--- a/proxy/http3/Http09App.h
+++ b/iocore/net/quic/QUICStreamAdapter.h
@@ -23,29 +23,43 @@
 
 #pragma once
 
-#include "IPAllow.h"
+#include "QUICStream.h"
 
-#include "HttpSessionAccept.h"
+class QUICStreamAdapter
+{
+public:
+  QUICStreamAdapter(QUICStream &stream) : _stream(stream) {}
+  virtual ~QUICStreamAdapter() = default;
 
-#include "QUICApplication.h"
+  QUICStream &
+  stream()
+  {
+    return _stream;
+  }
 
-class QUICNetVConnection;
-class Http09Session;
+  virtual int64_t write(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin) = 0;
+  Ptr<IOBufferBlock> read(size_t len);
+  virtual bool is_eos()         = 0;
+  virtual uint64_t unread_len() = 0;
+  virtual uint64_t read_len()   = 0;
+  virtual uint64_t total_len()  = 0;
 
-/**
- * @brief A simple multi-streamed application.
- * @detail Response to simple HTTP/0.9 GETs
- * This will be removed when HTTP/0.9 over QUIC support is dropped
- *
- */
-class Http09App : public QUICApplication
-{
-public:
-  Http09App(QUICNetVConnection *client_vc, IpAllow::ACL &&session_acl, const HttpSessionAccept::Options &options);
-  ~Http09App();
+  /**
+   * Tell the application that there is data to read
+   */
+  virtual void encourge_read() = 0;
+
+  /**
+   * Tell the application that there is some space to write data
+   */
+  virtual void encourge_write() = 0;
 
-  int main_event_handler(int event, Event *data);
+  /**
+   * Tell the application that there is no more data to read
+   */
+  virtual void notify_eos() = 0;
 
-private:
-  Http09Session *_ssn = nullptr;
+protected:
+  virtual Ptr<IOBufferBlock> _read(size_t len) = 0;
+  QUICStream &_stream;
 };
diff --git a/iocore/net/quic/QUICStreamFactory.cc b/iocore/net/quic/QUICStreamFactory.cc
index 548be10..1f09c75 100644
--- a/iocore/net/quic/QUICStreamFactory.cc
+++ b/iocore/net/quic/QUICStreamFactory.cc
@@ -26,10 +26,10 @@
 #include "QUICUnidirectionalStream.h"
 #include "QUICStreamFactory.h"
 
-QUICStreamVConnection *
+QUICStream *
 QUICStreamFactory::create(QUICStreamId sid, uint64_t local_max_stream_data, uint64_t remote_max_stream_data)
 {
-  QUICStreamVConnection *stream = nullptr;
+  QUICStream *stream = nullptr;
   switch (QUICTypeUtil::detect_stream_direction(sid, this->_info->direction())) {
   case QUICStreamDirection::BIDIRECTIONAL:
     stream = new QUICBidirectionalStream(this->_rtt_provider, this->_info, sid, local_max_stream_data, remote_max_stream_data);
@@ -50,7 +50,7 @@ QUICStreamFactory::create(QUICStreamId sid, uint64_t local_max_stream_data, uint
 }
 
 void
-QUICStreamFactory::delete_stream(QUICStreamVConnection *stream)
+QUICStreamFactory::delete_stream(QUICStream *stream)
 {
   delete stream;
 }
diff --git a/iocore/net/quic/QUICStreamFactory.h b/iocore/net/quic/QUICStreamFactory.h
index fd497bf..43690ed 100644
--- a/iocore/net/quic/QUICStreamFactory.h
+++ b/iocore/net/quic/QUICStreamFactory.h
@@ -25,7 +25,7 @@
 
 #include "QUICTypes.h"
 
-class QUICStreamVConnection;
+class QUICStream;
 
 // PS: this class function should not static because of  THREAD_ALLOC and THREAD_FREE
 class QUICStreamFactory
@@ -35,10 +35,10 @@ public:
   ~QUICStreamFactory() {}
 
   // create a bidistream, send only stream or receive only stream
-  QUICStreamVConnection *create(QUICStreamId sid, uint64_t recv_max_stream_data, uint64_t send_max_stream_data);
+  QUICStream *create(QUICStreamId sid, uint64_t recv_max_stream_data, uint64_t send_max_stream_data);
 
   // delete stream by stream type
-  void delete_stream(QUICStreamVConnection *stream);
+  void delete_stream(QUICStream *stream);
 
 private:
   QUICRTTProvider *_rtt_provider    = nullptr;
diff --git a/iocore/net/quic/QUICStreamManager.cc b/iocore/net/quic/QUICStreamManager.cc
index 54a6d20..2338321 100644
--- a/iocore/net/quic/QUICStreamManager.cc
+++ b/iocore/net/quic/QUICStreamManager.cc
@@ -94,17 +94,14 @@ QUICConnectionErrorUPtr
 QUICStreamManager::create_stream(QUICStreamId stream_id)
 {
   // TODO: check stream_id
-  QUICConnectionErrorUPtr error    = nullptr;
-  QUICStreamVConnection *stream_vc = this->_find_or_create_stream_vc(stream_id);
-  if (!stream_vc) {
+  QUICConnectionErrorUPtr error = nullptr;
+  QUICStream *stream            = this->_find_or_create_stream(stream_id);
+  if (!stream) {
     return std::make_unique<QUICConnectionError>(QUICTransErrorCode::STREAM_LIMIT_ERROR);
   }
 
   QUICApplication *application = this->_app_map->get(stream_id);
-
-  if (!application->is_stream_set(stream_vc)) {
-    application->set_stream(stream_vc);
-  }
+  application->on_new_stream(*stream);
 
   return error;
 }
@@ -136,7 +133,7 @@ QUICStreamManager::create_bidi_stream(QUICStreamId &new_stream_id)
 void
 QUICStreamManager::reset_stream(QUICStreamId stream_id, QUICStreamErrorUPtr error)
 {
-  auto stream = this->_find_stream_vc(stream_id);
+  auto stream = this->_find_stream(stream_id);
   stream->reset(std::move(error));
 }
 
@@ -177,7 +174,7 @@ QUICStreamManager::handle_frame(QUICEncryptionLevel level, const QUICFrame &fram
 QUICConnectionErrorUPtr
 QUICStreamManager::_handle_frame(const QUICMaxStreamDataFrame &frame)
 {
-  QUICStreamVConnection *stream = this->_find_or_create_stream_vc(frame.stream_id());
+  QUICStream *stream = this->_find_or_create_stream(frame.stream_id());
   if (stream) {
     return stream->recv(frame);
   } else {
@@ -188,7 +185,7 @@ QUICStreamManager::_handle_frame(const QUICMaxStreamDataFrame &frame)
 QUICConnectionErrorUPtr
 QUICStreamManager::_handle_frame(const QUICStreamDataBlockedFrame &frame)
 {
-  QUICStreamVConnection *stream = this->_find_or_create_stream_vc(frame.stream_id());
+  QUICStream *stream = this->_find_or_create_stream(frame.stream_id());
   if (stream) {
     return stream->recv(frame);
   } else {
@@ -199,24 +196,18 @@ QUICStreamManager::_handle_frame(const QUICStreamDataBlockedFrame &frame)
 QUICConnectionErrorUPtr
 QUICStreamManager::_handle_frame(const QUICStreamFrame &frame)
 {
-  QUICStreamVConnection *stream = this->_find_or_create_stream_vc(frame.stream_id());
-  if (!stream) {
+  QUICStream *stream = this->_find_or_create_stream(frame.stream_id());
+  if (stream) {
+    return stream->recv(frame);
+  } else {
     return std::make_unique<QUICConnectionError>(QUICTransErrorCode::STREAM_LIMIT_ERROR);
   }
-
-  QUICApplication *application = this->_app_map->get(frame.stream_id());
-
-  if (application && !application->is_stream_set(stream)) {
-    application->set_stream(stream);
-  }
-
-  return stream->recv(frame);
 }
 
 QUICConnectionErrorUPtr
 QUICStreamManager::_handle_frame(const QUICRstStreamFrame &frame)
 {
-  QUICStream *stream = this->_find_or_create_stream_vc(frame.stream_id());
+  QUICStream *stream = this->_find_or_create_stream(frame.stream_id());
   if (stream) {
     return stream->recv(frame);
   } else {
@@ -227,7 +218,7 @@ QUICStreamManager::_handle_frame(const QUICRstStreamFrame &frame)
 QUICConnectionErrorUPtr
 QUICStreamManager::_handle_frame(const QUICStopSendingFrame &frame)
 {
-  QUICStream *stream = this->_find_or_create_stream_vc(frame.stream_id());
+  QUICStream *stream = this->_find_or_create_stream(frame.stream_id());
   if (stream) {
     return stream->recv(frame);
   } else {
@@ -247,10 +238,10 @@ QUICStreamManager::_handle_frame(const QUICMaxStreamsFrame &frame)
   return nullptr;
 }
 
-QUICStreamVConnection *
-QUICStreamManager::_find_stream_vc(QUICStreamId id)
+QUICStream *
+QUICStreamManager::_find_stream(QUICStreamId id)
 {
-  for (QUICStreamVConnection *s = this->stream_list.head; s; s = s->link.next) {
+  for (QUICStream *s = this->stream_list.head; s; s = s->link.next) {
     if (s->id() == id) {
       return s;
     }
@@ -258,10 +249,10 @@ QUICStreamManager::_find_stream_vc(QUICStreamId id)
   return nullptr;
 }
 
-QUICStreamVConnection *
-QUICStreamManager::_find_or_create_stream_vc(QUICStreamId stream_id)
+QUICStream *
+QUICStreamManager::_find_or_create_stream(QUICStreamId stream_id)
 {
-  QUICStreamVConnection *stream = this->_find_stream_vc(stream_id);
+  QUICStream *stream = this->_find_stream(stream_id);
   if (!stream) {
     if (!this->_local_tp) {
       return nullptr;
@@ -353,7 +344,11 @@ QUICStreamManager::_find_or_create_stream_vc(QUICStreamId stream_id)
 
     stream = this->_stream_factory.create(stream_id, local_max_stream_data, remote_max_stream_data);
     ink_assert(stream != nullptr);
+    stream->set_state_listener(this);
     this->stream_list.push(stream);
+
+    QUICApplication *application = this->_app_map->get(stream_id);
+    application->on_new_stream(*stream);
   }
 
   return stream;
@@ -365,7 +360,7 @@ QUICStreamManager::total_reordered_bytes() const
   uint64_t total_bytes = 0;
 
   // FIXME Iterating all (open + closed) streams is expensive
-  for (QUICStreamVConnection *s = this->stream_list.head; s; s = s->link.next) {
+  for (QUICStream *s = this->stream_list.head; s; s = s->link.next) {
     total_bytes += s->reordered_bytes();
   }
   return total_bytes;
@@ -377,7 +372,7 @@ QUICStreamManager::total_offset_received() const
   uint64_t total_offset_received = 0;
 
   // FIXME Iterating all (open + closed) streams is expensive
-  for (QUICStreamVConnection *s = this->stream_list.head; s; s = s->link.next) {
+  for (QUICStream *s = this->stream_list.head; s; s = s->link.next) {
     total_offset_received += s->largest_offset_received();
   }
   return total_offset_received;
@@ -400,7 +395,7 @@ uint32_t
 QUICStreamManager::stream_count() const
 {
   uint32_t count = 0;
-  for (QUICStreamVConnection *s = this->stream_list.head; s; s = s->link.next) {
+  for (QUICStream *s = this->stream_list.head; s; s = s->link.next) {
     ++count;
   }
   return count;
@@ -429,7 +424,7 @@ QUICStreamManager::will_generate_frame(QUICEncryptionLevel level, size_t current
     return false;
   }
 
-  for (QUICStreamVConnection *s = this->stream_list.head; s; s = s->link.next) {
+  for (QUICStream *s = this->stream_list.head; s; s = s->link.next) {
     if (s->will_generate_frame(level, current_packet_size, ack_eliciting, seq_num)) {
       return true;
     }
@@ -454,7 +449,7 @@ QUICStreamManager::generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint6
   }
 
   // FIXME We should pick a stream based on priority
-  for (QUICStreamVConnection *s = this->stream_list.head; s; s = s->link.next) {
+  for (QUICStream *s = this->stream_list.head; s; s = s->link.next) {
     frame = s->generate_frame(buf, level, connection_credit, maximum_frame_size, current_packet_size, seq_num);
     if (frame) {
       break;
@@ -468,6 +463,34 @@ QUICStreamManager::generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint6
   return frame;
 }
 
+void
+QUICStreamManager::on_stream_state_close(const QUICStream *stream)
+{
+  auto direction = this->_context->connection_info()->direction();
+  switch (QUICTypeUtil::detect_stream_type(stream->id())) {
+  case QUICStreamType::SERVER_BIDI:
+    if (direction == NET_VCONNECTION_OUT) {
+      this->_local_max_streams_bidi += 1;
+    }
+    break;
+  case QUICStreamType::SERVER_UNI:
+    if (direction == NET_VCONNECTION_OUT) {
+      this->_local_max_streams_uni += 1;
+    }
+    break;
+  case QUICStreamType::CLIENT_BIDI:
+    if (direction == NET_VCONNECTION_IN) {
+      this->_local_max_streams_bidi += 1;
+    }
+    break;
+  case QUICStreamType::CLIENT_UNI:
+    if (direction == NET_VCONNECTION_IN) {
+      this->_local_max_streams_uni += 1;
+    }
+    break;
+  }
+}
+
 bool
 QUICStreamManager::_is_level_matched(QUICEncryptionLevel level)
 {
diff --git a/iocore/net/quic/QUICStreamManager.h b/iocore/net/quic/QUICStreamManager.h
index f158035..a52a375 100644
--- a/iocore/net/quic/QUICStreamManager.h
+++ b/iocore/net/quic/QUICStreamManager.h
@@ -36,7 +36,7 @@
 
 class QUICTransportParameters;
 
-class QUICStreamManager : public QUICFrameHandler, public QUICFrameGenerator
+class QUICStreamManager : public QUICFrameHandler, public QUICFrameGenerator, public QUICStreamStateListener
 {
 public:
   QUICStreamManager(QUICContext *context, QUICApplicationMap *app_map);
@@ -58,7 +58,7 @@ public:
 
   void set_default_application(QUICApplication *app);
 
-  DLL<QUICStreamVConnection> stream_list;
+  DLL<QUICStream> stream_list;
 
   // QUICFrameHandler
   virtual std::vector<QUICFrameType> interests() override;
@@ -69,12 +69,15 @@ public:
   QUICFrame *generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint64_t connection_credit, uint16_t maximum_frame_size,
                             size_t current_packet_size, uint32_t timestamp) override;
 
+  // QUICStreamStateListener
+  void on_stream_state_close(const QUICStream *stream) override;
+
 protected:
   virtual bool _is_level_matched(QUICEncryptionLevel level) override;
 
 private:
-  QUICStreamVConnection *_find_stream_vc(QUICStreamId id);
-  QUICStreamVConnection *_find_or_create_stream_vc(QUICStreamId stream_id);
+  QUICStream *_find_stream(QUICStreamId id);
+  QUICStream *_find_or_create_stream(QUICStreamId stream_id);
   void _add_total_offset_sent(uint32_t sent_byte);
   QUICConnectionErrorUPtr _handle_frame(const QUICStreamFrame &frame);
   QUICConnectionErrorUPtr _handle_frame(const QUICRstStreamFrame &frame);
diff --git a/iocore/net/quic/QUICStreamState.cc b/iocore/net/quic/QUICStreamState.cc
index 7c5b8d1..e5ddb6c 100644
--- a/iocore/net/quic/QUICStreamState.cc
+++ b/iocore/net/quic/QUICStreamState.cc
@@ -72,14 +72,16 @@ QUICReceiveStreamStateMachine::is_allowed_to_receive(QUICFrameType type) const
   return false;
 }
 
-void
+bool
 QUICReceiveStreamStateMachine::update_with_sending_frame(const QUICFrame &frame)
 {
+  return false;
 }
 
-void
+bool
 QUICReceiveStreamStateMachine::update_with_receiving_frame(const QUICFrame &frame)
 {
+  bool state_changed = false;
   // The receiving part of a stream initiated by a peer (types 1 and 3 for a client, or 0 and 2 for a server) is created when the
   // first STREAM, STREAM_DATA_BLOCKED, or RESET_STREAM is received for that stream.
   QUICReceiveStreamState state = this->get();
@@ -87,32 +89,32 @@ QUICReceiveStreamStateMachine::update_with_receiving_frame(const QUICFrame &fram
 
   if (state == QUICReceiveStreamState::Init &&
       (type == QUICFrameType::STREAM || type == QUICFrameType::STREAM_DATA_BLOCKED || type == QUICFrameType::RESET_STREAM)) {
-    this->_set_state(QUICReceiveStreamState::Recv);
+    state_changed |= this->_set_state(QUICReceiveStreamState::Recv);
   }
 
   switch (this->get()) {
   case QUICReceiveStreamState::Recv:
     if (type == QUICFrameType::STREAM) {
       if (static_cast<const QUICStreamFrame &>(frame).has_fin_flag()) {
-        this->_set_state(QUICReceiveStreamState::SizeKnown);
+        state_changed |= this->_set_state(QUICReceiveStreamState::SizeKnown);
         if (this->_in_progress->is_transfer_complete()) {
-          this->_set_state(QUICReceiveStreamState::DataRecvd);
+          state_changed |= this->_set_state(QUICReceiveStreamState::DataRecvd);
         }
       }
     } else if (type == QUICFrameType::RESET_STREAM) {
-      this->_set_state(QUICReceiveStreamState::ResetRecvd);
+      state_changed |= this->_set_state(QUICReceiveStreamState::ResetRecvd);
     }
     break;
   case QUICReceiveStreamState::SizeKnown:
     if (type == QUICFrameType::STREAM && this->_in_progress->is_transfer_complete()) {
-      this->_set_state(QUICReceiveStreamState::DataRecvd);
+      state_changed |= this->_set_state(QUICReceiveStreamState::DataRecvd);
     } else if (type == QUICFrameType::RESET_STREAM) {
-      this->_set_state(QUICReceiveStreamState::ResetRecvd);
+      state_changed |= this->_set_state(QUICReceiveStreamState::ResetRecvd);
     }
     break;
   case QUICReceiveStreamState::DataRecvd:
     if (type == QUICFrameType::STREAM && this->_in_progress->is_transfer_complete()) {
-      this->_set_state(QUICReceiveStreamState::ResetRecvd);
+      state_changed |= this->_set_state(QUICReceiveStreamState::ResetRecvd);
     }
     break;
   case QUICReceiveStreamState::Init:
@@ -124,23 +126,25 @@ QUICReceiveStreamStateMachine::update_with_receiving_frame(const QUICFrame &fram
     ink_assert(!"Unknown state");
     break;
   }
+  return state_changed;
 }
 
-void
+bool
 QUICReceiveStreamStateMachine::update_on_read()
 {
   if (this->_in_progress->is_transfer_complete()) {
-    this->_set_state(QUICReceiveStreamState::DataRead);
+    return this->_set_state(QUICReceiveStreamState::DataRead);
   }
+  return false;
 }
 
-void
+bool
 QUICReceiveStreamStateMachine::update_on_eos()
 {
-  this->_set_state(QUICReceiveStreamState::ResetRead);
+  return this->_set_state(QUICReceiveStreamState::ResetRead);
 }
 
-void
+bool
 QUICReceiveStreamStateMachine::update(const QUICSendStreamState state)
 {
   // The receiving part of a stream enters the "Recv" state when the sending part of a bidirectional stream initiated by the
@@ -148,12 +152,14 @@ QUICReceiveStreamStateMachine::update(const QUICSendStreamState state)
   switch (this->get()) {
   case QUICReceiveStreamState::Init:
     if (state == QUICSendStreamState::Ready) {
-      this->_set_state(QUICReceiveStreamState::Recv);
+      return this->_set_state(QUICReceiveStreamState::Recv);
     }
     break;
   default:
     break;
   }
+
+  return false;
 }
 
 // ---------- QUICSendStreamState -------------
@@ -251,29 +257,30 @@ QUICSendStreamStateMachine::is_allowed_to_receive(QUICFrameType type) const
   return false;
 }
 
-void
+bool
 QUICSendStreamStateMachine::update_with_sending_frame(const QUICFrame &frame)
 {
+  bool state_changed        = false;
   QUICSendStreamState state = this->get();
   QUICFrameType type        = frame.type();
   if (state == QUICSendStreamState::Ready &&
       (type == QUICFrameType::STREAM || type == QUICFrameType::STREAM_DATA_BLOCKED || type == QUICFrameType::RESET_STREAM)) {
-    this->_set_state(QUICSendStreamState::Send);
+    state_changed |= this->_set_state(QUICSendStreamState::Send);
   }
 
   switch (this->get()) {
   case QUICSendStreamState::Send:
     if (type == QUICFrameType::STREAM) {
       if (static_cast<const QUICStreamFrame &>(frame).has_fin_flag()) {
-        this->_set_state(QUICSendStreamState::DataSent);
+        state_changed |= this->_set_state(QUICSendStreamState::DataSent);
       }
     } else if (type == QUICFrameType::RESET_STREAM) {
-      this->_set_state(QUICSendStreamState::ResetSent);
+      state_changed |= this->_set_state(QUICSendStreamState::ResetSent);
     }
     break;
   case QUICSendStreamState::DataSent:
     if (type == QUICFrameType::RESET_STREAM) {
-      this->_set_state(QUICSendStreamState::ResetSent);
+      state_changed |= this->_set_state(QUICSendStreamState::ResetSent);
     }
     break;
   case QUICSendStreamState::Init:
@@ -286,37 +293,42 @@ QUICSendStreamStateMachine::update_with_sending_frame(const QUICFrame &frame)
     ink_assert(!"Unknown state");
     break;
   }
+  return state_changed;
 }
 
-void
+bool
 QUICSendStreamStateMachine::update_with_receiving_frame(const QUICFrame &frame)
 {
+  return false;
 }
 
-void
+bool
 QUICSendStreamStateMachine::update_on_ack()
 {
   if (this->_out_progress->is_transfer_complete()) {
-    this->_set_state(QUICSendStreamState::DataRecvd);
+    return this->_set_state(QUICSendStreamState::DataRecvd);
   } else if (this->_out_progress->is_cancelled()) {
-    this->_set_state(QUICSendStreamState::ResetRecvd);
+    return this->_set_state(QUICSendStreamState::ResetRecvd);
   }
+  return false;
 }
 
-void
+bool
 QUICSendStreamStateMachine::update(const QUICReceiveStreamState state)
 {
+  bool state_changed = false;
   // The sending part of a bidirectional stream initiated by a peer (type 0 for a server, type 1 for a client) enters the "Ready"
   // state then immediately transitions to the "Send" state if the receiving part enters the "Recv" state (Section 3.2).
   switch (this->get()) {
   case QUICSendStreamState::Ready:
     if (state == QUICReceiveStreamState::Recv) {
-      this->_set_state(QUICSendStreamState::Send);
+      state_changed |= this->_set_state(QUICSendStreamState::Send);
     }
     break;
   default:
     break;
   }
+  return state_changed;
 }
 
 // ---------QUICBidirectionalStreamState -----------
@@ -369,48 +381,56 @@ QUICBidirectionalStreamStateMachine::get() const
   }
 }
 
-void
+bool
 QUICBidirectionalStreamStateMachine::update_with_sending_frame(const QUICFrame &frame)
 {
+  bool state_changed = false;
+
   // The receiving part of a stream enters the "Recv" state when the sending part of a bidirectional stream initiated by the
   // endpoint (type 0 for a client, type 1 for a server) enters the "Ready" state.
-  this->_send_stream_state.update_with_sending_frame(frame);
+  state_changed |= this->_send_stream_state.update_with_sending_frame(frame);
   // PS: It should not happen because we initialize the send side and read side together. And the SendState has the default state
   // "Ready". But to obey the specs, we do this as follow.
   if (this->_send_stream_state.get() == QUICSendStreamState::Ready &&
       this->_recv_stream_state.get() == QUICReceiveStreamState::Init) {
-    this->_recv_stream_state.update(this->_send_stream_state.get());
+    state_changed |= this->_recv_stream_state.update(this->_send_stream_state.get());
   }
+
+  return state_changed;
 }
 
-void
+bool
 QUICBidirectionalStreamStateMachine::update_with_receiving_frame(const QUICFrame &frame)
 {
+  bool state_changed = false;
+
   // The sending part of a bidirectional stream initiated by a peer (type 0 for a server, type 1 for a client) enters the "Ready"
   // state then immediately transitions to the "Send" state if the receiving part enters the "Recv" state (Section 3.2).
-  this->_recv_stream_state.update_with_receiving_frame(frame);
+  state_changed |= this->_recv_stream_state.update_with_receiving_frame(frame);
   if (this->_send_stream_state.get() == QUICSendStreamState::Ready &&
       this->_recv_stream_state.get() == QUICReceiveStreamState::Recv) {
-    this->_send_stream_state.update(this->_recv_stream_state.get());
+    state_changed |= this->_send_stream_state.update(this->_recv_stream_state.get());
   }
+
+  return state_changed;
 }
 
-void
+bool
 QUICBidirectionalStreamStateMachine::update_on_ack()
 {
-  this->_send_stream_state.update_on_ack();
+  return this->_send_stream_state.update_on_ack();
 }
 
-void
+bool
 QUICBidirectionalStreamStateMachine::update_on_read()
 {
-  this->_recv_stream_state.update_on_read();
+  return this->_recv_stream_state.update_on_read();
 }
 
-void
+bool
 QUICBidirectionalStreamStateMachine::update_on_eos()
 {
-  this->_recv_stream_state.update_on_eos();
+  return this->_recv_stream_state.update_on_eos();
 }
 
 bool
diff --git a/iocore/net/quic/QUICStreamState.h b/iocore/net/quic/QUICStreamState.h
index f95bfc0..235bfa3 100644
--- a/iocore/net/quic/QUICStreamState.h
+++ b/iocore/net/quic/QUICStreamState.h
@@ -67,8 +67,8 @@ public:
     return this->_state;
   }
 
-  virtual void update_with_sending_frame(const QUICFrame &frame)   = 0;
-  virtual void update_with_receiving_frame(const QUICFrame &frame) = 0;
+  [[nodiscard]] virtual bool update_with_sending_frame(const QUICFrame &frame)   = 0;
+  [[nodiscard]] virtual bool update_with_receiving_frame(const QUICFrame &frame) = 0;
 
   virtual bool is_allowed_to_send(QUICFrameType type) const        = 0;
   virtual bool is_allowed_to_send(const QUICFrame &frame) const    = 0;
@@ -76,11 +76,16 @@ public:
   virtual bool is_allowed_to_receive(const QUICFrame &frame) const = 0;
 
 protected:
-  void
+  bool
   _set_state(T s)
   {
     ink_assert(s != T::Init);
-    this->_state = s;
+    if (this->_state != s) {
+      this->_state = s;
+      return true;
+    } else {
+      return false;
+    }
   }
 
 private:
@@ -109,16 +114,16 @@ public:
     this->_set_state(QUICSendStreamState::Ready);
   }
 
-  void update_with_sending_frame(const QUICFrame &frame) override;
-  void update_with_receiving_frame(const QUICFrame &frame) override;
-  void update_on_ack();
+  [[nodiscard]] bool update_with_sending_frame(const QUICFrame &frame) override;
+  [[nodiscard]] bool update_with_receiving_frame(const QUICFrame &frame) override;
+  [[nodiscard]] bool update_on_ack();
 
   bool is_allowed_to_send(QUICFrameType type) const override;
   bool is_allowed_to_send(const QUICFrame &frame) const override;
   bool is_allowed_to_receive(QUICFrameType type) const override;
   bool is_allowed_to_receive(const QUICFrame &frame) const override;
 
-  void update(const QUICReceiveStreamState opposite_side);
+  [[nodiscard]] bool update(const QUICReceiveStreamState opposite_side);
 };
 
 class QUICReceiveStreamStateMachine : public QUICUnidirectionalStreamStateMachine,
@@ -130,17 +135,17 @@ public:
   {
   }
 
-  void update_with_sending_frame(const QUICFrame &frame) override;
-  void update_with_receiving_frame(const QUICFrame &frame) override;
-  void update_on_read();
-  void update_on_eos();
+  [[nodiscard]] bool update_with_sending_frame(const QUICFrame &frame) override;
+  [[nodiscard]] bool update_with_receiving_frame(const QUICFrame &frame) override;
+  [[nodiscard]] bool update_on_read();
+  [[nodiscard]] bool update_on_eos();
 
   bool is_allowed_to_send(QUICFrameType type) const override;
   bool is_allowed_to_send(const QUICFrame &frame) const override;
   bool is_allowed_to_receive(QUICFrameType type) const override;
   bool is_allowed_to_receive(const QUICFrame &frame) const override;
 
-  void update(const QUICSendStreamState opposite_side);
+  [[nodiscard]] bool update(const QUICSendStreamState opposite_side);
 };
 
 class QUICBidirectionalStreamStateMachine : public QUICStreamStateMachine<QUICBidirectionalStreamState>
@@ -150,16 +155,16 @@ public:
                                       QUICTransferProgressProvider *recv_in, QUICTransferProgressProvider *recv_out)
     : _send_stream_state(send_in, send_out), _recv_stream_state(recv_in, recv_out)
   {
-    this->_recv_stream_state.update(this->_send_stream_state.get());
+    ink_assert(this->_recv_stream_state.update(this->_send_stream_state.get()));
   };
 
   QUICBidirectionalStreamState get() const override;
 
-  void update_with_sending_frame(const QUICFrame &frame) override;
-  void update_with_receiving_frame(const QUICFrame &frame) override;
-  void update_on_ack();
-  void update_on_read();
-  void update_on_eos();
+  [[nodiscard]] bool update_with_sending_frame(const QUICFrame &frame) override;
+  [[nodiscard]] bool update_with_receiving_frame(const QUICFrame &frame) override;
+  [[nodiscard]] bool update_on_ack();
+  [[nodiscard]] bool update_on_read();
+  [[nodiscard]] bool update_on_eos();
 
   bool is_allowed_to_send(QUICFrameType type) const override;
   bool is_allowed_to_send(const QUICFrame &frame) const override;
diff --git a/iocore/net/quic/QUICStreamVCAdapter.cc b/iocore/net/quic/QUICStreamVCAdapter.cc
new file mode 100644
index 0000000..b8c4afc
--- /dev/null
+++ b/iocore/net/quic/QUICStreamVCAdapter.cc
@@ -0,0 +1,320 @@
+/** @file
+ *
+ *  A brief file description
+ *
+ *  @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 "I_VConnection.h"
+#include "QUICStreamVCAdapter.h"
+
+QUICStreamVCAdapter::QUICStreamVCAdapter(QUICStream &stream) : QUICStreamAdapter(stream), VConnection(new_ProxyMutex())
+{
+  SET_HANDLER(&QUICStreamVCAdapter::state_stream_open);
+}
+
+QUICStreamVCAdapter::~QUICStreamVCAdapter()
+{
+  if (this->_read_event) {
+    this->_read_event->cancel();
+    this->_read_event = nullptr;
+  }
+
+  if (this->_write_event) {
+    this->_write_event->cancel();
+    this->_write_event = nullptr;
+  }
+}
+
+int64_t
+QUICStreamVCAdapter::write(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin)
+{
+  uint64_t bytes_added = -1;
+  if (this->_read_vio.op == VIO::READ) {
+    SCOPED_MUTEX_LOCK(lock, this->_read_vio.mutex, this_ethread());
+
+    bytes_added = this->_read_vio.get_writer()->write(data, data_length);
+
+    // Until receive FIN flag, keep nbytes INT64_MAX
+    if (fin && bytes_added == data_length) {
+      this->_read_vio.nbytes = offset + data_length;
+    }
+  }
+
+  return bytes_added;
+}
+
+Ptr<IOBufferBlock>
+QUICStreamVCAdapter::_read(size_t len)
+{
+  Ptr<IOBufferBlock> block;
+
+  if (this->_write_vio.op == VIO::WRITE) {
+    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+
+    IOBufferReader *reader = this->_write_vio.get_reader();
+    block                  = make_ptr<IOBufferBlock>(reader->get_current_block()->clone());
+    if (block->size()) {
+      block->consume(reader->start_offset);
+      block->_end = std::min(block->start() + len, block->_buf_end);
+      this->_write_vio.ndone += len;
+    }
+    reader->consume(block->size());
+  }
+
+  return block;
+}
+
+bool
+QUICStreamVCAdapter::is_eos()
+{
+  if (this->_write_vio.op == VIO::WRITE) {
+    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+
+    if (this->_write_vio.nbytes == INT64_MAX) {
+      return false;
+    }
+    if (this->_write_vio.ntodo() != 0) {
+      return false;
+    }
+    return true;
+  } else {
+    return false;
+  }
+}
+
+uint64_t
+QUICStreamVCAdapter::unread_len()
+{
+  if (this->_write_vio.op == VIO::WRITE) {
+    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+    return this->_write_vio.get_reader()->block_read_avail();
+  } else {
+    return 0;
+  }
+}
+
+uint64_t
+QUICStreamVCAdapter::read_len()
+{
+  if (this->_write_vio.op == VIO::WRITE) {
+    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+    return this->_write_vio.ndone;
+  } else {
+    return 0;
+  }
+}
+
+uint64_t
+QUICStreamVCAdapter::total_len()
+{
+  if (this->_write_vio.op == VIO::WRITE) {
+    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+    return this->_write_vio.nbytes;
+  } else {
+    return 0;
+  }
+}
+
+/**
+ * @brief Signal event to this->_read_vio.cont
+ */
+void
+QUICStreamVCAdapter::encourge_read()
+{
+  if (this->_read_vio.op == VIO::READ) {
+    SCOPED_MUTEX_LOCK(lock, this->_read_vio.mutex, this_ethread());
+
+    if (this->_read_vio.cont == nullptr) {
+      return;
+    }
+
+    int event = this->_read_vio.nbytes == INT64_MAX ? VC_EVENT_READ_READY : VC_EVENT_READ_COMPLETE;
+    this_ethread()->schedule_imm(this->_read_vio.cont, event, &this->_read_vio);
+  }
+}
+
+/**
+ * @brief Signal event to this->_write_vio.cont
+ */
+void
+QUICStreamVCAdapter::encourge_write()
+{
+  if (this->_write_vio.op == VIO::WRITE) {
+    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+
+    if (this->_write_vio.cont == nullptr) {
+      return;
+    }
+
+    int event = this->_write_vio.ntodo() ? VC_EVENT_WRITE_READY : VC_EVENT_WRITE_COMPLETE;
+    this_ethread()->schedule_imm(this->_write_vio.cont, event, &this->_write_vio);
+  }
+}
+
+/**
+ * @brief Signal event to this->_read_vio.cont
+ */
+void
+QUICStreamVCAdapter::notify_eos()
+{
+  if (this->_read_vio.op == VIO::READ) {
+    if (this->_read_vio.cont == nullptr) {
+      return;
+    }
+    int event = VC_EVENT_EOS;
+
+    MUTEX_TRY_LOCK(lock, this->_read_vio.mutex, this_ethread());
+    if (lock.is_locked()) {
+      this->_read_vio.cont->handleEvent(event, &this->_read_vio);
+    } else {
+      this_ethread()->schedule_imm(this->_read_vio.cont, event, &this->_read_vio);
+    }
+  }
+}
+
+// this->_read_vio.nbytes should be INT64_MAX until receive FIN flag
+VIO *
+QUICStreamVCAdapter::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
+{
+  if (buf) {
+    this->_read_vio.buffer.writer_for(buf);
+  } else {
+    this->_read_vio.buffer.clear();
+  }
+
+  this->_read_vio.mutex     = c ? c->mutex : this->mutex;
+  this->_read_vio.cont      = c;
+  this->_read_vio.nbytes    = nbytes;
+  this->_read_vio.ndone     = 0;
+  this->_read_vio.vc_server = this;
+  this->_read_vio.op        = VIO::READ;
+
+  return &this->_read_vio;
+}
+
+VIO *
+QUICStreamVCAdapter::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, bool owner)
+{
+  if (buf) {
+    this->_write_vio.buffer.reader_for(buf);
+  } else {
+    this->_write_vio.buffer.clear();
+  }
+
+  this->_write_vio.mutex     = c ? c->mutex : this->mutex;
+  this->_write_vio.cont      = c;
+  this->_write_vio.nbytes    = nbytes;
+  this->_write_vio.ndone     = 0;
+  this->_write_vio.vc_server = this;
+  this->_write_vio.op        = VIO::WRITE;
+
+  return &this->_write_vio;
+}
+
+void
+QUICStreamVCAdapter::do_io_close(int lerrno)
+{
+  SET_HANDLER(&QUICStreamVCAdapter::state_stream_closed);
+
+  this->_read_vio.buffer.clear();
+  this->_read_vio.nbytes = 0;
+  this->_read_vio.op     = VIO::NONE;
+  this->_read_vio.cont   = nullptr;
+
+  this->_write_vio.buffer.clear();
+  this->_write_vio.nbytes = 0;
+  this->_write_vio.op     = VIO::NONE;
+  this->_write_vio.cont   = nullptr;
+}
+
+void
+QUICStreamVCAdapter::do_io_shutdown(ShutdownHowTo_t howto)
+{
+  ink_assert(false); // unimplemented yet
+  return;
+}
+
+void
+QUICStreamVCAdapter::reenable(VIO *vio)
+{
+  // TODO We probably need to tell QUICStream that the application consumed received data
+  // to update receive window here. In other words, we should not update receive window
+  // until the application consume data.
+}
+
+int
+QUICStreamVCAdapter::state_stream_open(int event, void *data)
+{
+  QUICErrorUPtr error = nullptr;
+
+  switch (event) {
+  case VC_EVENT_READ_READY:
+  case VC_EVENT_READ_COMPLETE: {
+    this->encourge_read();
+    break;
+  }
+  case VC_EVENT_WRITE_READY:
+  case VC_EVENT_WRITE_COMPLETE: {
+    this->encourge_write();
+    break;
+  }
+  case VC_EVENT_EOS:
+  case VC_EVENT_ERROR:
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+  case VC_EVENT_ACTIVE_TIMEOUT: {
+    // TODO
+    ink_assert(false);
+    break;
+  }
+  default:
+    ink_assert(false);
+  }
+
+  return EVENT_DONE;
+}
+
+int
+QUICStreamVCAdapter::state_stream_closed(int event, void *data)
+{
+  switch (event) {
+  case VC_EVENT_READ_READY:
+  case VC_EVENT_READ_COMPLETE: {
+    // ignore
+    break;
+  }
+  case VC_EVENT_WRITE_READY:
+  case VC_EVENT_WRITE_COMPLETE: {
+    // ignore
+    break;
+  }
+  case VC_EVENT_EOS:
+  case VC_EVENT_ERROR:
+  case VC_EVENT_INACTIVITY_TIMEOUT:
+  case VC_EVENT_ACTIVE_TIMEOUT: {
+    // TODO
+    ink_assert(false);
+    break;
+  }
+  default:
+    ink_assert(false);
+  }
+
+  return EVENT_DONE;
+}
diff --git a/iocore/net/quic/QUICStreamVCAdapter.h b/iocore/net/quic/QUICStreamVCAdapter.h
new file mode 100644
index 0000000..3613159
--- /dev/null
+++ b/iocore/net/quic/QUICStreamVCAdapter.h
@@ -0,0 +1,105 @@
+/** @file
+ *
+ *  A brief file description
+ *
+ *  @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.
+ */
+
+#pragma once
+#include "QUICStreamAdapter.h"
+#include "I_IOBuffer.h"
+
+class QUICStreamVCAdapter : public QUICStreamAdapter, public VConnection
+{
+public:
+  class IOInfo;
+
+  QUICStreamVCAdapter(QUICStream &stream);
+  virtual ~QUICStreamVCAdapter();
+
+  // Implement QUICStreamAdapter Interface
+  int64_t write(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin) override;
+  void encourge_read() override;
+  bool is_eos() override;
+  uint64_t unread_len() override;
+  uint64_t read_len() override;
+  uint64_t total_len() override;
+  void encourge_write() override;
+  void notify_eos() override;
+
+  // Implement VConnection Interface.
+  VIO *do_io_read(Continuation *c, int64_t nbytes = INT64_MAX, MIOBuffer *buf = 0) override;
+  VIO *do_io_write(Continuation *c = nullptr, int64_t nbytes = INT64_MAX, IOBufferReader *buf = 0, bool owner = false) override;
+  void do_io_close(int lerrno = -1) override;
+  void do_io_shutdown(ShutdownHowTo_t howto) override;
+  void reenable(VIO *vio) override;
+
+  int state_stream_open(int event, void *data);
+  int state_stream_closed(int event, void *data);
+
+protected:
+  Ptr<IOBufferBlock> _read(size_t len) override;
+
+  VIO _read_vio;
+  VIO _write_vio;
+
+  Event *_read_event  = nullptr;
+  Event *_write_event = nullptr;
+};
+
+class QUICStreamVCAdapter::IOInfo
+{
+public:
+  IOInfo(QUICStream &stream)
+    : adapter(stream), read_buffer(new_MIOBuffer(BUFFER_SIZE_INDEX_8K)), write_buffer(new_MIOBuffer(BUFFER_SIZE_INDEX_8K))
+  {
+  }
+  ~IOInfo()
+  {
+    free_MIOBuffer(this->read_buffer);
+    free_MIOBuffer(this->write_buffer);
+  }
+
+  void
+  setup_read_vio(Continuation *c)
+  {
+    read_vio = adapter.do_io_read(c, INT64_MAX, read_buffer);
+
+    // This is uncommon but it has basically the same effect as
+    // read_buffer->alloc_reader, and it allows VIO user to obtain the
+    // reader by calling read_vio.get_reader()
+    // It limits a number of readers to one, but it wouldn't be a real
+    // limitation for this particular usecase in QUICStreamVCAdapter.
+    read_vio->set_reader(read_buffer->alloc_reader());
+    adapter.encourge_read();
+  }
+
+  void
+  setup_write_vio(Continuation *c)
+  {
+    write_vio = adapter.do_io_write(c, INT64_MAX, write_buffer->alloc_reader());
+    adapter.encourge_write();
+  }
+
+  QUICStreamVCAdapter adapter;
+  MIOBuffer *read_buffer;
+  MIOBuffer *write_buffer;
+  VIO *read_vio  = nullptr;
+  VIO *write_vio = nullptr;
+};
diff --git a/iocore/net/quic/QUICTransferProgressProvider.cc b/iocore/net/quic/QUICTransferProgressProvider.cc
new file mode 100644
index 0000000..a01198b
--- /dev/null
+++ b/iocore/net/quic/QUICTransferProgressProvider.cc
@@ -0,0 +1,83 @@
+/** @file
+ *
+ *  Interface for providing transfer progress
+ *
+ *  @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 "I_IOBuffer.h"
+#include "QUICStreamAdapter.h"
+
+void
+QUICTransferProgressProviderSA::set_stream_adapter(QUICStreamAdapter *adapter)
+{
+  this->_adapter = adapter;
+}
+
+bool
+QUICTransferProgressProviderSA::is_transfer_goal_set() const
+{
+  return this->transfer_goal() != INT64_MAX;
+}
+
+uint64_t
+QUICTransferProgressProviderSA::transfer_progress() const
+{
+  return this->_adapter->read_len();
+}
+
+uint64_t
+QUICTransferProgressProviderSA::transfer_goal() const
+{
+  return this->_adapter->total_len();
+}
+
+bool
+QUICTransferProgressProviderSA::is_cancelled() const
+{
+  return false;
+}
+
+//
+// QUICTransferProgressProviderVIO::
+//
+
+bool
+QUICTransferProgressProviderVIO::is_transfer_goal_set() const
+{
+  return this->_vio.nbytes != INT64_MAX;
+}
+
+uint64_t
+QUICTransferProgressProviderVIO::transfer_progress() const
+{
+  return this->_vio.ndone;
+}
+
+uint64_t
+QUICTransferProgressProviderVIO::transfer_goal() const
+{
+  return this->_vio.nbytes;
+}
+
+bool
+QUICTransferProgressProviderVIO::is_cancelled() const
+{
+  return false;
+}
diff --git a/iocore/net/quic/QUICTransferProgressProvider.h b/iocore/net/quic/QUICTransferProgressProvider.h
index ce798fe..f0bc747 100644
--- a/iocore/net/quic/QUICTransferProgressProvider.h
+++ b/iocore/net/quic/QUICTransferProgressProvider.h
@@ -21,10 +21,11 @@
  *  limitations under the License.
  */
 
-#include "I_VIO.h"
-
 #pragma once
 
+class VIO;
+class QUICStreamAdapter;
+
 class QUICTransferProgressProvider
 {
 public:
@@ -40,34 +41,29 @@ public:
   }
 };
 
-class QUICTransferProgressProviderVIO : public QUICTransferProgressProvider
+class QUICTransferProgressProviderSA : public QUICTransferProgressProvider
 {
 public:
-  QUICTransferProgressProviderVIO(VIO &vio) : _vio(vio) {}
+  void set_stream_adapter(QUICStreamAdapter *adapter);
 
-  bool
-  is_transfer_goal_set() const
-  {
-    return this->_vio.nbytes != INT64_MAX;
-  }
+  bool is_transfer_goal_set() const override;
+  uint64_t transfer_progress() const override;
+  uint64_t transfer_goal() const override;
+  bool is_cancelled() const override;
 
-  uint64_t
-  transfer_progress() const
-  {
-    return this->_vio.ndone;
-  }
+private:
+  QUICStreamAdapter *_adapter;
+};
 
-  uint64_t
-  transfer_goal() const
-  {
-    return this->_vio.nbytes;
-  }
+class QUICTransferProgressProviderVIO : public QUICTransferProgressProvider
+{
+public:
+  QUICTransferProgressProviderVIO(VIO &vio) : _vio(vio) {}
 
-  bool
-  is_cancelled() const
-  {
-    return false;
-  }
+  bool is_transfer_goal_set() const override;
+  uint64_t transfer_progress() const override;
+  uint64_t transfer_goal() const override;
+  bool is_cancelled() const override;
 
 private:
   VIO &_vio;
diff --git a/iocore/net/quic/QUICUnidirectionalStream.cc b/iocore/net/quic/QUICUnidirectionalStream.cc
index c6a3d7a..599af3a 100644
--- a/iocore/net/quic/QUICUnidirectionalStream.cc
+++ b/iocore/net/quic/QUICUnidirectionalStream.cc
@@ -22,115 +22,25 @@
  */
 
 #include "QUICUnidirectionalStream.h"
+#include "QUICStreamAdapter.h"
 
 //
 // QUICSendStream
 //
 QUICSendStream::QUICSendStream(QUICConnectionInfoProvider *cinfo, QUICStreamId sid, uint64_t send_max_stream_data)
-  : QUICStreamVConnection(cinfo, sid), _remote_flow_controller(send_max_stream_data, _id), _state(nullptr, &this->_progress_vio)
+  : QUICStream(cinfo, sid), _remote_flow_controller(send_max_stream_data, _id), _state(nullptr, &this->_progress_sa)
 {
-  SET_HANDLER(&QUICSendStream::state_stream_open);
-
   QUICStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
                     this->_remote_flow_controller.current_limit());
 }
 
-int
-QUICSendStream::state_stream_open(int event, void *data)
-{
-  QUICVStreamDebug("%s (%d)", get_vc_event_name(event), event);
-  QUICErrorUPtr error = nullptr;
-
-  switch (event) {
-  case VC_EVENT_READ_READY:
-  case VC_EVENT_READ_COMPLETE: {
-    // should not schedule read event.
-    ink_assert(0);
-    break;
-  }
-  case VC_EVENT_WRITE_READY:
-  case VC_EVENT_WRITE_COMPLETE: {
-    int64_t len = this->_process_write_vio();
-    if (len > 0) {
-      this->_signal_write_event();
-    }
-
-    break;
-  }
-  case VC_EVENT_EOS:
-  case VC_EVENT_ERROR:
-  case VC_EVENT_INACTIVITY_TIMEOUT:
-  case VC_EVENT_ACTIVE_TIMEOUT: {
-    // TODO
-    ink_assert(false);
-    break;
-  }
-  default:
-    QUICStreamDebug("unknown event");
-    ink_assert(false);
-  }
-
-  // FIXME error is always nullptr
-  if (error != nullptr) {
-    if (error->cls == QUICErrorClass::TRANSPORT) {
-      QUICStreamDebug("QUICError: %s (%u), %s (0x%x)", QUICDebugNames::error_class(error->cls),
-                      static_cast<unsigned int>(error->cls), QUICDebugNames::error_code(error->code),
-                      static_cast<unsigned int>(error->code));
-    } else {
-      QUICStreamDebug("QUICError: %s (%u), APPLICATION ERROR (0x%x)", QUICDebugNames::error_class(error->cls),
-                      static_cast<unsigned int>(error->cls), static_cast<unsigned int>(error->code));
-    }
-    if (dynamic_cast<QUICStreamError *>(error.get()) != nullptr) {
-      // Stream Error
-      QUICStreamErrorUPtr serror = QUICStreamErrorUPtr(static_cast<QUICStreamError *>(error.get()));
-      this->reset(std::move(serror));
-    } else {
-      // Connection Error
-      // TODO Close connection (Does this really happen?)
-    }
-  }
-
-  return EVENT_DONE;
-}
-
-int
-QUICSendStream::state_stream_closed(int event, void *data)
-{
-  QUICVStreamDebug("%s (%d)", get_vc_event_name(event), event);
-
-  switch (event) {
-  case VC_EVENT_READ_READY:
-  case VC_EVENT_READ_COMPLETE: {
-    // ignore
-    break;
-  }
-  case VC_EVENT_WRITE_READY:
-  case VC_EVENT_WRITE_COMPLETE: {
-    // ignore
-    break;
-  }
-  case VC_EVENT_EOS:
-  case VC_EVENT_ERROR:
-  case VC_EVENT_INACTIVITY_TIMEOUT:
-  case VC_EVENT_ACTIVE_TIMEOUT: {
-    // TODO
-    ink_assert(false);
-    break;
-  }
-  default:
-    ink_assert(false);
-  }
-
-  return EVENT_DONE;
-}
-
 bool
 QUICSendStream::will_generate_frame(QUICEncryptionLevel level, size_t current_packet_size, bool ack_eliciting, uint32_t seq_num)
 {
   if (!this->is_retransmited_frame_queue_empty()) {
     return true;
   }
-  if (this->_write_vio.op != VIO::NONE && this->_write_vio.get_reader()->is_read_avail_more_than(0)) {
+  if (this->_adapter && this->_adapter->unread_len() > 0) {
     return true;
   }
   return false;
@@ -155,96 +65,94 @@ QUICSendStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint64_t
       return nullptr;
     }
     this->_records_rst_stream_frame(level, *static_cast<QUICRstStreamFrame *>(frame));
-    this->_state.update_with_sending_frame(*frame);
+    if (this->_state.update_with_sending_frame(*frame)) {
+      this->_notify_state_change();
+    }
     this->_is_reset_sent = true;
     return frame;
   }
 
-  if (this->_write_vio.op != VIO::NONE && this->_state.is_allowed_to_send(QUICFrameType::STREAM)) {
-    SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
+  if (!this->_adapter || !this->_state.is_allowed_to_send(QUICFrameType::STREAM)) {
+    return frame;
+  }
+
+  uint64_t maximum_data_size = 0;
+  if (maximum_frame_size <= MAX_STREAM_FRAME_OVERHEAD) {
+    return frame;
+  }
+  maximum_data_size = maximum_frame_size - MAX_STREAM_FRAME_OVERHEAD;
+
+  bool pure_fin = false;
+  bool fin      = false;
+  if (this->_adapter->is_eos()) {
+    // Pure FIN stream should be sent regardless status of remote flow controller, because the length is zero.
+    pure_fin = true;
+    fin      = true;
+  }
 
-    uint64_t maximum_data_size = 0;
-    if (maximum_frame_size <= MAX_STREAM_FRAME_OVERHEAD) {
+  uint64_t len = 0;
+  if (!pure_fin) {
+    uint64_t data_len = this->_adapter->unread_len();
+    if (data_len == 0) {
       return frame;
     }
-    maximum_data_size = maximum_frame_size - MAX_STREAM_FRAME_OVERHEAD;
-
-    bool pure_fin = false;
-    bool fin      = false;
-    if ((this->_write_vio.nbytes != 0 || this->_write_vio.nbytes != INT64_MAX) &&
-        this->_write_vio.nbytes == static_cast<int64_t>(this->_send_offset)) {
-      // Pure FIN stream should be sent regardless status of remote flow controller, because the length is zero.
-      pure_fin = true;
-      fin      = true;
-    }
 
-    uint64_t len           = 0;
-    IOBufferReader *reader = this->_write_vio.get_reader();
-    if (!pure_fin) {
-      uint64_t data_len = reader->block_read_avail();
-      if (data_len == 0) {
-        return frame;
-      }
-
-      // Check Connection/Stream level credit only if the generating STREAM frame is not pure fin
-      uint64_t stream_credit = this->_remote_flow_controller.credit();
-      if (stream_credit == 0) {
-        // STREAM_DATA_BLOCKED
-        frame =
-          this->_remote_flow_controller.generate_frame(buf, level, UINT16_MAX, maximum_frame_size, current_packet_size, seq_num);
-        return frame;
-      }
-
-      if (connection_credit == 0) {
-        // BLOCKED - BLOCKED frame will be sent by connection level remote flow controller
-        return frame;
-      }
-
-      len = std::min(data_len, std::min(maximum_data_size, std::min(stream_credit, connection_credit)));
-
-      // data_len, maximum_data_size, stream_credit and connection_credit are already checked they're larger than 0
-      ink_assert(len != 0);
-
-      if (this->_write_vio.nbytes == static_cast<int64_t>(this->_send_offset + len)) {
-        fin = true;
-      }
+    // Check Connection/Stream level credit only if the generating STREAM frame is not pure fin
+    uint64_t stream_credit = this->_remote_flow_controller.credit();
+    if (stream_credit == 0) {
+      // STREAM_DATA_BLOCKED
+      frame =
+        this->_remote_flow_controller.generate_frame(buf, level, UINT16_MAX, maximum_frame_size, current_packet_size, seq_num);
+      return frame;
     }
 
-    Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(reader->get_current_block()->clone());
-    block->consume(reader->start_offset);
-    block->_end = std::min(block->start() + len, block->_buf_end);
-    ink_assert(static_cast<uint64_t>(block->read_avail()) == len);
-
-    // STREAM - Pure FIN or data length is lager than 0
-    // FIXME has_length_flag and has_offset_flag should be configurable
-    frame = QUICFrameFactory::create_stream_frame(buf, block, this->_id, this->_send_offset, fin, true, true,
-                                                  this->_issue_frame_id(), this);
-    if (!this->_state.is_allowed_to_send(*frame)) {
-      QUICStreamDebug("Canceled sending %s frame due to the stream state", QUICDebugNames::frame_type(frame->type()));
+    if (connection_credit == 0) {
+      // BLOCKED - BLOCKED frame will be sent by connection level remote flow controller
       return frame;
     }
 
-    if (!pure_fin) {
-      int ret = this->_remote_flow_controller.update(this->_send_offset + len);
-      // We cannot cancel sending the frame after updating the flow controller
+    len = std::min(data_len, std::min(maximum_data_size, std::min(stream_credit, connection_credit)));
 
-      // Calling update always success, because len is always less than stream_credit
-      ink_assert(ret == 0);
+    // data_len, maximum_data_size, stream_credit and connection_credit are already checked they're larger than 0
+    ink_assert(len != 0);
 
-      QUICStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
-                        this->_remote_flow_controller.current_limit());
-      if (this->_remote_flow_controller.current_offset() == this->_remote_flow_controller.current_limit()) {
-        QUICStreamDebug("Flow Controller will block sending a STREAM frame");
-      }
+    if (this->_adapter->total_len() == this->_send_offset + len) {
+      fin = true;
+    }
+  }
+
+  Ptr<IOBufferBlock> block = this->_adapter->read(len);
+  ink_assert(static_cast<uint64_t>(block->read_avail()) == len);
+
+  // STREAM - Pure FIN or data length is lager than 0
+  // FIXME has_length_flag and has_offset_flag should be configurable
+  frame = QUICFrameFactory::create_stream_frame(buf, block, this->_id, this->_send_offset, fin, true, true, this->_issue_frame_id(),
+                                                this);
+  if (!this->_state.is_allowed_to_send(*frame)) {
+    QUICStreamDebug("Canceled sending %s frame due to the stream state", QUICDebugNames::frame_type(frame->type()));
+    return frame;
+  }
 
-      reader->consume(len);
-      this->_send_offset += len;
-      this->_write_vio.ndone += len;
+  if (!pure_fin) {
+    int ret = this->_remote_flow_controller.update(this->_send_offset + len);
+    // We cannot cancel sending the frame after updating the flow controller
+
+    // Calling update always success, because len is always less than stream_credit
+    ink_assert(ret == 0);
+
+    QUICStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
+                      this->_remote_flow_controller.current_limit());
+    if (this->_remote_flow_controller.current_offset() == this->_remote_flow_controller.current_limit()) {
+      QUICStreamDebug("Flow Controller will block sending a STREAM frame");
     }
-    this->_records_stream_frame(level, *static_cast<QUICStreamFrame *>(frame));
 
-    this->_signal_write_event();
-    this->_state.update_with_sending_frame(*frame);
+    this->_send_offset += len;
+  }
+  this->_records_stream_frame(level, *static_cast<QUICStreamFrame *>(frame));
+
+  this->_adapter->encourge_write();
+  if (this->_state.update_with_sending_frame(*frame)) {
+    this->_notify_state_change();
   }
 
   return frame;
@@ -253,7 +161,9 @@ QUICSendStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint64_t
 QUICConnectionErrorUPtr
 QUICSendStream::recv(const QUICStopSendingFrame &frame)
 {
-  this->_state.update_with_receiving_frame(frame);
+  if (this->_state.update_with_receiving_frame(frame)) {
+    this->_notify_state_change();
+  }
   this->reset(QUICStreamErrorUPtr(new QUICStreamError(this, QUIC_APP_ERROR_CODE_STOPPING)));
   // We received and processed STOP_SENDING frame, so return NO_ERROR here
   return nullptr;
@@ -266,96 +176,11 @@ QUICSendStream::recv(const QUICMaxStreamDataFrame &frame)
   QUICStreamFCDebug("[REMOTE] %" PRIu64 "/%" PRIu64, this->_remote_flow_controller.current_offset(),
                     this->_remote_flow_controller.current_limit());
 
-  int64_t len = this->_process_write_vio();
-  if (len > 0) {
-    this->_signal_write_event();
-  }
-
-  return nullptr;
-}
+  this->_adapter->encourge_write();
 
-VIO *
-QUICSendStream::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
-{
-  QUICStreamDebug("Warning wants to read from send only stream ignore");
-  // FIXME: should not assert here
-  ink_assert(!"read from send only stream");
   return nullptr;
 }
 
-VIO *
-QUICSendStream::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, bool owner)
-{
-  if (buf) {
-    this->_write_vio.buffer.reader_for(buf);
-  } else {
-    this->_write_vio.buffer.clear();
-  }
-
-  this->_write_vio.mutex     = c ? c->mutex : this->mutex;
-  this->_write_vio.cont      = c;
-  this->_write_vio.nbytes    = nbytes;
-  this->_write_vio.ndone     = 0;
-  this->_write_vio.vc_server = this;
-  this->_write_vio.op        = VIO::WRITE;
-
-  this->_process_write_vio();
-  this->_send_tracked_event(this->_write_event, VC_EVENT_WRITE_READY, &this->_write_vio);
-
-  return &this->_write_vio;
-}
-
-void
-QUICSendStream::do_io_close(int lerrno)
-{
-  SET_HANDLER(&QUICSendStream::state_stream_closed);
-
-  ink_assert(this->_read_vio.nbytes == 0);
-  ink_assert(this->_read_vio.op == VIO::NONE);
-  ink_assert(this->_read_vio.cont == nullptr);
-  this->_read_vio.buffer.clear();
-
-  this->_write_vio.buffer.clear();
-  this->_write_vio.nbytes = 0;
-  this->_write_vio.op     = VIO::NONE;
-  this->_write_vio.cont   = nullptr;
-}
-
-void
-QUICSendStream::do_io_shutdown(ShutdownHowTo_t howto)
-{
-  switch (howto) {
-  case IO_SHUTDOWN_READ:
-    // ignore
-    break;
-  case IO_SHUTDOWN_WRITE:
-  case IO_SHUTDOWN_READWRITE:
-    this->do_io_close();
-    break;
-  default:
-    ink_assert(0);
-    break;
-  }
-}
-
-void
-QUICSendStream::reenable(VIO *vio)
-{
-  ink_assert(vio == &this->_write_vio);
-  ink_assert(vio->op == VIO::WRITE);
-
-  int64_t len = this->_process_write_vio();
-  if (len > 0) {
-    this->_signal_write_event();
-  }
-}
-
-void
-QUICSendStream::reset(QUICStreamErrorUPtr error)
-{
-  this->_reset_reason = std::move(error);
-}
-
 void
 QUICSendStream::_on_frame_acked(QUICFrameInformationUPtr &info)
 {
@@ -405,111 +230,26 @@ QUICSendStream::largest_offset_sent() const
   return this->_remote_flow_controller.current_offset();
 }
 
+void
+QUICSendStream::reset(QUICStreamErrorUPtr error)
+{
+  this->_reset_reason = std::move(error);
+}
+
 //
 // QUICReceiveStream
 //
 QUICReceiveStream::QUICReceiveStream(QUICRTTProvider *rtt_provider, QUICConnectionInfoProvider *cinfo, QUICStreamId sid,
                                      uint64_t recv_max_stream_data)
-  : QUICStreamVConnection(cinfo, sid),
+  : QUICStream(cinfo, sid),
     _local_flow_controller(rtt_provider, recv_max_stream_data, _id),
     _flow_control_buffer_size(recv_max_stream_data),
     _state(this, nullptr)
 {
-  SET_HANDLER(&QUICReceiveStream::state_stream_open);
-
   QUICStreamFCDebug("[LOCAL] %" PRIu64 "/%" PRIu64, this->_local_flow_controller.current_offset(),
                     this->_local_flow_controller.current_limit());
 }
 
-int
-QUICReceiveStream::state_stream_open(int event, void *data)
-{
-  QUICVStreamDebug("%s (%d)", get_vc_event_name(event), event);
-  QUICErrorUPtr error = nullptr;
-
-  switch (event) {
-  case VC_EVENT_READ_READY:
-  case VC_EVENT_READ_COMPLETE: {
-    int64_t len = this->_process_read_vio();
-    if (len > 0) {
-      this->_signal_read_event();
-    }
-
-    break;
-  }
-  case VC_EVENT_WRITE_READY:
-  case VC_EVENT_WRITE_COMPLETE: {
-    // should not schedule write event
-    ink_assert(!"should not schedule write even");
-    break;
-  }
-  case VC_EVENT_EOS:
-  case VC_EVENT_ERROR:
-  case VC_EVENT_INACTIVITY_TIMEOUT:
-  case VC_EVENT_ACTIVE_TIMEOUT: {
-    // TODO
-    ink_assert(false);
-    break;
-  }
-  default:
-    QUICStreamDebug("unknown event");
-    ink_assert(false);
-  }
-
-  // FIXME error is always nullptr
-  if (error != nullptr) {
-    if (error->cls == QUICErrorClass::TRANSPORT) {
-      QUICStreamDebug("QUICError: %s (%u), %s (0x%x)", QUICDebugNames::error_class(error->cls),
-                      static_cast<unsigned int>(error->cls), QUICDebugNames::error_code(error->code),
-                      static_cast<unsigned int>(error->code));
-    } else {
-      QUICStreamDebug("QUICError: %s (%u), APPLICATION ERROR (0x%x)", QUICDebugNames::error_class(error->cls),
-                      static_cast<unsigned int>(error->cls), static_cast<unsigned int>(error->code));
-    }
-    if (dynamic_cast<QUICStreamError *>(error.get()) != nullptr) {
-      // Stream Error
-      QUICStreamErrorUPtr serror = QUICStreamErrorUPtr(static_cast<QUICStreamError *>(error.get()));
-      this->reset(std::move(serror));
-    } else {
-      // Connection Error
-      // TODO Close connection (Does this really happen?)
-    }
-  }
-
-  return EVENT_DONE;
-}
-
-int
-QUICReceiveStream::state_stream_closed(int event, void *data)
-{
-  QUICVStreamDebug("%s (%d)", get_vc_event_name(event), event);
-
-  switch (event) {
-  case VC_EVENT_READ_READY:
-  case VC_EVENT_READ_COMPLETE: {
-    // ignore
-    break;
-  }
-  case VC_EVENT_WRITE_READY:
-  case VC_EVENT_WRITE_COMPLETE: {
-    // ignore
-    break;
-  }
-  case VC_EVENT_EOS:
-  case VC_EVENT_ERROR:
-  case VC_EVENT_INACTIVITY_TIMEOUT:
-  case VC_EVENT_ACTIVE_TIMEOUT: {
-    // TODO
-    ink_assert(false);
-    break;
-  }
-  default:
-    ink_assert(false);
-  }
-
-  return EVENT_DONE;
-}
-
 bool
 QUICReceiveStream::is_transfer_goal_set() const
 {
@@ -560,7 +300,9 @@ QUICReceiveStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint6
       return nullptr;
     }
     this->_records_stop_sending_frame(level, *static_cast<QUICStopSendingFrame *>(frame));
-    this->_state.update_with_sending_frame(*frame);
+    if (this->_state.update_with_sending_frame(*frame)) {
+      this->_notify_state_change();
+    }
     this->_is_stop_sending_sent = true;
     return frame;
   }
@@ -574,8 +316,10 @@ QUICReceiveStream::generate_frame(uint8_t *buf, QUICEncryptionLevel level, uint6
 QUICConnectionErrorUPtr
 QUICReceiveStream::recv(const QUICRstStreamFrame &frame)
 {
-  this->_state.update_with_receiving_frame(frame);
-  this->_signal_read_eos_event();
+  if (this->_state.update_with_receiving_frame(frame)) {
+    this->_notify_state_change();
+  }
+  this->_adapter->notify_eos();
   return nullptr;
 }
 
@@ -597,7 +341,6 @@ QUICConnectionErrorUPtr
 QUICReceiveStream::recv(const QUICStreamFrame &frame)
 {
   ink_assert(_id == frame.stream_id());
-  ink_assert(this->_read_vio.op == VIO::READ);
 
   // Check stream state - Do this first before accept the frame
   if (!this->_state.is_allowed_to_receive(frame)) {
@@ -631,9 +374,11 @@ QUICReceiveStream::recv(const QUICStreamFrame &frame)
     last_offset  = stream_frame->offset();
     last_length  = stream_frame->data_length();
 
-    this->_write_to_read_vio(stream_frame->offset(), reinterpret_cast<uint8_t *>(stream_frame->data()->start()),
-                             stream_frame->data_length(), stream_frame->has_fin_flag());
-    this->_state.update_with_receiving_frame(*new_frame);
+    this->_adapter->write(stream_frame->offset(), reinterpret_cast<uint8_t *>(stream_frame->data()->start()),
+                          stream_frame->data_length(), stream_frame->has_fin_flag());
+    if (this->_state.update_with_receiving_frame(*new_frame)) {
+      this->_notify_state_change();
+    }
 
     delete new_frame;
     new_frame = this->_received_stream_frame_buffer.pop();
@@ -647,97 +392,25 @@ QUICReceiveStream::recv(const QUICStreamFrame &frame)
                       this->_local_flow_controller.current_limit());
   }
 
-  this->_signal_read_event();
+  this->_adapter->encourge_read();
 
   return nullptr;
 }
 
-VIO *
-QUICReceiveStream::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
-{
-  if (buf) {
-    this->_read_vio.buffer.writer_for(buf);
-  } else {
-    this->_read_vio.buffer.clear();
-  }
-
-  this->_read_vio.mutex     = c ? c->mutex : this->mutex;
-  this->_read_vio.cont      = c;
-  this->_read_vio.nbytes    = nbytes;
-  this->_read_vio.ndone     = 0;
-  this->_read_vio.vc_server = this;
-  this->_read_vio.op        = VIO::READ;
-
-  this->_process_read_vio();
-  this->_send_tracked_event(this->_read_event, VC_EVENT_READ_READY, &this->_read_vio);
-
-  return &this->_read_vio;
-}
-
-VIO *
-QUICReceiveStream::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *buf, bool owner)
-{
-  QUICStreamDebug("Warning wants to write to send only stream ignore");
-  // FIXME: should not assert here
-  ink_assert(!"write to send only stream");
-  return nullptr;
-}
-
-void
-QUICReceiveStream::do_io_close(int lerrno)
-{
-  SET_HANDLER(&QUICReceiveStream::state_stream_closed);
-
-  ink_assert(this->_write_vio.nbytes == 0);
-  ink_assert(this->_write_vio.op == VIO::NONE);
-  ink_assert(this->_write_vio.cont == nullptr);
-  this->_write_vio.buffer.clear();
-
-  this->_read_vio.buffer.clear();
-  this->_read_vio.nbytes = 0;
-  this->_read_vio.op     = VIO::NONE;
-  this->_read_vio.cont   = nullptr;
-}
-
-void
-QUICReceiveStream::do_io_shutdown(ShutdownHowTo_t howto)
-{
-  switch (howto) {
-  case IO_SHUTDOWN_WRITE:
-    // ignore
-    break;
-  case IO_SHUTDOWN_READ:
-  case IO_SHUTDOWN_READWRITE:
-    this->do_io_close();
-    break;
-  default:
-    ink_assert(0);
-    break;
-  }
-}
-
-void
-QUICReceiveStream::reenable(VIO *vio)
-{
-  ink_assert(vio == &this->_read_vio);
-  ink_assert(vio->op == VIO::READ);
-
-  int64_t len = this->_process_read_vio();
-  if (len > 0) {
-    this->_signal_read_event();
-  }
-}
-
 void
 QUICReceiveStream::on_read()
 {
-  this->_state.update_on_read();
+  if (this->_state.update_on_read()) {
+    this->_notify_state_change();
+  }
 }
 
 void
 QUICReceiveStream::on_eos()
 {
-  this->_state.update_on_eos();
+  if (this->_state.update_on_eos()) {
+    this->_notify_state_change();
+  }
 }
 
 QUICOffset
diff --git a/iocore/net/quic/QUICUnidirectionalStream.h b/iocore/net/quic/QUICUnidirectionalStream.h
index 5ebe552..315c24f 100644
--- a/iocore/net/quic/QUICUnidirectionalStream.h
+++ b/iocore/net/quic/QUICUnidirectionalStream.h
@@ -25,7 +25,7 @@
 
 #include "QUICStream.h"
 
-class QUICSendStream : public QUICStreamVConnection
+class QUICSendStream : public QUICStream
 {
 public:
   QUICSendStream(QUICConnectionInfoProvider *cinfo, QUICStreamId sid, uint64_t send_max_stream_data);
@@ -44,13 +44,6 @@ public:
   virtual QUICConnectionErrorUPtr recv(const QUICMaxStreamDataFrame &frame) override;
   virtual QUICConnectionErrorUPtr recv(const QUICStopSendingFrame &frame) override;
 
-  // Implement VConnection Interface.
-  VIO *do_io_read(Continuation *c, int64_t nbytes = INT64_MAX, MIOBuffer *buf = 0) override;
-  VIO *do_io_write(Continuation *c = nullptr, int64_t nbytes = INT64_MAX, IOBufferReader *buf = 0, bool owner = false) override;
-  void do_io_close(int lerrno = -1) override;
-  void do_io_shutdown(ShutdownHowTo_t howto) override;
-  void reenable(VIO *vio) override;
-
   void reset(QUICStreamErrorUPtr error) override;
 
   QUICOffset largest_offset_sent() const override;
@@ -62,7 +55,7 @@ private:
   bool _is_transfer_complete = false;
   bool _is_reset_complete    = false;
 
-  QUICTransferProgressProviderVIO _progress_vio = {this->_read_vio};
+  QUICTransferProgressProviderSA _progress_sa;
 
   QUICRemoteStreamFlowController _remote_flow_controller;
 
@@ -73,7 +66,7 @@ private:
   void _on_frame_lost(QUICFrameInformationUPtr &info) override;
 };
 
-class QUICReceiveStream : public QUICStreamVConnection, public QUICTransferProgressProvider
+class QUICReceiveStream : public QUICStream, public QUICTransferProgressProvider
 {
 public:
   QUICReceiveStream(QUICRTTProvider *rtt_provider, QUICConnectionInfoProvider *cinfo, QUICStreamId sid,
@@ -94,13 +87,6 @@ public:
   virtual QUICConnectionErrorUPtr recv(const QUICStreamDataBlockedFrame &frame) override;
   virtual QUICConnectionErrorUPtr recv(const QUICRstStreamFrame &frame) override;
 
-  // Implement VConnection Interface.
-  VIO *do_io_read(Continuation *c, int64_t nbytes = INT64_MAX, MIOBuffer *buf = 0) override;
-  VIO *do_io_write(Continuation *c = nullptr, int64_t nbytes = INT64_MAX, IOBufferReader *buf = 0, bool owner = false) override;
-  void do_io_close(int lerrno = -1) override;
-  void do_io_shutdown(ShutdownHowTo_t howto) override;
-  void reenable(VIO *vio) override;
-
   // QUICTransferProgressProvider
   bool is_transfer_goal_set() const override;
   uint64_t transfer_progress() const override;
diff --git a/iocore/net/quic/test/test_QUICStream.cc b/iocore/net/quic/test/test_QUICStream.cc
index 828049f..9261781 100644
--- a/iocore/net/quic/test/test_QUICStream.cc
+++ b/iocore/net/quic/test/test_QUICStream.cc
@@ -25,6 +25,7 @@
 
 #include "quic/QUICBidirectionalStream.h"
 #include "quic/QUICUnidirectionalStream.h"
+#include "quic/QUICStreamVCAdapter.h"
 #include "quic/Mock.h"
 
 TEST_CASE("QUICBidiStream", "[quic]")
@@ -88,7 +89,9 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, 1024, 1024));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     stream->recv(frame_1);
     stream->recv(frame_2);
@@ -116,7 +119,9 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     stream->recv(frame_8);
     stream->recv(frame_7);
@@ -144,7 +149,9 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     stream->recv(frame_8);
     stream->recv(frame_7);
@@ -175,7 +182,9 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, 4096, 4096));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
     block->alloc(BUFFER_SIZE_INDEX_32K);
@@ -216,11 +225,13 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, 4096, 4096));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
 
@@ -228,28 +239,28 @@ TEST_CASE("QUICBidiStream", "[quic]")
     QUICFrame *frame      = nullptr;
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == false);
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == false);
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == false);
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
@@ -257,7 +268,7 @@ TEST_CASE("QUICBidiStream", "[quic]")
 
     // This should not send a frame because of flow control
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame);
@@ -268,7 +279,7 @@ TEST_CASE("QUICBidiStream", "[quic]")
     stream->recv(*std::make_shared<QUICMaxStreamDataFrame>(stream_id, 5120));
 
     // This should send a frame
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
@@ -279,13 +290,13 @@ TEST_CASE("QUICBidiStream", "[quic]")
 
     // This should send a frame
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
 
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM_DATA_BLOCKED);
@@ -293,7 +304,7 @@ TEST_CASE("QUICBidiStream", "[quic]")
     // Update window
     stream->recv(*std::make_shared<QUICMaxStreamDataFrame>(stream_id, 6144));
 
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
@@ -312,10 +323,12 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
     const char data1[]        = "this is a test data";
@@ -327,7 +340,7 @@ TEST_CASE("QUICBidiStream", "[quic]")
 
     // Write data1
     write_buffer->write(data1, sizeof(data1));
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     // Generate STREAM frame
     frame  = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     frame1 = static_cast<QUICStreamFrame *>(frame);
@@ -339,7 +352,7 @@ TEST_CASE("QUICBidiStream", "[quic]")
 
     // Write data2
     write_buffer->write(data2, sizeof(data2));
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     // Lost the frame
     stream->on_frame_lost(frame->id());
     // Regenerate a frame
@@ -361,10 +374,12 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
     QUICFrame *frame          = nullptr;
@@ -392,10 +407,12 @@ TEST_CASE("QUICBidiStream", "[quic]")
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICBidirectionalStream> stream(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
     QUICFrame *frame          = nullptr;
@@ -426,9 +443,11 @@ TEST_CASE("QUICBidiStream", "[quic]")
     // STOP_SENDING
     std::unique_ptr<QUICBidirectionalStream> stream1(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    MockContinuation mock_cont1(stream1->mutex);
-    stream1->do_io_write(&mock_cont1, INT64_MAX, write_buffer_reader);
-    SCOPED_MUTEX_LOCK(lock1, stream1->mutex, this_ethread());
+    QUICStreamVCAdapter adapter1(*stream1);
+    stream1->set_io_adapter(&adapter1);
+    MockContinuation mock_cont1(adapter1.mutex);
+    adapter1.do_io_write(&mock_cont1, INT64_MAX, write_buffer_reader);
+    SCOPED_MUTEX_LOCK(lock1, adapter1.mutex, this_ethread());
     stream1->stop_sending(QUICStreamErrorUPtr(new QUICStreamError(stream1.get(), QUIC_APP_ERROR_CODE_STOPPING)));
     frame = stream1->generate_frame(frame_buf, level, 4096, 0, 0, 0);
     CHECK(frame == nullptr);
@@ -436,9 +455,11 @@ TEST_CASE("QUICBidiStream", "[quic]")
     // RESET_STREAM
     std::unique_ptr<QUICBidirectionalStream> stream2(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    MockContinuation mock_cont2(stream2->mutex);
-    stream2->do_io_write(&mock_cont2, INT64_MAX, write_buffer_reader);
-    SCOPED_MUTEX_LOCK(lock2, stream2->mutex, this_ethread());
+    QUICStreamVCAdapter adapter2(*stream2);
+    stream2->set_io_adapter(&adapter2);
+    MockContinuation mock_cont2(adapter2.mutex);
+    adapter2.do_io_write(&mock_cont2, INT64_MAX, write_buffer_reader);
+    SCOPED_MUTEX_LOCK(lock2, adapter2.mutex, this_ethread());
     stream2->reset(QUICStreamErrorUPtr(new QUICStreamError(stream2.get(), QUIC_APP_ERROR_CODE_STOPPING)));
     frame = stream2->generate_frame(frame_buf, level, 4096, 0, 0, 0);
     CHECK(frame == nullptr);
@@ -446,12 +467,14 @@ TEST_CASE("QUICBidiStream", "[quic]")
     // STREAM
     std::unique_ptr<QUICBidirectionalStream> stream3(
       new QUICBidirectionalStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX, UINT64_MAX));
-    MockContinuation mock_cont3(stream3->mutex);
-    stream3->do_io_write(&mock_cont3, INT64_MAX, write_buffer_reader);
-    SCOPED_MUTEX_LOCK(lock3, stream3->mutex, this_ethread());
+    QUICStreamVCAdapter adapter3(*stream3);
+    stream3->set_io_adapter(&adapter3);
+    MockContinuation mock_cont3(adapter3.mutex);
+    adapter3.do_io_write(&mock_cont3, INT64_MAX, write_buffer_reader);
+    SCOPED_MUTEX_LOCK(lock3, adapter3.mutex, this_ethread());
     const char data[] = "this is a test data";
     write_buffer->write(data, sizeof(data));
-    stream3->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter3.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     frame = stream3->generate_frame(frame_buf, level, 4096, 0, 0, 0);
     CHECK(frame == nullptr);
   }
@@ -517,7 +540,9 @@ TEST_CASE("QUIC receive only stream", "[quic]")
     QUICRTTMeasure rtt_provider;
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICReceiveStream> stream(new QUICReceiveStream(&rtt_provider, &cinfo_provider, stream_id, 1024));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     stream->recv(frame_1);
     stream->recv(frame_2);
@@ -544,7 +569,9 @@ TEST_CASE("QUIC receive only stream", "[quic]")
     QUICRTTMeasure rtt_provider;
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICReceiveStream> stream(new QUICReceiveStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     stream->recv(frame_8);
     stream->recv(frame_7);
@@ -571,7 +598,9 @@ TEST_CASE("QUIC receive only stream", "[quic]")
     QUICRTTMeasure rtt_provider;
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICReceiveStream> stream(new QUICReceiveStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     stream->recv(frame_8);
     stream->recv(frame_7);
@@ -601,7 +630,9 @@ TEST_CASE("QUIC receive only stream", "[quic]")
     QUICRTTMeasure rtt_provider;
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICReceiveStream> stream(new QUICReceiveStream(&rtt_provider, &cinfo_provider, stream_id, 4096));
-    stream->do_io_read(nullptr, INT64_MAX, read_buffer);
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    adapter.do_io_read(nullptr, INT64_MAX, read_buffer);
 
     Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
     block->alloc(BUFFER_SIZE_INDEX_32K);
@@ -635,7 +666,9 @@ TEST_CASE("QUIC receive only stream", "[quic]")
     QUICRTTMeasure rtt_provider;
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICReceiveStream> stream(new QUICReceiveStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
     QUICFrame *frame          = nullptr;
@@ -663,8 +696,10 @@ TEST_CASE("QUIC receive only stream", "[quic]")
 
     // STOP_SENDING
     std::unique_ptr<QUICReceiveStream> stream1(new QUICReceiveStream(&rtt_provider, &cinfo_provider, stream_id, UINT64_MAX));
-    MockContinuation mock_cont1(stream1->mutex);
-    SCOPED_MUTEX_LOCK(lock1, stream1->mutex, this_ethread());
+    QUICStreamVCAdapter adapter1(*stream1);
+    stream1->set_io_adapter(&adapter1);
+    MockContinuation mock_cont1(adapter1.mutex);
+    SCOPED_MUTEX_LOCK(lock1, adapter1.mutex, this_ethread());
     stream1->stop_sending(QUICStreamErrorUPtr(new QUICStreamError(stream1.get(), QUIC_APP_ERROR_CODE_STOPPING)));
     frame = stream1->generate_frame(frame_buf, level, 4096, 0, 0, 0);
     CHECK(frame == nullptr);
@@ -732,10 +767,12 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICSendStream> stream(new QUICSendStream(&cinfo_provider, stream_id, 4096));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
 
@@ -743,28 +780,28 @@ TEST_CASE("QUIC send only stream", "[quic]")
     QUICFrame *frame      = nullptr;
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == false);
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == false);
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == false);
 
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
@@ -772,7 +809,7 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     // This should not send a frame because of flow control
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame);
@@ -783,7 +820,7 @@ TEST_CASE("QUIC send only stream", "[quic]")
     stream->recv(*std::make_shared<QUICMaxStreamDataFrame>(stream_id, 5120));
 
     // This should send a frame
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
@@ -794,13 +831,13 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     // This should send a frame
     write_buffer->write(data, 1024);
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
 
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM_DATA_BLOCKED);
@@ -808,7 +845,7 @@ TEST_CASE("QUIC send only stream", "[quic]")
     // Update window
     stream->recv(*std::make_shared<QUICMaxStreamDataFrame>(stream_id, 6144));
 
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     CHECK(stream->will_generate_frame(level, 0, false, 0) == true);
     frame = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     CHECK(frame->type() == QUICFrameType::STREAM);
@@ -825,10 +862,12 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICSendStream> stream(new QUICSendStream(&cinfo_provider, stream_id, UINT64_MAX));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
     const char data1[]        = "this is a test data";
@@ -840,7 +879,7 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     // Write data1
     write_buffer->write(data1, sizeof(data1));
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     // Generate STREAM frame
     frame  = stream->generate_frame(frame_buf, level, 4096, 4096, 0, 0);
     frame1 = static_cast<QUICStreamFrame *>(frame);
@@ -852,7 +891,7 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     // Write data2
     write_buffer->write(data2, sizeof(data2));
-    stream->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     // Lost the frame
     stream->on_frame_lost(frame->id());
     // Regenerate a frame
@@ -872,10 +911,12 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     MockQUICConnectionInfoProvider cinfo_provider;
     std::unique_ptr<QUICSendStream> stream(new QUICSendStream(&cinfo_provider, stream_id, UINT64_MAX));
-    SCOPED_MUTEX_LOCK(lock, stream->mutex, this_ethread());
+    QUICStreamVCAdapter adapter(*stream);
+    stream->set_io_adapter(&adapter);
+    SCOPED_MUTEX_LOCK(lock, adapter.mutex, this_ethread());
 
-    MockContinuation mock_cont(stream->mutex);
-    stream->do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
+    MockContinuation mock_cont(adapter.mutex);
+    adapter.do_io_write(&mock_cont, INT64_MAX, write_buffer_reader);
 
     QUICEncryptionLevel level = QUICEncryptionLevel::ONE_RTT;
     QUICFrame *frame          = nullptr;
@@ -904,21 +945,25 @@ TEST_CASE("QUIC send only stream", "[quic]")
 
     // RESET_STREAM
     std::unique_ptr<QUICSendStream> stream2(new QUICSendStream(&cinfo_provider, stream_id, UINT64_MAX));
-    MockContinuation mock_cont2(stream2->mutex);
-    stream2->do_io_write(&mock_cont2, INT64_MAX, write_buffer_reader);
-    SCOPED_MUTEX_LOCK(lock2, stream2->mutex, this_ethread());
+    QUICStreamVCAdapter adapter2(*stream2);
+    stream2->set_io_adapter(&adapter2);
+    MockContinuation mock_cont2(adapter2.mutex);
+    adapter2.do_io_write(&mock_cont2, INT64_MAX, write_buffer_reader);
+    SCOPED_MUTEX_LOCK(lock2, adapter2.mutex, this_ethread());
     stream2->reset(QUICStreamErrorUPtr(new QUICStreamError(stream2.get(), QUIC_APP_ERROR_CODE_STOPPING)));
     frame = stream2->generate_frame(frame_buf, level, 4096, 0, 0, 0);
     CHECK(frame == nullptr);
 
     // STREAM
     std::unique_ptr<QUICSendStream> stream3(new QUICSendStream(&cinfo_provider, stream_id, UINT64_MAX));
-    MockContinuation mock_cont3(stream3->mutex);
-    stream3->do_io_write(&mock_cont3, INT64_MAX, write_buffer_reader);
-    SCOPED_MUTEX_LOCK(lock3, stream3->mutex, this_ethread());
+    QUICStreamVCAdapter adapter3(*stream3);
+    stream3->set_io_adapter(&adapter3);
+    MockContinuation mock_cont3(adapter3.mutex);
+    adapter3.do_io_write(&mock_cont3, INT64_MAX, write_buffer_reader);
+    SCOPED_MUTEX_LOCK(lock3, adapter3.mutex, this_ethread());
     const char data[] = "this is a test data";
     write_buffer->write(data, sizeof(data));
-    stream3->handleEvent(VC_EVENT_WRITE_READY, nullptr);
+    adapter3.handleEvent(VC_EVENT_WRITE_READY, nullptr);
     frame = stream3->generate_frame(frame_buf, level, 4096, 0, 0, 0);
     CHECK(frame == nullptr);
   }
diff --git a/iocore/net/quic/test/test_QUICStreamState.cc b/iocore/net/quic/test/test_QUICStreamState.cc
index 9eb1128..e6f9989 100644
--- a/iocore/net/quic/test/test_QUICStreamState.cc
+++ b/iocore/net/quic/test/test_QUICStreamState.cc
@@ -57,17 +57,17 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case2. Send STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame);
+    CHECK(ss.update_with_sending_frame(*stream_frame));
     CHECK(ss.get() == QUICSendStreamState::Send);
 
     // Case3. Send STREAM_DATA_BLOCKED
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM_DATA_BLOCKED));
-    ss.update_with_sending_frame(*stream_data_blocked_frame);
+    CHECK(!ss.update_with_sending_frame(*stream_data_blocked_frame));
     CHECK(ss.get() == QUICSendStreamState::Send);
 
     // Case3. Send FIN in a STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_sending_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICSendStreamState::DataSent);
 
     // Case4. STREAM is not allowed to send
@@ -75,7 +75,7 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case5. Receive all ACKs
     pp.set_transfer_complete(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICSendStreamState::DataRecvd);
   }
 
@@ -87,7 +87,7 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case2. Send STREAM_DATA_BLOCKED
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM_DATA_BLOCKED));
-    ss.update_with_sending_frame(*stream_data_blocked_frame);
+    CHECK(ss.update_with_sending_frame(*stream_data_blocked_frame));
     CHECK(ss.get() == QUICSendStreamState::Send);
   }
 
@@ -101,7 +101,7 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case2. Send RESET_STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::RESET_STREAM));
-    ss.update_with_sending_frame(*rst_stream_frame);
+    CHECK(ss.update_with_sending_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICSendStreamState::ResetSent);
 
     // Case3. Receive ACK for STREAM
@@ -109,7 +109,7 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case4. Receive ACK for RESET_STREAM
     pp.set_cancelled(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICSendStreamState::ResetRecvd);
   }
 
@@ -121,21 +121,21 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case2. Send STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame);
+    CHECK(ss.update_with_sending_frame(*stream_frame));
     CHECK(ss.get() == QUICSendStreamState::Send);
 
     // Case3. Send RESET_STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::RESET_STREAM));
-    ss.update_with_sending_frame(*rst_stream_frame);
+    CHECK(ss.update_with_sending_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICSendStreamState::ResetSent);
 
     // Case4. Receive ACK for STREAM
-    ss.update_on_ack();
+    CHECK(!ss.update_on_ack());
     CHECK(ss.get() == QUICSendStreamState::ResetSent);
 
     // Case5. Receive ACK for RESET_STREAM
     pp.set_cancelled(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICSendStreamState::ResetRecvd);
   }
 
@@ -147,17 +147,17 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case2. Send STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame);
+    CHECK(ss.update_with_sending_frame(*stream_frame));
     CHECK(ss.get() == QUICSendStreamState::Send);
 
     // Case3. Send STREAM_DATA_BLOCKED
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM_DATA_BLOCKED));
-    ss.update_with_sending_frame(*stream_data_blocked_frame);
+    CHECK(!ss.update_with_sending_frame(*stream_data_blocked_frame));
     CHECK(ss.get() == QUICSendStreamState::Send);
 
     // Case3. Send FIN in a STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_sending_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICSendStreamState::DataSent);
 
     // Case4. STREAM is not allowed to send
@@ -165,16 +165,16 @@ TEST_CASE("QUICSendStreamState", "[quic]")
 
     // Case4. Send RESET_STREAM
     CHECK(ss.is_allowed_to_send(QUICFrameType::RESET_STREAM));
-    ss.update_with_sending_frame(*rst_stream_frame);
+    CHECK(ss.update_with_sending_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICSendStreamState::ResetSent);
 
     // Case5. Receive ACK for STREAM
-    ss.update_on_ack();
+    CHECK(!ss.update_on_ack());
     CHECK(ss.get() == QUICSendStreamState::ResetSent);
 
     // Case6. Receive ACK for RESET_STREAM
     pp.set_cancelled(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICSendStreamState::ResetRecvd);
   }
 }
@@ -209,29 +209,29 @@ TEST_CASE("QUICReceiveStreamState", "[quic]")
     CHECK(ss.is_allowed_to_send(QUICFrameType::MAX_STREAM_DATA) == false);
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_progress(1);
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case2. Recv STREAM_DATA_BLOCKED
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM_DATA_BLOCKED));
-    ss.update_with_receiving_frame(*stream_data_blocked_frame);
+    CHECK(!ss.update_with_receiving_frame(*stream_data_blocked_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case3. Recv FIN in a STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_goal(3);
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICReceiveStreamState::SizeKnown);
 
     // Case4. Recv ALL data
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_progress(3);
-    ss.update_with_receiving_frame(*stream_frame_delayed);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_delayed));
     CHECK(ss.get() == QUICReceiveStreamState::DataRecvd);
 
     // Case5. Read data
     in_progress.set_transfer_complete(true);
-    ss.update_on_read();
+    CHECK(ss.update_on_read());
     CHECK(ss.get() == QUICReceiveStreamState::DataRead);
   }
 
@@ -242,16 +242,16 @@ TEST_CASE("QUICReceiveStreamState", "[quic]")
     // Case1. Recv STREAM
     QUICReceiveStreamStateMachine ss(&in_progress, nullptr);
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case2. Recv RESET_STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::RESET_STREAM));
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::ResetRecvd);
 
     // Case3. Handle reset
-    ss.update_on_eos();
+    CHECK(ss.update_on_eos());
     CHECK(ss.get() == QUICReceiveStreamState::ResetRead);
   }
 
@@ -262,17 +262,17 @@ TEST_CASE("QUICReceiveStreamState", "[quic]")
     // Case1. Recv STREAM
     QUICReceiveStreamStateMachine ss(&in_progress, nullptr);
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case2. Recv FIN in a STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICReceiveStreamState::SizeKnown);
 
     // Case3. Recv RESET_STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::RESET_STREAM));
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::ResetRecvd);
   }
 
@@ -284,24 +284,24 @@ TEST_CASE("QUICReceiveStreamState", "[quic]")
     QUICReceiveStreamStateMachine ss(&in_progress, nullptr);
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_progress(1);
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case2. Recv FIN in a STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_goal(3);
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICReceiveStreamState::SizeKnown);
 
     // Case3. Recv ALL data
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_progress(3);
-    ss.update_with_receiving_frame(*stream_frame_delayed);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_delayed));
     CHECK(ss.get() == QUICReceiveStreamState::DataRecvd);
 
     // Case4. Recv RESET_STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::RESET_STREAM));
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(!ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::DataRecvd);
   }
 
@@ -313,24 +313,24 @@ TEST_CASE("QUICReceiveStreamState", "[quic]")
     QUICReceiveStreamStateMachine ss(&in_progress, nullptr);
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_progress(1);
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case2. Recv FIN in a STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_goal(3);
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICReceiveStreamState::SizeKnown);
 
     // Case3. Recv RESET_STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::RESET_STREAM));
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::ResetRecvd);
 
     // Case4. Recv ALL data
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_progress(3);
-    ss.update_with_receiving_frame(*stream_frame_delayed);
+    CHECK(!ss.update_with_receiving_frame(*stream_frame_delayed));
     CHECK(ss.get() == QUICReceiveStreamState::ResetRecvd);
     CHECK(ss.is_allowed_to_send(QUICFrameType::STOP_SENDING) == false);
   }
@@ -342,18 +342,18 @@ TEST_CASE("QUICReceiveStreamState", "[quic]")
     // Case1. Recv STREAM
     QUICReceiveStreamStateMachine ss(&in_progress, nullptr);
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
     CHECK(ss.get() == QUICReceiveStreamState::Recv);
 
     // Case2. Recv FIN in a STREAM
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICReceiveStreamState::SizeKnown);
 
     // // Case3. Recv ALL data
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
     in_progress.set_transfer_complete(true);
-    ss.update_with_receiving_frame(*stream_frame_delayed);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_delayed));
     // ss.update_on_transport_recv_event();
     CHECK(ss.get() == QUICReceiveStreamState::DataRecvd);
 
@@ -390,11 +390,11 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
 
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
     in_progress.set_transfer_complete(true);
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_R);
   }
 
@@ -407,10 +407,10 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(ss.update_with_receiving_frame(*stream_frame));
 
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_R);
   }
 
@@ -423,17 +423,17 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame);
-
+    CHECK(ss.update_with_sending_frame(*stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
-    ss.update_with_sending_frame(*stream_frame_with_fin);
+
+    CHECK(ss.update_with_sending_frame(*stream_frame_with_fin)); // internal state is changed
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
 
     out_progress.set_transfer_complete(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
 
-    ss.update_with_sending_frame(*stream_frame_delayed);
+    CHECK(!ss.update_with_sending_frame(*stream_frame_delayed));
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
   }
 
@@ -446,10 +446,10 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame);
-
+    CHECK(ss.update_with_sending_frame(*stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
-    ss.update_with_sending_frame(*rst_stream_frame);
+
+    CHECK(ss.update_with_sending_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
   }
 
@@ -462,19 +462,19 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame);
-
+    CHECK(ss.update_with_sending_frame(*stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
-    ss.update_with_sending_frame(*rst_stream_frame);
+
+    CHECK(ss.update_with_sending_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
 
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(!ss.update_with_receiving_frame(*stream_frame));
 
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::Closed);
 
-    ss.update_on_eos();
+    CHECK(ss.update_on_eos()); // internal state is changed
     CHECK(ss.get() == QUICBidirectionalStreamState::Closed);
   }
 
@@ -487,20 +487,20 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_sending_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
     out_progress.set_transfer_complete(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
 
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame);
+    CHECK(!ss.update_with_receiving_frame(*stream_frame));
 
-    ss.update_with_receiving_frame(*rst_stream_frame);
+    CHECK(ss.update_with_receiving_frame(*rst_stream_frame));
     CHECK(ss.get() == QUICBidirectionalStreamState::Closed);
 
     in_progress.set_transfer_complete(true);
-    ss.update_on_eos();
+    CHECK(ss.update_on_eos()); // internal state is changed
     CHECK(ss.get() == QUICBidirectionalStreamState::Closed);
   }
 
@@ -513,20 +513,20 @@ TEST_CASE("QUICBidiState", "[quic]")
     CHECK(ss.get() == QUICBidirectionalStreamState::Idle);
 
     CHECK(ss.is_allowed_to_send(QUICFrameType::STREAM));
-    ss.update_with_sending_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_sending_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICBidirectionalStreamState::Open);
     out_progress.set_transfer_complete(true);
-    ss.update_on_ack();
+    CHECK(ss.update_on_ack());
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
 
     CHECK(ss.is_allowed_to_receive(QUICFrameType::STREAM));
-    ss.update_with_receiving_frame(*stream_frame_delayed);
+    CHECK(!ss.update_with_receiving_frame(*stream_frame_delayed));
 
-    ss.update_with_receiving_frame(*stream_frame_with_fin);
+    CHECK(ss.update_with_receiving_frame(*stream_frame_with_fin));
     CHECK(ss.get() == QUICBidirectionalStreamState::HC_L);
 
     in_progress.set_transfer_complete(true);
-    ss.update_on_read();
+    CHECK(ss.update_on_read());
     CHECK(ss.get() == QUICBidirectionalStreamState::Closed);
   }
 }
diff --git a/proxy/http3/Http09App.cc b/proxy/http3/Http09App.cc
index cbde812..b6dc02a 100644
--- a/proxy/http3/Http09App.cc
+++ b/proxy/http3/Http09App.cc
@@ -29,6 +29,7 @@
 #include "P_VConnection.h"
 #include "P_QUICNetVConnection.h"
 #include "QUICDebugNames.h"
+#include "QUICStreamVCAdapter.h"
 
 #include "Http3Session.h"
 #include "Http3Transaction.h"
@@ -54,40 +55,68 @@ Http09App::~Http09App()
   delete this->_ssn;
 }
 
+void
+Http09App::on_new_stream(QUICStream &stream)
+{
+  auto ret   = this->_streams.emplace(stream.id(), stream);
+  auto &info = ret.first->second;
+
+  switch (stream.direction()) {
+  case QUICStreamDirection::BIDIRECTIONAL:
+    info.setup_read_vio(this);
+    info.setup_write_vio(this);
+    break;
+  case QUICStreamDirection::SEND:
+    info.setup_write_vio(this);
+    break;
+  case QUICStreamDirection::RECEIVE:
+    info.setup_read_vio(this);
+    break;
+  default:
+    ink_assert(false);
+    break;
+  }
+
+  stream.set_io_adapter(&info.adapter);
+}
+
 int
 Http09App::main_event_handler(int event, Event *data)
 {
   Debug(debug_tag_v, "[%s] %s (%d)", this->_qc->cids().data(), get_vc_event_name(event), event);
 
-  VIO *vio                = reinterpret_cast<VIO *>(data);
-  QUICStreamIO *stream_io = this->_find_stream_io(vio);
+  VIO *vio                     = reinterpret_cast<VIO *>(data->cookie);
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
 
-  if (stream_io == nullptr) {
+  if (adapter == nullptr) {
     Debug(debug_tag, "[%s] Unknown Stream", this->_qc->cids().data());
     return -1;
   }
 
-  QUICStreamId stream_id = stream_io->stream_id();
+  bool is_bidirectional = adapter->stream().is_bidirectional();
+
+  QUICStreamId stream_id = adapter->stream().id();
   Http09Transaction *txn = static_cast<Http09Transaction *>(this->_ssn->get_transaction(stream_id));
 
-  uint8_t dummy;
   switch (event) {
   case VC_EVENT_READ_READY:
   case VC_EVENT_READ_COMPLETE:
-    if (!stream_io->is_bidirectional()) {
+    if (!is_bidirectional) {
       // FIXME Ignore unidirectional streams for now
       break;
     }
-    if (stream_io->peek(&dummy, 1)) {
-      if (txn == nullptr) {
-        txn = new Http09Transaction(this->_ssn, stream_io);
-        SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
 
+    if (txn == nullptr) {
+      if (auto ret = this->_streams.find(stream_id); ret != this->_streams.end()) {
+        txn = new Http09Transaction(this->_ssn, ret->second);
+        SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
         txn->new_transaction();
       } else {
-        SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
-        txn->handleEvent(event);
+        ink_assert(!"Stream info should exist");
       }
+    } else {
+      SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
+      txn->handleEvent(event);
     }
     break;
   case VC_EVENT_WRITE_READY:
diff --git a/proxy/http3/Http09App.h b/proxy/http3/Http09App.h
index ab35399..14ee8e6 100644
--- a/proxy/http3/Http09App.h
+++ b/proxy/http3/Http09App.h
@@ -28,6 +28,7 @@
 #include "HttpSessionAccept.h"
 
 #include "QUICApplication.h"
+#include "QUICStreamVCAdapter.h"
 
 class QUICNetVConnection;
 class Http09Session;
@@ -44,8 +45,11 @@ public:
   Http09App(QUICNetVConnection *client_vc, IpAllow::ACL &&session_acl, const HttpSessionAccept::Options &options);
   ~Http09App();
 
+  void on_new_stream(QUICStream &stream) override;
+
   int main_event_handler(int event, Event *data);
 
 private:
   Http09Session *_ssn = nullptr;
+  std::unordered_map<QUICStreamId, QUICStreamVCAdapter::IOInfo> _streams;
 };
diff --git a/proxy/http3/Http3App.cc b/proxy/http3/Http3App.cc
index e53cf14..1de466a 100644
--- a/proxy/http3/Http3App.cc
+++ b/proxy/http3/Http3App.cc
@@ -23,11 +23,14 @@
 
 #include "Http3App.h"
 
+#include <utility>
+
 #include "tscore/ink_resolver.h"
 
 #include "P_Net.h"
 #include "P_VConnection.h"
 #include "P_QUICNetVConnection.h"
+#include "QUICStreamVCAdapter.h"
 
 #include "Http3.h"
 #include "Http3Config.h"
@@ -71,10 +74,6 @@ Http3App::start()
   QUICConnectionErrorUPtr error;
 
   error = this->create_uni_stream(stream_id, Http3StreamType::CONTROL);
-  if (error == nullptr) {
-    this->_local_control_stream = this->_find_stream_io(stream_id);
-    this->_handle_uni_stream_on_write_ready(VC_EVENT_WRITE_READY, this->_local_control_stream);
-  }
 
   // TODO: Open uni streams for QPACK when dynamic table is used
   // error = this->create_uni_stream(stream_id, Http3StreamType::QPACK_ENCODER);
@@ -88,41 +87,68 @@ Http3App::start()
   // }
 }
 
+void
+Http3App::on_new_stream(QUICStream &stream)
+{
+  auto ret   = this->_streams.emplace(stream.id(), stream);
+  auto &info = ret.first->second;
+
+  switch (stream.direction()) {
+  case QUICStreamDirection::BIDIRECTIONAL:
+    info.setup_read_vio(this);
+    info.setup_write_vio(this);
+    break;
+  case QUICStreamDirection::SEND:
+    info.setup_write_vio(this);
+    break;
+  case QUICStreamDirection::RECEIVE:
+    info.setup_read_vio(this);
+    break;
+  default:
+    ink_assert(false);
+    break;
+  }
+
+  stream.set_io_adapter(&info.adapter);
+}
+
 int
 Http3App::main_event_handler(int event, Event *data)
 {
   Debug(debug_tag_v, "[%s] %s (%d)", this->_qc->cids().data(), get_vc_event_name(event), event);
 
-  VIO *vio                = reinterpret_cast<VIO *>(data);
-  QUICStreamIO *stream_io = this->_find_stream_io(vio);
+  VIO *vio                     = reinterpret_cast<VIO *>(data->cookie);
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
 
-  if (stream_io == nullptr) {
+  if (adapter == nullptr) {
     Debug(debug_tag, "[%s] Unknown Stream", this->_qc->cids().data());
     return -1;
   }
 
+  bool is_bidirectional = adapter->stream().is_bidirectional();
+
   switch (event) {
   case VC_EVENT_READ_READY:
   case VC_EVENT_READ_COMPLETE:
-    if (stream_io->is_bidirectional()) {
-      this->_handle_bidi_stream_on_read_ready(event, stream_io);
+    if (is_bidirectional) {
+      this->_handle_bidi_stream_on_read_ready(event, vio);
     } else {
-      this->_handle_uni_stream_on_read_ready(event, stream_io);
+      this->_handle_uni_stream_on_read_ready(event, vio);
     }
     break;
   case VC_EVENT_WRITE_READY:
   case VC_EVENT_WRITE_COMPLETE:
-    if (stream_io->is_bidirectional()) {
-      this->_handle_bidi_stream_on_write_ready(event, stream_io);
+    if (is_bidirectional) {
+      this->_handle_bidi_stream_on_write_ready(event, vio);
     } else {
-      this->_handle_uni_stream_on_write_ready(event, stream_io);
+      this->_handle_uni_stream_on_write_ready(event, vio);
     }
     break;
   case VC_EVENT_EOS:
-    if (stream_io->is_bidirectional()) {
-      this->_handle_bidi_stream_on_eos(event, stream_io);
+    if (is_bidirectional) {
+      this->_handle_bidi_stream_on_eos(event, vio);
     } else {
-      this->_handle_uni_stream_on_eos(event, stream_io);
+      this->_handle_uni_stream_on_eos(event, vio);
     }
     break;
   case VC_EVENT_ERROR:
@@ -143,11 +169,6 @@ Http3App::create_uni_stream(QUICStreamId &new_stream_id, Http3StreamType type)
   QUICConnectionErrorUPtr error = this->_qc->stream_manager()->create_uni_stream(new_stream_id);
 
   if (error == nullptr) {
-    QUICStreamIO *stream_io = this->_find_stream_io(new_stream_id);
-
-    uint8_t buf[] = {static_cast<uint8_t>(type)};
-    stream_io->write(buf, sizeof(uint8_t));
-
     this->_local_uni_stream_map.insert(std::make_pair(new_stream_id, type));
 
     Debug("http3", "[%" PRIu64 "] %s stream is created", new_stream_id, Http3DebugNames::stream_type(type));
@@ -159,26 +180,24 @@ Http3App::create_uni_stream(QUICStreamId &new_stream_id, Http3StreamType type)
 }
 
 void
-Http3App::_handle_uni_stream_on_read_ready(int /* event */, QUICStreamIO *stream_io)
+Http3App::_handle_uni_stream_on_read_ready(int /* event */, VIO *vio)
 {
   Http3StreamType type;
-  auto it = this->_remote_uni_stream_map.find(stream_io->stream_id());
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
+  auto it                      = this->_remote_uni_stream_map.find(adapter->stream().id());
   if (it == this->_remote_uni_stream_map.end()) {
     // Set uni stream suitable app (HTTP/3 or QPACK) by stream type
     uint8_t buf;
-    stream_io->read(&buf, 1);
+    vio->get_reader()->read(&buf, 1);
     type = Http3Stream::type(&buf);
 
-    Debug("http3", "[%d] %s stream is opened", stream_io->stream_id(), Http3DebugNames::stream_type(type));
+    Debug("http3", "[%" PRIu64 "] %s stream is opened", adapter->stream().id(), Http3DebugNames::stream_type(type));
 
-    if (type == Http3StreamType::CONTROL) {
-      if (this->_remote_control_stream) {
-        // TODO: make error
-      }
-      this->_remote_control_stream = stream_io;
+    auto ret = this->_remote_uni_stream_map.insert(std::make_pair(adapter->stream().id(), type));
+    if (!ret.second) {
+      // A stream for the type is already exisits
+      // TODO Return an error
     }
-
-    this->_remote_uni_stream_map.insert(std::make_pair(stream_io->stream_id(), type));
   } else {
     type = it->second;
   }
@@ -187,13 +206,13 @@ Http3App::_handle_uni_stream_on_read_ready(int /* event */, QUICStreamIO *stream
   case Http3StreamType::CONTROL:
   case Http3StreamType::PUSH: {
     uint64_t nread = 0;
-    this->_control_stream_dispatcher.on_read_ready(*stream_io, nread);
+    this->_control_stream_dispatcher.on_read_ready(adapter->stream().id(), *vio->get_reader(), nread);
     // TODO: when PUSH comes from client, send stream error with HTTP_WRONG_STREAM_DIRECTION
     break;
   }
   case Http3StreamType::QPACK_ENCODER:
   case Http3StreamType::QPACK_DECODER: {
-    this->_set_qpack_stream(type, stream_io);
+    this->_set_qpack_stream(type, adapter);
   }
   case Http3StreamType::UNKNOWN:
   default:
@@ -203,43 +222,57 @@ Http3App::_handle_uni_stream_on_read_ready(int /* event */, QUICStreamIO *stream
 }
 
 void
-Http3App::_handle_bidi_stream_on_read_ready(int event, QUICStreamIO *stream_io)
+Http3App::_handle_bidi_stream_on_read_ready(int event, VIO *vio)
 {
-  uint8_t dummy;
-  if (stream_io->peek(&dummy, 1)) {
-    QUICStreamId stream_id = stream_io->stream_id();
-    Http3Transaction *txn  = static_cast<Http3Transaction *>(this->_ssn->get_transaction(stream_id));
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
 
-    if (txn == nullptr) {
-      txn = new Http3Transaction(this->_ssn, stream_io);
-      SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
+  QUICStreamId stream_id = adapter->stream().id();
+  Http3Transaction *txn  = static_cast<Http3Transaction *>(this->_ssn->get_transaction(stream_id));
 
+  if (txn == nullptr) {
+    if (auto ret = this->_streams.find(stream_id); ret != this->_streams.end()) {
+      txn = new Http3Transaction(this->_ssn, ret->second);
+      SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
       txn->new_transaction();
     } else {
-      SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
-      txn->handleEvent(event);
+      ink_assert(!"Stream info should exist");
     }
+  } else {
+    SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
+    txn->handleEvent(event);
   }
 }
 
 void
-Http3App::_handle_uni_stream_on_write_ready(int /* event */, QUICStreamIO *stream_io)
+Http3App::_handle_uni_stream_on_write_ready(int /* event */, VIO *vio)
 {
-  auto it = this->_local_uni_stream_map.find(stream_io->stream_id());
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
+
+  auto it = this->_local_uni_stream_map.find(adapter->stream().id());
   if (it == this->_local_uni_stream_map.end()) {
     ink_abort("stream not found");
     return;
   }
 
   switch (it->second) {
-  case Http3StreamType::CONTROL: {
-    size_t nwritten = 0;
-    this->_control_stream_collector.on_write_ready(stream_io, nwritten);
+  case Http3StreamType::CONTROL:
+    if (!this->_is_control_stream_initialized) {
+      uint8_t buf[] = {static_cast<uint8_t>(it->second)};
+      vio->get_writer()->write(buf, sizeof(uint8_t));
+      this->_is_control_stream_initialized = true;
+    } else {
+      size_t nwritten = 0;
+      bool all_done   = false;
+      this->_control_stream_collector.on_write_ready(adapter->stream().id(), *vio->get_writer(), nwritten, all_done);
+      vio->nbytes += nwritten;
+      if (all_done) {
+        vio->done();
+      }
+    }
     break;
-  }
   case Http3StreamType::QPACK_ENCODER:
   case Http3StreamType::QPACK_DECODER: {
-    this->_set_qpack_stream(it->second, stream_io);
+    this->_set_qpack_stream(it->second, adapter);
   }
   case Http3StreamType::UNKNOWN:
   case Http3StreamType::PUSH:
@@ -249,32 +282,32 @@ Http3App::_handle_uni_stream_on_write_ready(int /* event */, QUICStreamIO *strea
 }
 
 void
-Http3App::_handle_bidi_stream_on_eos(int /* event */, QUICStreamIO *stream_io)
+Http3App::_handle_bidi_stream_on_eos(int /* event */, VIO *vio)
 {
   // TODO: handle eos
 }
 
 void
-Http3App::_handle_uni_stream_on_eos(int /* event */, QUICStreamIO *stream_io)
+Http3App::_handle_uni_stream_on_eos(int /* event */, VIO *v)
 {
   // TODO: handle eos
 }
 
 void
-Http3App::_set_qpack_stream(Http3StreamType type, QUICStreamIO *stream_io)
+Http3App::_set_qpack_stream(Http3StreamType type, QUICStreamVCAdapter *adapter)
 {
   // Change app to QPACK from Http3
   if (type == Http3StreamType::QPACK_ENCODER) {
     if (this->_qc->direction() == NET_VCONNECTION_IN) {
-      this->_ssn->remote_qpack()->set_encoder_stream(stream_io);
+      this->_ssn->remote_qpack()->set_encoder_stream(adapter->stream().id());
     } else {
-      this->_ssn->local_qpack()->set_encoder_stream(stream_io);
+      this->_ssn->local_qpack()->set_encoder_stream(adapter->stream().id());
     }
   } else if (type == Http3StreamType::QPACK_DECODER) {
     if (this->_qc->direction() == NET_VCONNECTION_IN) {
-      this->_ssn->local_qpack()->set_decoder_stream(stream_io);
+      this->_ssn->local_qpack()->set_decoder_stream(adapter->stream().id());
     } else {
-      this->_ssn->remote_qpack()->set_decoder_stream(stream_io);
+      this->_ssn->remote_qpack()->set_decoder_stream(adapter->stream().id());
     }
   } else {
     ink_abort("unknown stream type");
@@ -282,9 +315,11 @@ Http3App::_set_qpack_stream(Http3StreamType type, QUICStreamIO *stream_io)
 }
 
 void
-Http3App::_handle_bidi_stream_on_write_ready(int event, QUICStreamIO *stream_io)
+Http3App::_handle_bidi_stream_on_write_ready(int event, VIO *vio)
 {
-  QUICStreamId stream_id = stream_io->stream_id();
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
+
+  QUICStreamId stream_id = adapter->stream().id();
   Http3Transaction *txn  = static_cast<Http3Transaction *>(this->_ssn->get_transaction(stream_id));
   if (txn != nullptr) {
     SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
@@ -353,7 +388,7 @@ Http3SettingsHandler::handle_frame(std::shared_ptr<const Http3Frame> frame)
 // SETTINGS frame framer
 //
 Http3FrameUPtr
-Http3SettingsFramer::generate_frame(uint16_t max_size)
+Http3SettingsFramer::generate_frame()
 {
   if (this->_is_sent) {
     return Http3FrameFactory::create_null_frame();
diff --git a/proxy/http3/Http3App.h b/proxy/http3/Http3App.h
index a48432a..5de5aea 100644
--- a/proxy/http3/Http3App.h
+++ b/proxy/http3/Http3App.h
@@ -23,9 +23,12 @@
 
 #pragma once
 
+#include <map>
+
 #include "IPAllow.h"
 
 #include "QUICApplication.h"
+#include "QUICStreamVCAdapter.h"
 
 #include "HttpSessionAccept.h"
 
@@ -48,6 +51,8 @@ public:
   Http3App(QUICNetVConnection *client_vc, IpAllow::ACL &&session_acl, const HttpSessionAccept::Options &options);
   virtual ~Http3App();
 
+  void on_new_stream(QUICStream &stream) override;
+
   virtual void start();
   virtual int main_event_handler(int event, Event *data);
 
@@ -58,15 +63,17 @@ public:
 protected:
   Http3Session *_ssn = nullptr;
 
+  std::unordered_map<QUICStreamId, QUICStreamVCAdapter::IOInfo> _streams;
+
 private:
-  void _handle_uni_stream_on_read_ready(int event, QUICStreamIO *stream_io);
-  void _handle_uni_stream_on_write_ready(int event, QUICStreamIO *stream_io);
-  void _handle_uni_stream_on_eos(int event, QUICStreamIO *stream_io);
-  void _handle_bidi_stream_on_read_ready(int event, QUICStreamIO *stream_io);
-  void _handle_bidi_stream_on_write_ready(int event, QUICStreamIO *stream_io);
-  void _handle_bidi_stream_on_eos(int event, QUICStreamIO *stream_io);
+  void _handle_uni_stream_on_read_ready(int event, VIO *vio);
+  void _handle_uni_stream_on_write_ready(int event, VIO *vio);
+  void _handle_uni_stream_on_eos(int event, VIO *vio);
+  void _handle_bidi_stream_on_read_ready(int event, VIO *vio);
+  void _handle_bidi_stream_on_write_ready(int event, VIO *vio);
+  void _handle_bidi_stream_on_eos(int event, VIO *vio);
 
-  void _set_qpack_stream(Http3StreamType type, QUICStreamIO *stream_io);
+  void _set_qpack_stream(Http3StreamType type, QUICStreamVCAdapter *adapter);
 
   Http3FrameHandler *_settings_handler  = nullptr;
   Http3FrameGenerator *_settings_framer = nullptr;
@@ -74,11 +81,10 @@ private:
   Http3FrameDispatcher _control_stream_dispatcher;
   Http3FrameCollector _control_stream_collector;
 
-  QUICStreamIO *_remote_control_stream;
-  QUICStreamIO *_local_control_stream;
-
   std::map<QUICStreamId, Http3StreamType> _remote_uni_stream_map;
   std::map<QUICStreamId, Http3StreamType> _local_uni_stream_map;
+
+  bool _is_control_stream_initialized = false;
 };
 
 class Http3SettingsHandler : public Http3FrameHandler
@@ -101,7 +107,7 @@ public:
   Http3SettingsFramer(NetVConnectionContext_t context) : _context(context){};
 
   // Http3FrameGenerator
-  Http3FrameUPtr generate_frame(uint16_t max_size) override;
+  Http3FrameUPtr generate_frame() override;
   bool is_done() const override;
 
 private:
diff --git a/proxy/http3/Http3DataFramer.cc b/proxy/http3/Http3DataFramer.cc
index eaf5862..5fbb56d 100644
--- a/proxy/http3/Http3DataFramer.cc
+++ b/proxy/http3/Http3DataFramer.cc
@@ -28,7 +28,7 @@
 Http3DataFramer::Http3DataFramer(Http3Transaction *transaction, VIO *source) : _transaction(transaction), _source_vio(source) {}
 
 Http3FrameUPtr
-Http3DataFramer::generate_frame(uint16_t max_size)
+Http3DataFramer::generate_frame()
 {
   if (!this->_transaction->is_response_header_sent()) {
     return Http3FrameFactory::create_null_frame();
@@ -37,11 +37,7 @@ Http3DataFramer::generate_frame(uint16_t max_size)
   Http3FrameUPtr frame   = Http3FrameFactory::create_null_frame();
   IOBufferReader *reader = this->_source_vio->get_reader();
 
-  if (max_size <= Http3Frame::MAX_FRAM_HEADER_OVERHEAD) {
-    return frame;
-  }
-
-  size_t payload_len = max_size - Http3Frame::MAX_FRAM_HEADER_OVERHEAD;
+  size_t payload_len = 128 * 1024;
   if (!reader->is_read_avail_more_than(payload_len)) {
     payload_len = reader->read_avail();
   }
diff --git a/proxy/http3/Http3DataFramer.h b/proxy/http3/Http3DataFramer.h
index 045e86c..0db8715 100644
--- a/proxy/http3/Http3DataFramer.h
+++ b/proxy/http3/Http3DataFramer.h
@@ -35,7 +35,7 @@ public:
   Http3DataFramer(Http3Transaction *transaction, VIO *source);
 
   // Http3FrameGenerator
-  Http3FrameUPtr generate_frame(uint16_t max_size) override;
+  Http3FrameUPtr generate_frame() override;
   bool is_done() const override;
 
 private:
diff --git a/proxy/http3/Http3Frame.cc b/proxy/http3/Http3Frame.cc
index db8c98b..6bb195e 100644
--- a/proxy/http3/Http3Frame.cc
+++ b/proxy/http3/Http3Frame.cc
@@ -31,6 +31,8 @@ ClassAllocator<Http3DataFrame> http3DataFrameAllocator("http3DataFrameAllocator"
 ClassAllocator<Http3HeadersFrame> http3HeadersFrameAllocator("http3HeadersFrameAllocator");
 ClassAllocator<Http3SettingsFrame> http3SettingsFrameAllocator("http3SettingsFrameAllocator");
 
+constexpr int HEADER_OVERHEAD = 10; // This should work as long as a payload length is less than 64 bits
+
 //
 // Static functions
 //
@@ -100,11 +102,11 @@ Http3Frame::type() const
   }
 }
 
-void
-Http3Frame::store(uint8_t *buf, size_t *len) const
+Ptr<IOBufferBlock>
+Http3Frame::to_io_buffer_block() const
 {
-  // If you really need this, you should keep the data passed to its constructor
-  ink_assert(!"Not supported");
+  Ptr<IOBufferBlock> block;
+  return block;
 }
 
 void
@@ -119,11 +121,20 @@ Http3Frame::reset(const uint8_t *buf, size_t len)
 //
 Http3UnknownFrame::Http3UnknownFrame(const uint8_t *buf, size_t buf_len) : Http3Frame(buf, buf_len), _buf(buf), _buf_len(buf_len) {}
 
-void
-Http3UnknownFrame::store(uint8_t *buf, size_t *len) const
+Ptr<IOBufferBlock>
+Http3UnknownFrame::to_io_buffer_block() const
 {
-  memcpy(buf, this->_buf, this->_buf_len);
-  *len = this->_buf_len;
+  Ptr<IOBufferBlock> block;
+  size_t n = 0;
+
+  block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
+  block->alloc(iobuffer_size_to_index(HEADER_OVERHEAD + this->length(), BUFFER_SIZE_INDEX_32K));
+  uint8_t *block_start = reinterpret_cast<uint8_t *>(block->start());
+  memcpy(block_start, this->_buf, this->_buf_len);
+  n += this->_buf_len;
+
+  block->fill(n);
+  return block;
 }
 
 //
@@ -142,18 +153,26 @@ Http3DataFrame::Http3DataFrame(ats_unique_buf payload, size_t payload_len)
   this->_payload = this->_payload_uptr.get();
 }
 
-void
-Http3DataFrame::store(uint8_t *buf, size_t *len) const
+Ptr<IOBufferBlock>
+Http3DataFrame::to_io_buffer_block() const
 {
+  Ptr<IOBufferBlock> block;
+  size_t n       = 0;
   size_t written = 0;
-  size_t n;
-  QUICVariableInt::encode(buf, UINT64_MAX, n, static_cast<uint64_t>(this->_type));
+
+  block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
+  block->alloc(iobuffer_size_to_index(HEADER_OVERHEAD + this->length(), BUFFER_SIZE_INDEX_32K));
+  uint8_t *block_start = reinterpret_cast<uint8_t *>(block->start());
+
+  QUICVariableInt::encode(block_start, UINT64_MAX, n, static_cast<uint64_t>(this->_type));
   written += n;
-  QUICVariableInt::encode(buf + written, UINT64_MAX, n, this->_length);
+  QUICVariableInt::encode(block_start + written, UINT64_MAX, n, this->_length);
   written += n;
-  memcpy(buf + written, this->_payload, this->_payload_len);
+  memcpy(block_start + written, this->_payload, this->_payload_len);
   written += this->_payload_len;
-  *len = written;
+
+  block->fill(written);
+  return block;
 }
 
 void
@@ -191,18 +210,26 @@ Http3HeadersFrame::Http3HeadersFrame(ats_unique_buf header_block, size_t header_
   this->_header_block = this->_header_block_uptr.get();
 }
 
-void
-Http3HeadersFrame::store(uint8_t *buf, size_t *len) const
+Ptr<IOBufferBlock>
+Http3HeadersFrame::to_io_buffer_block() const
 {
+  Ptr<IOBufferBlock> block;
+  size_t n       = 0;
   size_t written = 0;
-  size_t n;
-  QUICVariableInt::encode(buf, UINT64_MAX, n, static_cast<uint64_t>(this->_type));
+
+  block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
+  block->alloc(iobuffer_size_to_index(HEADER_OVERHEAD + this->length(), BUFFER_SIZE_INDEX_32K));
+  uint8_t *block_start = reinterpret_cast<uint8_t *>(block->start());
+
+  QUICVariableInt::encode(block_start, UINT64_MAX, n, static_cast<uint64_t>(this->_type));
   written += n;
-  QUICVariableInt::encode(buf + written, UINT64_MAX, n, this->_length);
+  QUICVariableInt::encode(block_start + written, UINT64_MAX, n, this->_length);
   written += n;
-  memcpy(buf + written, this->_header_block, this->_header_block_len);
+  memcpy(block_start + written, this->_header_block, this->_header_block_len);
   written += this->_header_block_len;
-  *len = written;
+
+  block->fill(written);
+  return block;
 }
 
 void
@@ -270,40 +297,48 @@ Http3SettingsFrame::Http3SettingsFrame(const uint8_t *buf, size_t buf_len, uint3
   }
 }
 
-void
-Http3SettingsFrame::store(uint8_t *buf, size_t *len) const
+Ptr<IOBufferBlock>
+Http3SettingsFrame::to_io_buffer_block() const
 {
-  uint8_t payload[Http3SettingsFrame::MAX_PAYLOAD_SIZE] = {0};
-  uint8_t *p                                            = payload;
-  size_t l                                              = 0;
+  Ptr<IOBufferBlock> header_block;
+  Ptr<IOBufferBlock> payload_block;
+  size_t n       = 0;
+  size_t written = 0;
+
+  payload_block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
+  payload_block->alloc(iobuffer_size_to_index(Http3SettingsFrame::MAX_PAYLOAD_SIZE, BUFFER_SIZE_INDEX_32K));
+  uint8_t *payload_block_start = reinterpret_cast<uint8_t *>(payload_block->start());
 
   for (auto &it : this->_settings) {
-    QUICIntUtil::write_QUICVariableInt(static_cast<uint64_t>(it.first), p, &l);
-    p += l;
-    QUICIntUtil::write_QUICVariableInt(it.second, p, &l);
-    p += l;
+    QUICIntUtil::write_QUICVariableInt(static_cast<uint64_t>(it.first), payload_block_start + written, &n);
+    written += n;
+    QUICIntUtil::write_QUICVariableInt(it.second, payload_block_start + written, &n);
+    written += n;
   }
 
   // Exercise the requirement that unknown identifiers be ignored. - 4.2.5.1.
-  QUICIntUtil::write_QUICVariableInt(static_cast<uint64_t>(Http3SettingsId::UNKNOWN), p, &l);
-  p += l;
-  QUICIntUtil::write_QUICVariableInt(0, p, &l);
-  p += l;
+  QUICIntUtil::write_QUICVariableInt(static_cast<uint64_t>(Http3SettingsId::UNKNOWN), payload_block_start + written, &n);
+  written += n;
+  QUICIntUtil::write_QUICVariableInt(0, payload_block_start + written, &n);
+  written += n;
+  payload_block->fill(written);
 
-  size_t written     = 0;
-  size_t payload_len = p - payload;
+  size_t payload_len = written;
+  written            = 0;
+
+  header_block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
+  header_block->alloc(iobuffer_size_to_index(HEADER_OVERHEAD, BUFFER_SIZE_INDEX_32K));
+  uint8_t *header_block_start = reinterpret_cast<uint8_t *>(header_block->start());
 
-  size_t n;
-  QUICVariableInt::encode(buf, UINT64_MAX, n, static_cast<uint64_t>(this->_type));
+  QUICVariableInt::encode(header_block_start, UINT64_MAX, n, static_cast<uint64_t>(this->_type));
   written += n;
-  QUICVariableInt::encode(buf + written, UINT64_MAX, n, payload_len);
+  QUICVariableInt::encode(header_block_start + written, UINT64_MAX, n, payload_len);
   written += n;
+  header_block->fill(written);
 
-  // Payload
-  memcpy(buf + written, payload, payload_len);
-  written += payload_len;
+  header_block->next = payload_block;
 
-  *len = written;
+  return header_block;
 }
 
 void
@@ -415,14 +450,14 @@ Http3FrameFactory::fast_create(const uint8_t *buf, size_t len)
 }
 
 std::shared_ptr<const Http3Frame>
-Http3FrameFactory::fast_create(QUICStreamIO &stream_io, size_t frame_len)
+Http3FrameFactory::fast_create(IOBufferReader &reader, size_t frame_len)
 {
   uint8_t buf[65536];
 
   // FIXME DATA frames can be giga bytes
   ink_assert(sizeof(buf) > frame_len);
 
-  if (stream_io.peek(buf, frame_len) < static_cast<int64_t>(frame_len)) {
+  if (reader.read(buf, frame_len) < static_cast<int64_t>(frame_len)) {
     // Return if whole frame data is not available
     return nullptr;
   }
diff --git a/proxy/http3/Http3Frame.h b/proxy/http3/Http3Frame.h
index 4ae00a9..0207c2f 100644
--- a/proxy/http3/Http3Frame.h
+++ b/proxy/http3/Http3Frame.h
@@ -42,7 +42,7 @@ public:
   uint64_t total_length() const;
   uint64_t length() const;
   Http3FrameType type() const;
-  virtual void store(uint8_t *buf, size_t *len) const;
+  virtual Ptr<IOBufferBlock> to_io_buffer_block() const;
   virtual void reset(const uint8_t *buf, size_t len);
   static int length(const uint8_t *buf, size_t buf_len, uint64_t &length);
   static Http3FrameType type(const uint8_t *buf, size_t buf_len);
@@ -59,7 +59,7 @@ public:
   Http3UnknownFrame() : Http3Frame() {}
   Http3UnknownFrame(const uint8_t *buf, size_t len);
 
-  void store(uint8_t *buf, size_t *len) const override;
+  Ptr<IOBufferBlock> to_io_buffer_block() const override;
 
 protected:
   const uint8_t *_buf = nullptr;
@@ -77,7 +77,7 @@ public:
   Http3DataFrame(const uint8_t *buf, size_t len);
   Http3DataFrame(ats_unique_buf payload, size_t payload_len);
 
-  void store(uint8_t *buf, size_t *len) const override;
+  Ptr<IOBufferBlock> to_io_buffer_block() const override;
   void reset(const uint8_t *buf, size_t len) override;
 
   const uint8_t *payload() const;
@@ -100,7 +100,7 @@ public:
   Http3HeadersFrame(const uint8_t *buf, size_t len);
   Http3HeadersFrame(ats_unique_buf header_block, size_t header_block_len);
 
-  void store(uint8_t *buf, size_t *len) const override;
+  Ptr<IOBufferBlock> to_io_buffer_block() const override;
   void reset(const uint8_t *buf, size_t len) override;
 
   const uint8_t *header_block() const;
@@ -130,7 +130,7 @@ public:
     Http3SettingsId::NUM_PLACEHOLDERS,
   };
 
-  void store(uint8_t *buf, size_t *len) const override;
+  Ptr<IOBufferBlock> to_io_buffer_block() const override;
   void reset(const uint8_t *buf, size_t len) override;
 
   bool is_valid() const;
@@ -223,7 +223,7 @@ public:
    * This works almost the same as create() but it reuses created objects for performance.
    * If you create a frame object which has the same frame type that you created before, the object will be reset by new data.
    */
-  std::shared_ptr<const Http3Frame> fast_create(QUICStreamIO &stream_io, size_t frame_len);
+  std::shared_ptr<const Http3Frame> fast_create(IOBufferReader &reader, size_t frame_len);
   std::shared_ptr<const Http3Frame> fast_create(const uint8_t *buf, size_t len);
 
   /*
diff --git a/proxy/http3/Http3FrameCollector.cc b/proxy/http3/Http3FrameCollector.cc
index 213a1e3..3118bac 100644
--- a/proxy/http3/Http3FrameCollector.cc
+++ b/proxy/http3/Http3FrameCollector.cc
@@ -26,10 +26,9 @@
 #include "Http3DebugNames.h"
 
 Http3ErrorUPtr
-Http3FrameCollector::on_write_ready(QUICStreamIO *stream_io, size_t &nwritten)
+Http3FrameCollector::on_write_ready(QUICStreamId stream_id, MIOBuffer &writer, size_t &nwritten, bool &all_done)
 {
-  bool all_done = true;
-  uint8_t tmp[32768];
+  all_done = true;
   nwritten = 0;
 
   for (auto g : this->_generators) {
@@ -37,26 +36,17 @@ Http3FrameCollector::on_write_ready(QUICStreamIO *stream_io, size_t &nwritten)
       continue;
     }
     size_t len           = 0;
-    Http3FrameUPtr frame = g->generate_frame(sizeof(tmp) - nwritten);
+    Http3FrameUPtr frame = g->generate_frame();
     if (frame) {
-      frame->store(tmp + nwritten, &len);
+      auto b = frame->to_io_buffer_block();
+      len    = writer.write(b.get(), INT64_MAX, 0);
       nwritten += len;
-
-      Debug("http3", "[TX] [%d] | %s size=%zu", stream_io->stream_id(), Http3DebugNames::frame_type(frame->type()), len);
+      Debug("http3", "[TX] [%" PRIu64 "] | %s size=%zu", stream_id, Http3DebugNames::frame_type(frame->type()), len);
     }
 
     all_done &= g->is_done();
   }
 
-  if (nwritten) {
-    int64_t len = stream_io->write(tmp, nwritten);
-    ink_assert(len > 0 && (uint64_t)len == nwritten);
-  }
-
-  if (all_done) {
-    stream_io->write_done();
-  }
-
   return Http3ErrorUPtr(new Http3NoError());
 }
 
diff --git a/proxy/http3/Http3FrameCollector.h b/proxy/http3/Http3FrameCollector.h
index 73b7c5c..3faaf5d 100644
--- a/proxy/http3/Http3FrameCollector.h
+++ b/proxy/http3/Http3FrameCollector.h
@@ -28,10 +28,12 @@
 #include "Http3FrameGenerator.h"
 #include <vector>
 
+class QUICStreamVCAdapter;
+
 class Http3FrameCollector
 {
 public:
-  Http3ErrorUPtr on_write_ready(QUICStreamIO *stream_io, size_t &nread);
+  Http3ErrorUPtr on_write_ready(QUICStreamId stream_id, MIOBuffer &writer, size_t &nread, bool &all_done);
 
   void add_generator(Http3FrameGenerator *generator);
 
diff --git a/proxy/http3/Http3FrameDispatcher.cc b/proxy/http3/Http3FrameDispatcher.cc
index 64df8d5..5a578c2 100644
--- a/proxy/http3/Http3FrameDispatcher.cc
+++ b/proxy/http3/Http3FrameDispatcher.cc
@@ -41,7 +41,7 @@ Http3FrameDispatcher::add_handler(Http3FrameHandler *handler)
 }
 
 Http3ErrorUPtr
-Http3FrameDispatcher::on_read_ready(QUICStreamIO &stream_io, uint64_t &nread)
+Http3FrameDispatcher::on_read_ready(QUICStreamId stream_id, IOBufferReader &reader, uint64_t &nread)
 {
   std::shared_ptr<const Http3Frame> frame(nullptr);
   Http3ErrorUPtr error = Http3ErrorUPtr(new Http3NoError());
@@ -50,7 +50,8 @@ Http3FrameDispatcher::on_read_ready(QUICStreamIO &stream_io, uint64_t &nread)
   while (true) {
     // Read a length of Type field and hopefully a length of Length field too
     uint8_t head[16];
-    int64_t read_len = stream_io.peek(head, 16);
+    auto p           = reader.memcpy(head, 16);
+    int64_t read_len = p - reinterpret_cast<char *>(head);
     Debug("v_http3", "reading H3 frame: state=%d read_len=%" PRId64, this->_reading_state, read_len);
 
     if (this->_reading_state == READING_TYPE_LEN) {
@@ -87,19 +88,21 @@ Http3FrameDispatcher::on_read_ready(QUICStreamIO &stream_io, uint64_t &nread)
     if (this->_reading_state == READING_PAYLOAD) {
       // Create a frame
       // Type field length + Length field length + Payload length
-      size_t frame_len = this->_reading_frame_type_len + this->_reading_frame_length_len + this->_reading_frame_payload_len;
-      frame            = this->_frame_factory.fast_create(stream_io, frame_len);
+      size_t frame_len   = this->_reading_frame_type_len + this->_reading_frame_length_len + this->_reading_frame_payload_len;
+      auto cloned_reader = reader.clone();
+      frame              = this->_frame_factory.fast_create(*cloned_reader, frame_len);
+      cloned_reader->dealloc();
       if (frame == nullptr) {
         break;
       }
 
       // Consume buffer if frame is created
       nread += frame_len;
-      stream_io.consume(frame_len);
+      reader.consume(frame_len);
 
       // Dispatch
       Http3FrameType type = frame->type();
-      Debug("http3", "[RX] [%d] | %s size=%zu", stream_io.stream_id(), Http3DebugNames::frame_type(type), frame_len);
+      Debug("http3", "[RX] [%" PRIu64 "] | %s size=%zu", stream_id, Http3DebugNames::frame_type(type), frame_len);
       std::vector<Http3FrameHandler *> handlers = this->_handlers[static_cast<uint8_t>(type)];
       for (auto h : handlers) {
         error = h->handle_frame(frame);
diff --git a/proxy/http3/Http3FrameDispatcher.h b/proxy/http3/Http3FrameDispatcher.h
index fae5121..315a5b0 100644
--- a/proxy/http3/Http3FrameDispatcher.h
+++ b/proxy/http3/Http3FrameDispatcher.h
@@ -28,10 +28,12 @@
 #include "Http3FrameHandler.h"
 #include <vector>
 
+class QUICStreamVCAdapter;
+
 class Http3FrameDispatcher
 {
 public:
-  Http3ErrorUPtr on_read_ready(QUICStreamIO &stream_io, uint64_t &nread);
+  Http3ErrorUPtr on_read_ready(QUICStreamId stream_id, IOBufferReader &reader, uint64_t &nread);
 
   void add_handler(Http3FrameHandler *handler);
 
diff --git a/proxy/http3/Http3FrameGenerator.h b/proxy/http3/Http3FrameGenerator.h
index 6da188f..de5a4a1 100644
--- a/proxy/http3/Http3FrameGenerator.h
+++ b/proxy/http3/Http3FrameGenerator.h
@@ -29,6 +29,6 @@ class Http3FrameGenerator
 {
 public:
   virtual ~Http3FrameGenerator(){};
-  virtual Http3FrameUPtr generate_frame(uint16_t max_size) = 0;
-  virtual bool is_done() const                             = 0;
+  virtual Http3FrameUPtr generate_frame() = 0;
+  virtual bool is_done() const            = 0;
 };
diff --git a/proxy/http3/Http3HeaderFramer.cc b/proxy/http3/Http3HeaderFramer.cc
index b69f298..079c5d2 100644
--- a/proxy/http3/Http3HeaderFramer.cc
+++ b/proxy/http3/Http3HeaderFramer.cc
@@ -38,8 +38,12 @@ Http3HeaderFramer::Http3HeaderFramer(Http3Transaction *transaction, VIO *source,
 }
 
 Http3FrameUPtr
-Http3HeaderFramer::generate_frame(uint16_t max_size)
+Http3HeaderFramer::generate_frame()
 {
+  if (!this->_source_vio->get_reader()) {
+    return Http3FrameFactory::create_null_frame();
+  }
+
   ink_assert(!this->_transaction->is_response_header_sent());
 
   if (!this->_header_block) {
@@ -48,8 +52,7 @@ Http3HeaderFramer::generate_frame(uint16_t max_size)
   }
 
   if (this->_header_block) {
-    // Create frames on demand base on max_size since we don't know how much we can write now
-    uint64_t len = std::min(this->_header_block_len - this->_header_block_wrote, static_cast<uint64_t>(max_size));
+    uint64_t len = std::min(this->_header_block_len - this->_header_block_wrote, UINT64_C(64 * 1024));
 
     Http3FrameUPtr frame = Http3FrameFactory::create_headers_frame(this->_header_block_reader, len);
 
diff --git a/proxy/http3/Http3HeaderFramer.h b/proxy/http3/Http3HeaderFramer.h
index 2e70338..9559edb 100644
--- a/proxy/http3/Http3HeaderFramer.h
+++ b/proxy/http3/Http3HeaderFramer.h
@@ -39,7 +39,7 @@ public:
   Http3HeaderFramer(Http3Transaction *transaction, VIO *source, QPACK *qpack, uint64_t stream_id);
 
   // Http3FrameGenerator
-  Http3FrameUPtr generate_frame(uint16_t max_size) override;
+  Http3FrameUPtr generate_frame() override;
   bool is_done() const override;
 
 private:
diff --git a/proxy/http3/Http3HeaderVIOAdaptor.cc b/proxy/http3/Http3HeaderVIOAdaptor.cc
index 6a55ae7..0d246c5 100644
--- a/proxy/http3/Http3HeaderVIOAdaptor.cc
+++ b/proxy/http3/Http3HeaderVIOAdaptor.cc
@@ -25,10 +25,26 @@
 
 #include "I_VIO.h"
 #include "HTTP.h"
+#include "HTTP2.h"
 
-Http3HeaderVIOAdaptor::Http3HeaderVIOAdaptor(HTTPHdr *hdr, QPACK *qpack, Continuation *cont, uint64_t stream_id)
-  : _request_header(hdr), _qpack(qpack), _cont(cont), _stream_id(stream_id)
+// Constant strings for pseudo headers
+const char *HTTP3_VALUE_SCHEME    = ":scheme";
+const char *HTTP3_VALUE_AUTHORITY = ":authority";
+
+const unsigned HTTP3_LEN_SCHEME    = countof(":scheme") - 1;
+const unsigned HTTP3_LEN_AUTHORITY = countof(":authority") - 1;
+
+Http3HeaderVIOAdaptor::Http3HeaderVIOAdaptor(VIO *sink, HTTPType http_type, QPACK *qpack, uint64_t stream_id)
+  : _sink_vio(sink), _qpack(qpack), _stream_id(stream_id)
+{
+  SET_HANDLER(&Http3HeaderVIOAdaptor::event_handler);
+
+  this->_header.create(http_type);
+}
+
+Http3HeaderVIOAdaptor::~Http3HeaderVIOAdaptor()
 {
+  this->_header.destroy();
 }
 
 std::vector<Http3FrameType>
@@ -43,8 +59,7 @@ Http3HeaderVIOAdaptor::handle_frame(std::shared_ptr<const Http3Frame> frame)
   ink_assert(frame->type() == Http3FrameType::HEADERS);
   const Http3HeadersFrame *hframe = dynamic_cast<const Http3HeadersFrame *>(frame.get());
 
-  int res = this->_qpack->decode(this->_stream_id, hframe->header_block(), hframe->header_block_length(), *this->_request_header,
-                                 this->_cont);
+  int res = this->_qpack->decode(this->_stream_id, hframe->header_block(), hframe->header_block_length(), _header, this);
 
   if (res == 0) {
     // When decoding is not blocked, continuation should be called directly?
@@ -59,3 +74,102 @@ Http3HeaderVIOAdaptor::handle_frame(std::shared_ptr<const Http3Frame> frame)
 
   return Http3ErrorUPtr(new Http3NoError());
 }
+
+bool
+Http3HeaderVIOAdaptor::is_complete()
+{
+  return this->_is_complete;
+}
+
+int
+Http3HeaderVIOAdaptor::event_handler(int event, Event *data)
+{
+  switch (event) {
+  case QPACK_EVENT_DECODE_COMPLETE:
+    Debug("v_http3", "%s (%d)", "QPACK_EVENT_DECODE_COMPLETE", event);
+    if (this->_on_qpack_decode_complete()) {
+      // If READ_READY event is scheduled, should it be canceled?
+    }
+    break;
+  case QPACK_EVENT_DECODE_FAILED:
+    Debug("v_http3", "%s (%d)", "QPACK_EVENT_DECODE_FAILED", event);
+    // FIXME: handle error
+    break;
+  }
+
+  return EVENT_DONE;
+}
+
+int
+Http3HeaderVIOAdaptor::_on_qpack_decode_complete()
+{
+  ParseResult res = this->_convert_header_from_3_to_1_1(&this->_header);
+  if (res == PARSE_RESULT_ERROR) {
+    Debug("http3", "PARSE_RESULT_ERROR");
+    return -1;
+  }
+
+  // FIXME: response header might be delayed from first response body because of callback from QPACK
+  // Workaround fix for mixed response header and body
+  if (http_hdr_type_get(this->_header.m_http) == HTTP_TYPE_RESPONSE) {
+    return 0;
+  }
+
+  SCOPED_MUTEX_LOCK(lock, this->_sink_vio->mutex, this_ethread());
+  MIOBuffer *writer = this->_sink_vio->get_writer();
+
+  // TODO: Http2Stream::send_request has same logic. It originally comes from HttpSM::write_header_into_buffer.
+  // a). Make HttpSM::write_header_into_buffer static
+  //   or
+  // b). Add interface to HTTPHdr to dump data
+  //   or
+  // c). Add interface to HttpSM to handle HTTPHdr directly
+  int bufindex;
+  int dumpoffset = 0;
+  int done, tmp;
+  IOBufferBlock *block;
+  do {
+    bufindex = 0;
+    tmp      = dumpoffset;
+    block    = writer->get_current_block();
+    if (!block) {
+      writer->add_block();
+      block = writer->get_current_block();
+    }
+    done = this->_header.print(block->end(), block->write_avail(), &bufindex, &tmp);
+    dumpoffset += bufindex;
+    writer->fill(bufindex);
+    if (!done) {
+      writer->add_block();
+    }
+  } while (!done);
+
+  this->_is_complete = true;
+  return 1;
+}
+
+ParseResult
+Http3HeaderVIOAdaptor::_convert_header_from_3_to_1_1(HTTPHdr *hdrs)
+{
+  // TODO: do HTTP/3 specific convert, if there
+
+  if (http_hdr_type_get(hdrs->m_http) == HTTP_TYPE_REQUEST) {
+    // Dirty hack to bypass checks
+    MIMEField *field;
+    if ((field = hdrs->field_find(HTTP3_VALUE_SCHEME, HTTP3_LEN_SCHEME)) == nullptr) {
+      char value_s[]          = "https";
+      MIMEField *scheme_field = hdrs->field_create(HTTP3_VALUE_SCHEME, HTTP3_LEN_SCHEME);
+      scheme_field->value_set(hdrs->m_heap, hdrs->m_mime, value_s, sizeof(value_s) - 1);
+      hdrs->field_attach(scheme_field);
+    }
+
+    if ((field = hdrs->field_find(HTTP3_VALUE_AUTHORITY, HTTP3_LEN_AUTHORITY)) == nullptr) {
+      char value_a[]             = "localhost";
+      MIMEField *authority_field = hdrs->field_create(HTTP3_VALUE_AUTHORITY, HTTP3_LEN_AUTHORITY);
+      authority_field->value_set(hdrs->m_heap, hdrs->m_mime, value_a, sizeof(value_a) - 1);
+      hdrs->field_attach(authority_field);
+    }
+  }
+
+  return http2_convert_header_from_2_to_1_1(hdrs);
+}
diff --git a/proxy/http3/Http3HeaderVIOAdaptor.h b/proxy/http3/Http3HeaderVIOAdaptor.h
index f81456b..4f064a2 100644
--- a/proxy/http3/Http3HeaderVIOAdaptor.h
+++ b/proxy/http3/Http3HeaderVIOAdaptor.h
@@ -27,18 +27,27 @@
 
 #include "Http3FrameHandler.h"
 
-// TODO: rename, this is not VIOAdaptor anymore
-class Http3HeaderVIOAdaptor : public Http3FrameHandler
+class Http3HeaderVIOAdaptor : public Http3FrameHandler, public Continuation
 {
 public:
-  Http3HeaderVIOAdaptor(HTTPHdr *hdr, QPACK *qpack, Continuation *cont, uint64_t stream_id);
+  Http3HeaderVIOAdaptor(VIO *sink, HTTPType http_type, QPACK *qpack, uint64_t stream_id);
+  ~Http3HeaderVIOAdaptor();
+
   // Http3FrameHandler
   std::vector<Http3FrameType> interests() override;
   Http3ErrorUPtr handle_frame(std::shared_ptr<const Http3Frame> frame) override;
 
+  bool is_complete();
+  int event_handler(int event, Event *data);
+
 private:
-  HTTPHdr *_request_header = nullptr;
-  QPACK *_qpack            = nullptr;
-  Continuation *_cont      = nullptr;
-  uint64_t _stream_id      = 0;
+  VIO *_sink_vio      = nullptr;
+  QPACK *_qpack       = nullptr;
+  uint64_t _stream_id = 0;
+  bool _is_complete   = false;
+
+  HTTPHdr _header; ///< HTTP header buffer for decoding
+
+  int _on_qpack_decode_complete();
+  ParseResult _convert_header_from_3_to_1_1(HTTPHdr *hdr);
 };
diff --git a/proxy/http3/Http3Transaction.cc b/proxy/http3/Http3Transaction.cc
index 2e0b743..b8ffca7 100644
--- a/proxy/http3/Http3Transaction.cc
+++ b/proxy/http3/Http3Transaction.cc
@@ -32,7 +32,6 @@
 #include "Http3HeaderFramer.h"
 #include "Http3DataFramer.h"
 #include "HttpSM.h"
-#include "HTTP2.h"
 
 #define Http3TransDebug(fmt, ...)                                                                                            \
   Debug("http3_trans", "[%s] [%" PRIx32 "] " fmt,                                                                            \
@@ -57,27 +56,15 @@
 //
 // HQTransaction
 //
-HQTransaction::HQTransaction(HQSession *session, QUICStreamIO *stream_io) : super(session), _stream_io(stream_io)
+HQTransaction::HQTransaction(HQSession *session, QUICStreamVCAdapter::IOInfo &info) : super(session), _info(info)
 {
   this->mutex   = new_ProxyMutex();
   this->_thread = this_ethread();
 
   this->_reader = this->_read_vio_buf.alloc_reader();
-
-  HTTPType http_type = HTTP_TYPE_UNKNOWN;
-  if (this->direction() == NET_VCONNECTION_OUT) {
-    http_type = HTTP_TYPE_RESPONSE;
-  } else {
-    http_type = HTTP_TYPE_REQUEST;
-  }
-
-  this->_header.create(http_type);
 }
 
-HQTransaction::~HQTransaction()
-{
-  this->_header.destroy();
-}
+HQTransaction::~HQTransaction() {}
 
 void
 HQTransaction::set_active_timeout(ink_hrtime timeout_in)
@@ -194,14 +181,14 @@ HQTransaction::reenable(VIO *vio)
 {
   if (vio->op == VIO::READ) {
     int64_t len = this->_process_read_vio();
-    this->_stream_io->read_reenable();
+    this->_info.read_vio->reenable();
 
     if (len > 0) {
       this->_signal_read_event();
     }
   } else if (vio->op == VIO::WRITE) {
     int64_t len = this->_process_write_vio();
-    this->_stream_io->write_reenable();
+    this->_info.write_vio->reenable();
 
     if (len > 0) {
       this->_signal_write_event();
@@ -220,7 +207,7 @@ HQTransaction::transaction_done()
 int
 HQTransaction::get_transaction_id() const
 {
-  return this->_stream_io->stream_id();
+  return this->_info.adapter.stream().id();
 }
 
 void
@@ -306,17 +293,24 @@ HQTransaction::_signal_write_event()
 //
 // Http3Transaction
 //
-Http3Transaction::Http3Transaction(Http3Session *session, QUICStreamIO *stream_io) : super(session, stream_io)
+Http3Transaction::Http3Transaction(Http3Session *session, QUICStreamVCAdapter::IOInfo &info) : super(session, info)
 {
   static_cast<HQSession *>(this->_proxy_ssn)->add_transaction(static_cast<HQTransaction *>(this));
+  QUICStreamId stream_id = this->_info.adapter.stream().id();
 
-  this->_header_framer = new Http3HeaderFramer(this, &this->_write_vio, session->local_qpack(), stream_io->stream_id());
+  this->_header_framer = new Http3HeaderFramer(this, &this->_write_vio, session->local_qpack(), stream_id);
   this->_data_framer   = new Http3DataFramer(this, &this->_write_vio);
   this->_frame_collector.add_generator(this->_header_framer);
   this->_frame_collector.add_generator(this->_data_framer);
   // this->_frame_collector.add_generator(this->_push_controller);
 
-  this->_header_handler = new Http3HeaderVIOAdaptor(&this->_header, session->remote_qpack(), this, stream_io->stream_id());
+  HTTPType http_type = HTTP_TYPE_UNKNOWN;
+  if (this->direction() == NET_VCONNECTION_OUT) {
+    http_type = HTTP_TYPE_RESPONSE;
+  } else {
+    http_type = HTTP_TYPE_REQUEST;
+  }
+  this->_header_handler = new Http3HeaderVIOAdaptor(&this->_read_vio, http_type, session->remote_qpack(), stream_id);
   this->_data_handler   = new Http3StreamDataVIOAdaptor(&this->_read_vio);
 
   this->_frame_dispatcher.add_handler(this->_header_handler);
@@ -359,15 +353,20 @@ Http3Transaction::state_stream_open(int event, void *edata)
     if (this->_process_read_vio() > 0) {
       this->_signal_read_event();
     }
-    this->_stream_io->read_reenable();
+    this->_info.read_vio->reenable();
     break;
   case VC_EVENT_READ_COMPLETE:
+    if (!this->_header_handler->is_complete()) {
+      // Delay processing READ_COMPLETE
+      this_ethread()->schedule_imm(this, VC_EVENT_READ_COMPLETE);
+      break;
+    }
     Http3TransVDebug("%s (%d)", get_vc_event_name(event), event);
     this->_process_read_vio();
     this->_data_handler->finalize();
     // always signal regardless of progress
     this->_signal_read_event();
-    this->_stream_io->read_reenable();
+    this->_info.read_vio->reenable();
     break;
   case VC_EVENT_WRITE_READY:
     Http3TransVDebug("%s (%d)", get_vc_event_name(event), event);
@@ -375,14 +374,14 @@ Http3Transaction::state_stream_open(int event, void *edata)
     if (this->_process_write_vio() > 0) {
       this->_signal_write_event();
     }
-    this->_stream_io->write_reenable();
+    this->_info.write_vio->reenable();
     break;
   case VC_EVENT_WRITE_COMPLETE:
     Http3TransVDebug("%s (%d)", get_vc_event_name(event), event);
     this->_process_write_vio();
     // always signal regardless of progress
     this->_signal_write_event();
-    this->_stream_io->write_reenable();
+    this->_info.write_vio->reenable();
     break;
   case VC_EVENT_EOS:
   case VC_EVENT_ERROR:
@@ -391,20 +390,6 @@ Http3Transaction::state_stream_open(int event, void *edata)
     Http3TransVDebug("%s (%d)", get_vc_event_name(event), event);
     break;
   }
-  case QPACK_EVENT_DECODE_COMPLETE: {
-    Http3TransVDebug("%s (%d)", "QPACK_EVENT_DECODE_COMPLETE", event);
-    int res = this->_on_qpack_decode_complete();
-    if (res) {
-      // If READ_READY event is scheduled, should it be canceled?
-      this->_signal_read_event();
-    }
-    break;
-  }
-  case QPACK_EVENT_DECODE_FAILED: {
-    Http3TransVDebug("%s (%d)", "QPACK_EVENT_DECODE_FAILED", event);
-    // FIXME: handle error
-    break;
-  }
   default:
     Http3TransDebug("Unknown event %d", event);
   }
@@ -481,7 +466,7 @@ Http3Transaction::_process_read_vio()
   SCOPED_MUTEX_LOCK(lock, this->_read_vio.mutex, this_ethread());
 
   uint64_t nread = 0;
-  this->_frame_dispatcher.on_read_ready(*this->_stream_io, nread);
+  this->_frame_dispatcher.on_read_ready(this->_info.adapter.stream().id(), *this->_info.read_vio->get_reader(), nread);
   return nread;
 }
 
@@ -504,89 +489,15 @@ Http3Transaction::_process_write_vio()
   SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
 
   size_t nwritten = 0;
-  this->_frame_collector.on_write_ready(this->_stream_io, nwritten);
-
-  return nwritten;
-}
-
-// Constant strings for pseudo headers
-const char *HTTP3_VALUE_SCHEME    = ":scheme";
-const char *HTTP3_VALUE_AUTHORITY = ":authority";
-
-const unsigned HTTP3_LEN_SCHEME    = countof(":scheme") - 1;
-const unsigned HTTP3_LEN_AUTHORITY = countof(":authority") - 1;
-
-ParseResult
-Http3Transaction::_convert_header_from_3_to_1_1(HTTPHdr *hdrs)
-{
-  // TODO: do HTTP/3 specific convert, if there
-
-  if (http_hdr_type_get(hdrs->m_http) == HTTP_TYPE_REQUEST) {
-    // Dirty hack to bypass checks
-    MIMEField *field;
-    if ((field = hdrs->field_find(HTTP3_VALUE_SCHEME, HTTP3_LEN_SCHEME)) == nullptr) {
-      char value_s[]          = "https";
-      MIMEField *scheme_field = hdrs->field_create(HTTP3_VALUE_SCHEME, HTTP3_LEN_SCHEME);
-      scheme_field->value_set(hdrs->m_heap, hdrs->m_mime, value_s, sizeof(value_s) - 1);
-      hdrs->field_attach(scheme_field);
-    }
-
-    if ((field = hdrs->field_find(HTTP3_VALUE_AUTHORITY, HTTP3_LEN_AUTHORITY)) == nullptr) {
-      char value_a[]             = "localhost";
-      MIMEField *authority_field = hdrs->field_create(HTTP3_VALUE_AUTHORITY, HTTP3_LEN_AUTHORITY);
-      authority_field->value_set(hdrs->m_heap, hdrs->m_mime, value_a, sizeof(value_a) - 1);
-      hdrs->field_attach(authority_field);
-    }
+  bool all_done   = false;
+  this->_frame_collector.on_write_ready(this->_info.adapter.stream().id(), *this->_info.write_vio->get_writer(), nwritten,
+                                        all_done);
+  this->_info.write_vio->nbytes += nwritten;
+  if (all_done) {
+    this->_info.write_vio->done();
   }
 
-  return http2_convert_header_from_2_to_1_1(hdrs);
-}
-
-int
-Http3Transaction::_on_qpack_decode_complete()
-{
-  ParseResult res = this->_convert_header_from_3_to_1_1(&this->_header);
-  if (res == PARSE_RESULT_ERROR) {
-    Http3TransDebug("PARSE_RESULT_ERROR");
-    return -1;
-  }
-
-  // FIXME: response header might be delayed from first response body because of callback from QPACK
-  // Workaround fix for mixed response header and body
-  if (http_hdr_type_get(this->_header.m_http) == HTTP_TYPE_RESPONSE) {
-    return 0;
-  }
-
-  SCOPED_MUTEX_LOCK(lock, this->_read_vio.mutex, this_ethread());
-  MIOBuffer *writer = this->_read_vio.get_writer();
-
-  // TODO: Http2Stream::send_request has same logic. It originally comes from HttpSM::write_header_into_buffer.
-  // a). Make HttpSM::write_header_into_buffer static
-  //   or
-  // b). Add interface to HTTPHdr to dump data
-  //   or
-  // c). Add interface to HttpSM to handle HTTPHdr directly
-  int bufindex;
-  int dumpoffset = 0;
-  int done, tmp;
-  IOBufferBlock *block;
-  do {
-    bufindex = 0;
-    tmp      = dumpoffset;
-    block    = writer->get_current_block();
-    if (!block) {
-      writer->add_block();
-      block = writer->get_current_block();
-    }
-    done = this->_header.print(block->end(), block->write_avail(), &bufindex, &tmp);
-    dumpoffset += bufindex;
-    writer->fill(bufindex);
-    if (!done) {
-      writer->add_block();
-    }
-  } while (!done);
-
-  return 1;
+  return nwritten;
 }
 
 // TODO:  Just a place holder for now
@@ -599,7 +510,7 @@ Http3Transaction::has_request_body(int64_t content_length, bool is_chunked_set)
 //
 // Http09Transaction
 //
-Http09Transaction::Http09Transaction(Http09Session *session, QUICStreamIO *stream_io) : super(session, stream_io)
+Http09Transaction::Http09Transaction(Http09Session *session, QUICStreamVCAdapter::IOInfo &info) : super(session, info)
 {
   static_cast<HQSession *>(this->_proxy_ssn)->add_transaction(static_cast<HQTransaction *>(this));
 
@@ -637,7 +548,7 @@ Http09Transaction::state_stream_open(int event, void *edata)
     if (len > 0) {
       this->_signal_read_event();
     }
-    this->_stream_io->read_reenable();
+    this->_info.read_vio->reenable();
 
     break;
   }
@@ -647,7 +558,7 @@ Http09Transaction::state_stream_open(int event, void *edata)
     if (len > 0) {
       this->_signal_write_event();
     }
-    this->_stream_io->write_reenable();
+    this->_info.write_vio->reenable();
 
     break;
   }
@@ -720,13 +631,15 @@ Http09Transaction::_process_read_vio()
   }
 
   SCOPED_MUTEX_LOCK(lock, this->_read_vio.mutex, this_ethread());
+  IOBufferReader *reader = this->_info.read_vio->get_reader();
 
   // Nuke this block when we drop 0.9 support
   if (!this->_protocol_detected) {
     uint8_t start[3];
-    if (this->_stream_io->peek(start, 3) < 3) {
+    if (!reader->is_read_avail_more_than(3)) {
       return 0;
     }
+    reader->memcpy(start, 3);
     // If the first two bit are 0 and 1, the 3rd byte is type field.
     // Because there is no type value larger than 0x20, we can assume that the
     // request is HTTP/0.9 if the value is larger than 0x20.
@@ -739,16 +652,14 @@ Http09Transaction::_process_read_vio()
   if (this->_legacy_request) {
     uint64_t nread    = 0;
     MIOBuffer *writer = this->_read_vio.get_writer();
-
     // Nuke this branch when we drop 0.9 support
     if (!this->_client_req_header_complete) {
       uint8_t buf[4096];
-      int len = this->_stream_io->peek(buf, 4096);
+      int len = reader->read(buf, 4096);
       // Check client request is complete or not
       if (len < 2 || buf[len - 1] != '\n') {
         return 0;
       }
-      this->_stream_io->consume(len);
       nread += len;
       this->_client_req_header_complete = true;
 
@@ -765,7 +676,7 @@ Http09Transaction::_process_read_vio()
     } else {
       uint8_t buf[4096];
       int len;
-      while ((len = this->_stream_io->read(buf, 4096)) > 0) {
+      while ((len = reader->read(buf, 4096)) > 0) {
         nread += len;
         writer->write(buf, len);
       }
@@ -779,7 +690,7 @@ Http09Transaction::_process_read_vio()
     int len;
     uint64_t nread = 0;
 
-    while ((len = this->_stream_io->read(buf, 4096)) > 0) {
+    while ((len = reader->read(buf, 4096)) > 0) {
       nread += len;
     }
 
@@ -810,6 +721,9 @@ Http09Transaction::_process_write_vio()
   SCOPED_MUTEX_LOCK(lock, this->_write_vio.mutex, this_ethread());
 
   IOBufferReader *reader = this->_write_vio.get_reader();
+  if (!reader) {
+    return 0;
+  }
 
   if (this->_legacy_request) {
     // This branch is for HTTP/0.9
@@ -830,7 +744,7 @@ Http09Transaction::_process_write_vio()
 
     while (total_written < bytes_avail) {
       int64_t data_len      = reader->block_read_avail();
-      int64_t bytes_written = this->_stream_io->write(reader, data_len);
+      int64_t bytes_written = this->_info.write_vio->get_writer()->write(reader, data_len);
       if (bytes_written <= 0) {
         break;
       }
@@ -844,7 +758,7 @@ Http09Transaction::_process_write_vio()
     // is CHUNK_READ_DONE and set FIN flag
     if (this->_write_vio.ntodo() == 0) {
       // The size of respons to client
-      this->_stream_io->write_done();
+      this->_info.write_vio->done();
     }
 
     return total_written;
diff --git a/proxy/http3/Http3Transaction.h b/proxy/http3/Http3Transaction.h
index 8dccccf..b1ddbb6 100644
--- a/proxy/http3/Http3Transaction.h
+++ b/proxy/http3/Http3Transaction.h
@@ -25,6 +25,7 @@
 
 #include "I_VConnection.h"
 #include "ProxyTransaction.h"
+#include "quic/QUICStreamVCAdapter.h"
 #include "Http3FrameDispatcher.h"
 #include "Http3FrameCollector.h"
 
@@ -34,6 +35,7 @@ class Http09Session;
 class Http3Session;
 class Http3HeaderFramer;
 class Http3DataFramer;
+class Http3HeaderVIOAdaptor;
 class Http3StreamDataVIOAdaptor;
 
 class HQTransaction : public ProxyTransaction
@@ -41,7 +43,7 @@ class HQTransaction : public ProxyTransaction
 public:
   using super = ProxyTransaction;
 
-  HQTransaction(HQSession *session, QUICStreamIO *stream_io);
+  HQTransaction(HQSession *session, QUICStreamVCAdapter::IOInfo &info);
   virtual ~HQTransaction();
 
   // Implement ProxyClienTransaction interface
@@ -77,15 +79,13 @@ protected:
   EThread *_thread           = nullptr;
   Event *_cross_thread_event = nullptr;
 
-  MIOBuffer _read_vio_buf  = CLIENT_CONNECTION_FIRST_READ_BUFFER_SIZE_INDEX;
-  QUICStreamIO *_stream_io = nullptr;
+  MIOBuffer _read_vio_buf = CLIENT_CONNECTION_FIRST_READ_BUFFER_SIZE_INDEX;
+  QUICStreamVCAdapter::IOInfo &_info;
 
   VIO _read_vio;
   VIO _write_vio;
   Event *_read_event  = nullptr;
   Event *_write_event = nullptr;
-
-  HTTPHdr _header; ///< HTTP header buffer for decoding
 };
 
 class Http3Transaction : public HQTransaction
@@ -93,7 +93,7 @@ class Http3Transaction : public HQTransaction
 public:
   using super = HQTransaction;
 
-  Http3Transaction(Http3Session *session, QUICStreamIO *stream_io);
+  Http3Transaction(Http3Session *session, QUICStreamVCAdapter::IOInfo &info);
   ~Http3Transaction();
 
   int state_stream_open(int event, void *data) override;
@@ -111,15 +111,12 @@ private:
   int64_t _process_read_vio() override;
   int64_t _process_write_vio() override;
 
-  ParseResult _convert_header_from_3_to_1_1(HTTPHdr *hdr);
-  int _on_qpack_decode_complete();
-
   // These are for HTTP/3
   Http3FrameDispatcher _frame_dispatcher;
   Http3FrameCollector _frame_collector;
   Http3FrameGenerator *_header_framer      = nullptr;
   Http3FrameGenerator *_data_framer        = nullptr;
-  Http3FrameHandler *_header_handler       = nullptr;
+  Http3HeaderVIOAdaptor *_header_handler   = nullptr;
   Http3StreamDataVIOAdaptor *_data_handler = nullptr;
 };
 
@@ -131,7 +128,7 @@ class Http09Transaction : public HQTransaction
 public:
   using super = HQTransaction;
 
-  Http09Transaction(Http09Session *session, QUICStreamIO *stream_io);
+  Http09Transaction(Http09Session *session, QUICStreamVCAdapter::IOInfo &info);
   ~Http09Transaction();
 
   int state_stream_open(int event, void *data) override;
diff --git a/proxy/http3/QPACK.cc b/proxy/http3/QPACK.cc
index eaaa1a6..ca937f7 100644
--- a/proxy/http3/QPACK.cc
+++ b/proxy/http3/QPACK.cc
@@ -149,22 +149,47 @@ QPACK::QPACK(QUICConnection *qc, uint32_t max_header_list_size, uint16_t max_tab
 
 QPACK::~QPACK() {}
 
+void
+QPACK::on_new_stream(QUICStream &stream)
+{
+  auto *info = new QUICStreamVCAdapter::IOInfo(stream);
+
+  switch (stream.direction()) {
+  case QUICStreamDirection::BIDIRECTIONAL:
+    // ink_assert(!"QPACK does not use bidirectional streams");
+    // QPACK offline interop uses stream 0 as a encoder stream.
+    info->setup_write_vio(this);
+    info->setup_read_vio(this);
+    break;
+  case QUICStreamDirection::SEND:
+    info->setup_write_vio(this);
+    break;
+  case QUICStreamDirection::RECEIVE:
+    info->setup_read_vio(this);
+    break;
+  default:
+    ink_assert(false);
+    break;
+  }
+
+  stream.set_io_adapter(&info->adapter);
+}
+
 int
 QPACK::event_handler(int event, Event *data)
 {
-  VIO *vio                = reinterpret_cast<VIO *>(data);
-  QUICStreamIO *stream_io = this->_find_stream_io(vio);
+  VIO *vio = reinterpret_cast<VIO *>(data);
   int ret;
 
   switch (event) {
   case VC_EVENT_READ_READY:
-    ret = this->_on_read_ready(*stream_io);
+    ret = this->_on_read_ready(vio);
     break;
   case VC_EVENT_READ_COMPLETE:
     ret = EVENT_DONE;
     break;
   case VC_EVENT_WRITE_READY:
-    ret = this->_on_write_ready(*stream_io);
+    ret = this->_on_write_ready(vio);
     break;
   case VC_EVENT_WRITE_COMPLETE:
     ret = EVENT_DONE;
@@ -254,17 +279,15 @@ QPACK::decode(uint64_t stream_id, const uint8_t *header_block, size_t header_blo
 }
 
 void
-QPACK::set_encoder_stream(QUICStreamIO *stream_io)
+QPACK::set_encoder_stream(QUICStreamId id)
 {
-  this->_encoder_stream_id = stream_io->stream_id();
-  this->set_stream(stream_io);
+  this->_encoder_stream_id = id;
 }
 
 void
-QPACK::set_decoder_stream(QUICStreamIO *stream_io)
+QPACK::set_decoder_stream(QUICStreamId id)
 {
-  this->_decoder_stream_id = stream_io->stream_id();
-  this->set_stream(stream_io);
+  this->_decoder_stream_id = id;
 }
 
 void
@@ -1008,27 +1031,32 @@ QPACK::_abort_decode()
 }
 
 int
-QPACK::_on_read_ready(QUICStreamIO &stream_io)
+QPACK::_on_read_ready(VIO *vio)
 {
-  QUICStreamId stream_id = stream_io.stream_id();
+  int nread              = 0;
+  QUICStreamId stream_id = static_cast<QUICStreamVCAdapter *>(vio->vc_server)->stream().id();
+
   if (stream_id == this->_decoder_stream_id) {
-    return this->_on_decoder_stream_read_ready(stream_io);
+    nread = this->_on_decoder_stream_read_ready(*vio->get_reader());
   } else if (stream_id == this->_encoder_stream_id) {
-    return this->_on_encoder_stream_read_ready(stream_io);
+    nread = this->_on_encoder_stream_read_ready(*vio->get_reader());
   } else {
     ink_assert(!"The stream ID must match either encoder stream id or decoder stream id");
-    return EVENT_DONE;
   }
+
+  vio->ndone += nread;
+  return EVENT_DONE;
 }
 
 int
-QPACK::_on_write_ready(QUICStreamIO &stream_io)
+QPACK::_on_write_ready(VIO *vio)
 {
-  QUICStreamId stream_id = stream_io.stream_id();
+  QUICStreamId stream_id = static_cast<QUICStreamVCAdapter *>(vio->vc_server)->stream().id();
+
   if (stream_id == this->_decoder_stream_id) {
-    return this->_on_decoder_write_ready(stream_io);
+    return this->_on_decoder_write_ready(*vio->get_writer());
   } else if (stream_id == this->_encoder_stream_id) {
-    return this->_on_encoder_write_ready(stream_io);
+    return this->_on_encoder_write_ready(*vio->get_writer());
   } else {
     ink_assert(!"The stream ID must match either decoder stream id or decoder stream id");
     return EVENT_DONE;
@@ -1036,13 +1064,14 @@ QPACK::_on_write_ready(QUICStreamIO &stream_io)
 }
 
 int
-QPACK::_on_decoder_stream_read_ready(QUICStreamIO &stream_io)
+QPACK::_on_decoder_stream_read_ready(IOBufferReader &reader)
 {
-  uint8_t buf;
-  if (stream_io.peek(&buf, 1) > 0) {
+  if (reader.is_read_avail_more_than(0)) {
+    uint8_t buf;
+    reader.memcpy(&buf, 1);
     if (buf & 0x80) { // Header Acknowledgement
       uint64_t stream_id;
-      if (this->_read_header_acknowledgement(stream_io, stream_id) >= 0) {
+      if (this->_read_header_acknowledgement(reader, stream_id) >= 0) {
         QPACKDebug("Received Header Acknowledgement: stream_id=%" PRIu64, stream_id);
         this->_update_largest_known_received_index_by_stream_id(stream_id);
         this->_update_reference_counts(stream_id);
@@ -1050,14 +1079,14 @@ QPACK::_on_decoder_stream_read_ready(QUICStreamIO &stream_io)
       }
     } else if (buf & 0x40) { // Stream Cancellation
       uint64_t stream_id;
-      if (this->_read_stream_cancellation(stream_io, stream_id) >= 0) {
+      if (this->_read_stream_cancellation(reader, stream_id) >= 0) {
         QPACKDebug("Received Stream Cancellation: stream_id=%" PRIu64, stream_id);
         this->_update_reference_counts(stream_id);
         this->_references.erase(stream_id);
       }
     } else { // Table State Synchronize
       uint16_t insert_count;
-      if (this->_read_table_state_synchronize(stream_io, insert_count) >= 0) {
+      if (this->_read_table_state_synchronize(reader, insert_count) >= 0) {
         QPACKDebug("Received Table State Synchronize: inserted_count=%d", insert_count);
         this->_update_largest_known_received_index_by_insert_count(insert_count);
       }
@@ -1068,18 +1097,18 @@ QPACK::_on_decoder_stream_read_ready(QUICStreamIO &stream_io)
 }
 
 int
-QPACK::_on_encoder_stream_read_ready(QUICStreamIO &stream_io)
+QPACK::_on_encoder_stream_read_ready(IOBufferReader &reader)
 {
-  uint8_t buf;
-
-  while (stream_io.peek(&buf, 1) > 0) {
+  while (reader.is_read_avail_more_than(0)) {
+    uint8_t buf;
+    reader.memcpy(&buf, 1);
     if (buf & 0x80) { // Insert With Name Reference
       bool is_static;
       uint16_t index;
       Arena arena;
       char *value;
       uint16_t value_len;
-      if (this->_read_insert_with_name_ref(stream_io, is_static, index, arena, &value, value_len) < 0) {
+      if (this->_read_insert_with_name_ref(reader, is_static, index, arena, &value, value_len) < 0) {
         this->_abort_decode();
         return EVENT_DONE;
       }
@@ -1091,7 +1120,7 @@ QPACK::_on_encoder_stream_read_ready(QUICStreamIO &stream_io)
       uint16_t name_len;
       char *value;
       uint16_t value_len;
-      if (this->_read_insert_without_name_ref(stream_io, arena, &name, name_len, &value, value_len) < 0) {
+      if (this->_read_insert_without_name_ref(reader, arena, &name, name_len, &value, value_len) < 0) {
         this->_abort_decode();
         return EVENT_DONE;
       }
@@ -1099,7 +1128,7 @@ QPACK::_on_encoder_stream_read_ready(QUICStreamIO &stream_io)
       this->_dynamic_table.insert_entry(name, name_len, value, value_len);
     } else if (buf & 0x20) { // Dynamic Table Size Update
       uint16_t max_size;
-      if (this->_read_dynamic_table_size_update(stream_io, max_size) < 0) {
+      if (this->_read_dynamic_table_size_update(reader, max_size) < 0) {
         this->_abort_decode();
         return EVENT_DONE;
       }
@@ -1107,7 +1136,7 @@ QPACK::_on_encoder_stream_read_ready(QUICStreamIO &stream_io)
       this->_dynamic_table.update_size(max_size);
     } else { // Duplicates
       uint16_t index;
-      if (this->_read_duplicate(stream_io, index) < 0) {
+      if (this->_read_duplicate(reader, index) < 0) {
         this->_abort_decode();
         return EVENT_DONE;
       }
@@ -1122,17 +1151,17 @@ QPACK::_on_encoder_stream_read_ready(QUICStreamIO &stream_io)
 }
 
 int
-QPACK::_on_decoder_write_ready(QUICStreamIO &stream_io)
+QPACK::_on_decoder_write_ready(MIOBuffer &writer)
 {
-  int64_t written_len = stream_io.write(this->_decoder_stream_sending_instructions_reader, INT64_MAX);
+  int64_t written_len = writer.write(this->_decoder_stream_sending_instructions_reader, INT64_MAX);
   this->_decoder_stream_sending_instructions_reader->consume(written_len);
   return written_len;
 }
 
 int
-QPACK::_on_encoder_write_ready(QUICStreamIO &stream_io)
+QPACK::_on_encoder_write_ready(MIOBuffer &writer)
 {
-  int64_t written_len = stream_io.write(this->_encoder_stream_sending_instructions_reader, INT64_MAX);
+  int64_t written_len = writer.write(this->_encoder_stream_sending_instructions_reader, INT64_MAX);
   this->_encoder_stream_sending_instructions_reader->consume(written_len);
   return written_len;
 }
@@ -1611,14 +1640,14 @@ QPACK::_write_stream_cancellation(uint64_t stream_id)
 }
 
 int
-QPACK::_read_insert_with_name_ref(QUICStreamIO &stream_io, bool &is_static, uint16_t &index, Arena &arena, char **value,
+QPACK::_read_insert_with_name_ref(IOBufferReader &reader, bool &is_static, uint16_t &index, Arena &arena, char **value,
                                   uint16_t &value_len)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16384];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
 
   // S flag
   is_static = input[0] & 0x40;
@@ -1638,20 +1667,20 @@ QPACK::_read_insert_with_name_ref(QUICStreamIO &stream_io, bool &is_static, uint
   value_len = tmp;
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
 
 int
-QPACK::_read_insert_without_name_ref(QUICStreamIO &stream_io, Arena &arena, char **name, uint16_t &name_len, char **value,
+QPACK::_read_insert_without_name_ref(IOBufferReader &reader, Arena &arena, char **name, uint16_t &name_len, char **value,
                                      uint16_t &value_len)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16384];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
 
   // Name
   uint64_t tmp;
@@ -1668,19 +1697,19 @@ QPACK::_read_insert_without_name_ref(QUICStreamIO &stream_io, Arena &arena, char
   value_len = tmp;
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
 
 int
-QPACK::_read_duplicate(QUICStreamIO &stream_io, uint16_t &index)
+QPACK::_read_duplicate(IOBufferReader &reader, uint16_t &index)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
 
   // Index
   uint64_t tmp;
@@ -1690,19 +1719,19 @@ QPACK::_read_duplicate(QUICStreamIO &stream_io, uint16_t &index)
   index = tmp;
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
 
 int
-QPACK::_read_dynamic_table_size_update(QUICStreamIO &stream_io, uint16_t &max_size)
+QPACK::_read_dynamic_table_size_update(IOBufferReader &reader, uint16_t &max_size)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
   uint64_t tmp;
 
   // Max Size
@@ -1712,19 +1741,19 @@ QPACK::_read_dynamic_table_size_update(QUICStreamIO &stream_io, uint16_t &max_si
   max_size = tmp;
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
 
 int
-QPACK::_read_table_state_synchronize(QUICStreamIO &stream_io, uint16_t &insert_count)
+QPACK::_read_table_state_synchronize(IOBufferReader &reader, uint16_t &insert_count)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
   uint64_t tmp;
 
   // Insert Count
@@ -1734,19 +1763,19 @@ QPACK::_read_table_state_synchronize(QUICStreamIO &stream_io, uint16_t &insert_c
   insert_count = tmp;
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
 
 int
-QPACK::_read_header_acknowledgement(QUICStreamIO &stream_io, uint64_t &stream_id)
+QPACK::_read_header_acknowledgement(IOBufferReader &reader, uint64_t &stream_id)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
 
   // Stream ID
   // FIXME xpack_decode_integer does not support uint64_t
@@ -1755,19 +1784,19 @@ QPACK::_read_header_acknowledgement(QUICStreamIO &stream_io, uint64_t &stream_id
   }
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
 
 int
-QPACK::_read_stream_cancellation(QUICStreamIO &stream_io, uint64_t &stream_id)
+QPACK::_read_stream_cancellation(IOBufferReader &reader, uint64_t &stream_id)
 {
   size_t read_len = 0;
   int ret;
   uint8_t input[16];
-  int input_len;
-  input_len = stream_io.peek(input, sizeof(input));
+  uint8_t *p    = reinterpret_cast<uint8_t *>(reader.memcpy(input, sizeof(input)));
+  int input_len = p - input;
 
   // Stream ID
   // FIXME xpack_decode_integer does not support uint64_t
@@ -1776,7 +1805,7 @@ QPACK::_read_stream_cancellation(QUICStreamIO &stream_io, uint64_t &stream_id)
   }
   read_len += ret;
 
-  stream_io.consume(read_len);
+  reader.consume(read_len);
 
   return 0;
 }
diff --git a/proxy/http3/QPACK.h b/proxy/http3/QPACK.h
index e81b4ab..4324450 100644
--- a/proxy/http3/QPACK.h
+++ b/proxy/http3/QPACK.h
@@ -33,6 +33,7 @@
 #include "MIME.h"
 #include "HTTP.h"
 #include "QUICApplication.h"
+#include "QUICStreamVCAdapter.h"
 #include "QUICConnection.h"
 
 class HTTPHdr;
@@ -48,6 +49,8 @@ public:
   QPACK(QUICConnection *qc, uint32_t max_header_list_size, uint16_t max_table_size, uint16_t max_blocking_streams);
   virtual ~QPACK();
 
+  void on_new_stream(QUICStream &stream) override;
+
   int event_handler(int event, Event *data);
 
   /*
@@ -66,8 +69,8 @@ public:
 
   int cancel(uint64_t stream_id);
 
-  void set_encoder_stream(QUICStreamIO *stream_io);
-  void set_decoder_stream(QUICStreamIO *stream_io);
+  void set_encoder_stream(QUICStreamId id);
+  void set_decoder_stream(QUICStreamId id);
 
   void update_max_header_list_size(uint32_t max_header_list_size);
   void update_max_table_size(uint16_t max_table_size);
@@ -266,21 +269,21 @@ private:
   void _update_reference_counts(uint64_t stream_id);
 
   // Encoder Stream
-  int _read_insert_with_name_ref(QUICStreamIO &stream_io, bool &is_static, uint16_t &index, Arena &arena, char **value,
+  int _read_insert_with_name_ref(IOBufferReader &reader, bool &is_static, uint16_t &index, Arena &arena, char **value,
                                  uint16_t &value_len);
-  int _read_insert_without_name_ref(QUICStreamIO &stream_io, Arena &arena, char **name, uint16_t &name_len, char **value,
+  int _read_insert_without_name_ref(IOBufferReader &reader, Arena &arena, char **name, uint16_t &name_len, char **value,
                                     uint16_t &value_len);
-  int _read_duplicate(QUICStreamIO &stream_io, uint16_t &index);
-  int _read_dynamic_table_size_update(QUICStreamIO &stream_io, uint16_t &max_size);
+  int _read_duplicate(IOBufferReader &reader, uint16_t &index);
+  int _read_dynamic_table_size_update(IOBufferReader &reader, uint16_t &max_size);
   int _write_insert_with_name_ref(uint16_t index, bool dynamic, const char *value, uint16_t value_len);
   int _write_insert_without_name_ref(const char *name, int name_len, const char *value, uint16_t value_len);
   int _write_duplicate(uint16_t index);
   int _write_dynamic_table_size_update(uint16_t max_size);
 
   // Decoder Stream
-  int _read_table_state_synchronize(QUICStreamIO &stream_io, uint16_t &insert_count);
-  int _read_header_acknowledgement(QUICStreamIO &stream_io, uint64_t &stream_id);
-  int _read_stream_cancellation(QUICStreamIO &stream_io, uint64_t &stream_id);
+  int _read_table_state_synchronize(IOBufferReader &reader, uint16_t &insert_count);
+  int _read_header_acknowledgement(IOBufferReader &reader, uint64_t &stream_id);
+  int _read_stream_cancellation(IOBufferReader &reader, uint64_t &stream_id);
   int _write_table_state_synchronize(uint16_t insert_count);
   int _write_header_acknowledgement(uint64_t stream_id);
   int _write_stream_cancellation(uint64_t stream_id);
@@ -317,13 +320,13 @@ private:
   uint16_t _calc_postbase_index_from_absolute_index(uint16_t base_index, uint16_t absolute_index);
   void _attach_header(HTTPHdr &hdr, const char *name, int name_len, const char *value, int value_len, bool never_index);
 
-  int _on_read_ready(QUICStreamIO &stream_io);
-  int _on_decoder_stream_read_ready(QUICStreamIO &stream_io);
-  int _on_encoder_stream_read_ready(QUICStreamIO &stream_io);
+  int _on_read_ready(VIO *vio);
+  int _on_decoder_stream_read_ready(IOBufferReader &reader);
+  int _on_encoder_stream_read_ready(IOBufferReader &reader);
 
-  int _on_write_ready(QUICStreamIO &stream_io);
-  int _on_decoder_write_ready(QUICStreamIO &stream_io);
-  int _on_encoder_write_ready(QUICStreamIO &stream_io);
+  int _on_write_ready(VIO *vio);
+  int _on_decoder_write_ready(MIOBuffer &writer);
+  int _on_encoder_write_ready(MIOBuffer &writer);
 
   // Stream numbers
   // FIXME How are these stream ids negotiated? In interop, encoder stream id have to be 0 and decoder stream id must not be used.
diff --git a/proxy/http3/test/main.cc b/proxy/http3/test/main.cc
index 247e118..b0d4fa1 100644
--- a/proxy/http3/test/main.cc
+++ b/proxy/http3/test/main.cc
@@ -32,6 +32,8 @@
 #include "RecordsConfig.h"
 #include "Http3Config.h"
 
+#define TEST_THREADS 1
+
 struct EventProcessorListener : Catch::TestEventListenerBase {
   using TestEventListenerBase::TestEventListenerBase; // inherit constructor
 
@@ -48,6 +50,12 @@ struct EventProcessorListener : Catch::TestEventListenerBase {
     RecProcessInit(RECM_STAND_ALONE);
     LibRecordsConfigInit();
 
+    ink_event_system_init(EVENT_SYSTEM_MODULE_PUBLIC_VERSION);
+    eventProcessor.start(TEST_THREADS);
+
+    Thread *main_thread = new EThread;
+    main_thread->set_specific();
+
     Http3Config::startup();
   }
 };
diff --git a/proxy/http3/test/test_Http3Frame.cc b/proxy/http3/test/test_Http3Frame.cc
index 9aa4efd..68b93b5 100644
--- a/proxy/http3/test/test_Http3Frame.cc
+++ b/proxy/http3/test/test_Http3Frame.cc
@@ -90,7 +90,11 @@ TEST_CASE("Store DATA Frame", "[http3]")
     Http3DataFrame data_frame(std::move(payload1), 4);
     CHECK(data_frame.length() == 4);
 
-    data_frame.store(buf, &len);
+    auto ibb = data_frame.to_io_buffer_block();
+    IOBufferReader reader;
+    reader.block = ibb.get();
+    len          = reader.read_avail();
+    reader.read(buf, sizeof(buf));
     CHECK(len == 6);
     CHECK(memcmp(buf, expected1, len) == 0);
   }
@@ -115,7 +119,11 @@ TEST_CASE("Store HEADERS Frame", "[http3]")
     Http3HeadersFrame hdrs_frame(std::move(header_block), 4);
     CHECK(hdrs_frame.length() == 4);
 
-    hdrs_frame.store(buf, &len);
+    auto ibb = hdrs_frame.to_io_buffer_block();
+    IOBufferReader reader;
+    reader.block = ibb.get();
+    len          = reader.read_avail();
+    reader.read(buf, sizeof(buf));
     CHECK(len == 6);
     CHECK(memcmp(buf, expected1, len) == 0);
   }
@@ -169,7 +177,11 @@ TEST_CASE("Store SETTINGS Frame", "[http3]")
 
     uint8_t buf[32] = {0};
     size_t len;
-    settings_frame.store(buf, &len);
+    auto ibb = settings_frame.to_io_buffer_block();
+    IOBufferReader reader;
+    reader.block = ibb.get();
+    len          = reader.read_avail();
+    reader.read(buf, sizeof(buf));
     CHECK(len == sizeof(expected));
     CHECK(memcmp(buf, expected, len) == 0);
   }
@@ -190,7 +202,11 @@ TEST_CASE("Store SETTINGS Frame", "[http3]")
 
     uint8_t buf[32] = {0};
     size_t len;
-    settings_frame.store(buf, &len);
+    auto ibb = settings_frame.to_io_buffer_block();
+    IOBufferReader reader;
+    reader.block = ibb.get();
+    len          = reader.read_avail();
+    reader.read(buf, sizeof(buf));
     CHECK(len == sizeof(expected));
     CHECK(memcmp(buf, expected, len) == 0);
   }
diff --git a/proxy/http3/test/test_QPACK.cc b/proxy/http3/test/test_QPACK.cc
index 167ab7f..3faee2a 100644
--- a/proxy/http3/test/test_QPACK.cc
+++ b/proxy/http3/test/test_QPACK.cc
@@ -76,16 +76,18 @@ public:
   void
   write(const uint8_t *buf, size_t buf_len, QUICOffset offset, bool last)
   {
-    this->_write_to_read_vio(offset, buf, buf_len, last);
-    this->_signal_read_event();
+    this->_adapter->write(offset, buf, buf_len, last);
+    this->_adapter->encourge_read();
   }
 
   size_t
   read(uint8_t *buf, size_t buf_len)
   {
-    this->_signal_write_event();
-    IOBufferReader *reader = this->_write_vio.get_reader();
-    return reader->read(buf, buf_len);
+    this->_adapter->encourge_read();
+    auto ibb = this->_adapter->read(buf_len);
+    IOBufferReader reader;
+    reader.block = ibb;
+    return reader.read(buf, buf_len);
   }
 };
 
@@ -295,9 +297,11 @@ test_encode(const char *qif_file, const char *out_file, int dts, int mbs, int am
   QUICApplicationDriver driver;
   QPACK *qpack                   = new QPACK(driver.get_connection(), UINT32_MAX, dts, mbs);
   TestQUICStream *encoder_stream = new TestQUICStream(0);
-  TestQUICStream *decoder_stream = new TestQUICStream(9999);
-  qpack->set_stream(encoder_stream);
-  qpack->set_stream(decoder_stream);
+  TestQUICStream *decoder_stream = new TestQUICStream(10);
+  qpack->on_new_stream(*encoder_stream);
+  qpack->on_new_stream(*decoder_stream);
+  qpack->set_encoder_stream(encoder_stream->id());
+  qpack->set_decoder_stream(decoder_stream->id());
 
   uint64_t stream_id                  = 1;
   MIOBuffer *header_block             = new_MIOBuffer(BUFFER_SIZE_INDEX_32K);
@@ -354,7 +358,7 @@ test_decode(const char *enc_file, const char *out_file, int dts, int mbs, int am
   QUICApplicationDriver driver;
   QPACK *qpack                   = new QPACK(driver.get_connection(), UINT32_MAX, dts, mbs);
   TestQUICStream *encoder_stream = new TestQUICStream(0);
-  qpack->set_stream(encoder_stream);
+  qpack->on_new_stream(*encoder_stream);
 
   int offset     = 0;
   uint8_t *block = nullptr;
diff --git a/src/traffic_quic/quic_client.cc b/src/traffic_quic/quic_client.cc
index 04fb44d..8b3301a 100644
--- a/src/traffic_quic/quic_client.cc
+++ b/src/traffic_quic/quic_client.cc
@@ -152,6 +152,31 @@ Http09ClientApp::Http09ClientApp(QUICNetVConnection *qvc, const QUICClientConfig
 }
 
 void
+Http09ClientApp::on_new_stream(QUICStream &stream)
+{
+  auto ret   = this->_streams.emplace(stream.id(), stream);
+  auto &info = ret.first->second;
+
+  switch (stream.direction()) {
+  case QUICStreamDirection::BIDIRECTIONAL:
+    info.setup_read_vio(this);
+    info.setup_write_vio(this);
+    break;
+  case QUICStreamDirection::SEND:
+    info.setup_write_vio(this);
+    break;
+  case QUICStreamDirection::RECEIVE:
+    info.setup_read_vio(this);
+    break;
+  default:
+    ink_assert(false);
+    break;
+  }
+
+  stream.set_io_adapter(&info.adapter);
+}
+
+void
 Http09ClientApp::start()
 {
   if (this->_config->output[0] != 0x0) {
@@ -183,11 +208,11 @@ Http09ClientApp::_do_http_request()
 
   Http09ClientAppDebug("\n%s", request);
 
-  QUICStreamIO *stream_io = this->_find_stream_io(stream_id);
-
-  stream_io->write(reinterpret_cast<uint8_t *>(request), request_len);
-  stream_io->write_done();
-  stream_io->write_reenable();
+  auto ite       = this->_streams.find(stream_id);
+  VIO *write_vio = ite->second.write_vio;
+  write_vio->get_writer()->write(reinterpret_cast<uint8_t *>(request), request_len);
+  write_vio->done();
+  write_vio->reenable();
 }
 
 int
@@ -195,10 +220,10 @@ Http09ClientApp::main_event_handler(int event, Event *data)
 {
   Http09ClientAppVDebug("%s (%d)", get_vc_event_name(event), event);
 
-  VIO *vio                = reinterpret_cast<VIO *>(data);
-  QUICStreamIO *stream_io = this->_find_stream_io(vio);
+  VIO *vio                     = reinterpret_cast<VIO *>(data->cookie);
+  QUICStreamVCAdapter *adapter = static_cast<QUICStreamVCAdapter *>(vio->vc_server);
 
-  if (stream_io == nullptr) {
+  if (adapter == nullptr) {
     Http09ClientAppDebug("Unknown Stream");
     return -1;
   }
@@ -217,8 +242,10 @@ Http09ClientApp::main_event_handler(int event, Event *data)
 
     uint8_t buf[8192] = {0};
     int64_t nread;
-    while ((nread = stream_io->read(buf, sizeof(buf))) > 0) {
+    auto reader = vio->get_reader();
+    while ((nread = reader->read(buf, sizeof(buf))) > 0) {
       std::cout.write(reinterpret_cast<char *>(buf), nread);
+      vio->ndone += nread;
     }
     std::cout.flush();
 
@@ -227,7 +254,7 @@ Http09ClientApp::main_event_handler(int event, Event *data)
       std::cout.rdbuf(default_stream);
     }
 
-    if (stream_io->is_read_done() && this->_config->close) {
+    if (vio->ntodo() == 0 && this->_config->close) {
       // Connection Close Exercise
       this->_qc->close_quic_connection(
         QUICConnectionErrorUPtr(new QUICConnectionError(QUICTransErrorCode::NO_ERROR, "Close Exercise")));
@@ -278,7 +305,7 @@ Http3ClientApp::start()
   this->_resp_buf                 = new_MIOBuffer(BUFFER_SIZE_INDEX_32K);
   IOBufferReader *resp_buf_reader = _resp_buf->alloc_reader();
 
-  this->_resp_handler = new RespHandler(this->_config, resp_buf_reader, [&](void) {
+  this->_resp_handler  = new RespHandler(this->_config, resp_buf_reader, [&](void) {
     if (this->_config->close) {
       // Connection Close Exercise
       this->_qc->close_quic_connection(
@@ -288,6 +315,7 @@ Http3ClientApp::start()
       this->_qc->reset_quic_connection();
     }
   });
+  this->_req_generator = new ReqGenerator();
 
   super::start();
   this->_do_http_request();
@@ -304,10 +332,10 @@ Http3ClientApp::_do_http_request()
     ink_abort("Could not create bidi stream : %s", error->msg);
   }
 
-  QUICStreamIO *stream_io = this->_find_stream_io(stream_id);
+  auto ite = this->_streams.find(stream_id);
 
   // TODO: create Http3ServerTransaction
-  Http3Transaction *txn = new Http3Transaction(this->_ssn, stream_io);
+  Http3Transaction *txn = new Http3Transaction(this->_ssn, ite->second);
   SCOPED_MUTEX_LOCK(lock, txn->mutex, this_ethread());
 
   // TODO: fix below issue with H2 origin conn stuff
@@ -337,7 +365,7 @@ Http3ClientApp::_do_http_request()
   // TODO: check write avail size
   int64_t nbytes            = this->_req_buf->write(request, request_len);
   IOBufferReader *buf_start = this->_req_buf->alloc_reader();
-  txn->do_io_write(this, nbytes, buf_start);
+  txn->do_io_write(this->_req_generator, nbytes, buf_start);
 }
 
 //
@@ -407,3 +435,14 @@ RespHandler::main_event_handler(int event, Event *data)
 
   return EVENT_CONT;
 }
+
+ReqGenerator::ReqGenerator() : Continuation(new_ProxyMutex())
+{
+  SET_HANDLER(&ReqGenerator::main_event_handler);
+}
+
+int
+ReqGenerator::main_event_handler(int event, Event *data)
+{
+  return EVENT_CONT;
+}
diff --git a/src/traffic_quic/quic_client.h b/src/traffic_quic/quic_client.h
index fc12dbc..3a52450 100644
--- a/src/traffic_quic/quic_client.h
+++ b/src/traffic_quic/quic_client.h
@@ -61,6 +61,13 @@ private:
   std::function<void()> _on_complete;
 };
 
+class ReqGenerator : public Continuation
+{
+public:
+  ReqGenerator();
+  int main_event_handler(int event, Event *data);
+};
+
 class QUICClient : public Continuation
 {
 public:
@@ -81,6 +88,8 @@ class Http09ClientApp : public QUICApplication
 public:
   Http09ClientApp(QUICNetVConnection *qvc, const QUICClientConfig *config);
 
+  void on_new_stream(QUICStream &stream) override;
+
   void start();
   int main_event_handler(int event, Event *data);
 
@@ -89,6 +98,7 @@ private:
 
   const QUICClientConfig *_config = nullptr;
   const char *_filename           = nullptr;
+  std::unordered_map<QUICStreamId, QUICStreamVCAdapter::IOInfo> _streams;
 };
 
 class Http3ClientApp : public Http3App
@@ -106,6 +116,7 @@ private:
   void _do_http_request();
 
   RespHandler *_resp_handler      = nullptr;
+  ReqGenerator *_req_generator    = nullptr;
   const QUICClientConfig *_config = nullptr;
 
   MIOBuffer *_req_buf  = nullptr;
diff --git a/src/traffic_quic/traffic_quic.cc b/src/traffic_quic/traffic_quic.cc
index f3ceaa6..4ee2f5f 100644
--- a/src/traffic_quic/traffic_quic.cc
+++ b/src/traffic_quic/traffic_quic.cc
@@ -277,6 +277,11 @@ HttpDebugNames::get_api_hook_name(TSHttpHookID t)
 {
   return "dummy";
 }
+const char *
+HttpDebugNames::get_event_name(int)
+{
+  return "dummy";
+}
 
 #include "HttpSM.h"
 HttpSM::HttpSM() : Continuation(nullptr), vc_table(this) {}