From 0d7ce688229a9b07ecb1b4ae62dda43d733f68f5 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sat, 9 Dec 2017 05:29:11 +0100
Subject: [PATCH 01/77] src: minor refactoring to StreamBase writes

Instead of having per-request callbacks, always call a callback
on the `StreamBase` instance itself for `WriteWrap` and `ShutdownWrap`.

This makes `WriteWrap` cleanup consistent for all stream classes,
since the after-write callback is always the same now.

If special handling is needed for writes that happen to a sub-class,
`AfterWrite` can be overridden by that class, rather than that
class providing its own callback (e.g. updating the write
queue size for libuv streams).

If special handling is needed for writes that happen on another
stream instance, the existing `after_write_cb()` callback
is used for that (e.g. custom code after writing to the
transport from a TLS stream).

As a nice bonus, this also makes `WriteWrap` and `ShutdownWrap`
instances slightly smaller.

PR-URL: https://github.com/nodejs/node/pull/17564
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 src/js_stream.cc      |  2 +-
 src/node_http2.cc     |  5 +---
 src/stream_base-inl.h | 12 ++++++--
 src/stream_base.cc    | 23 ++++++---------
 src/stream_base.h     | 66 ++++++++++++++++++-------------------------
 src/stream_wrap.cc    | 17 ++++++-----
 src/stream_wrap.h     |  7 +++--
 src/tls_wrap.cc       | 26 ++++++++---------
 src/tls_wrap.h        |  4 +--
 9 files changed, 73 insertions(+), 89 deletions(-)

diff --git a/src/js_stream.cc b/src/js_stream.cc
index 7d4ad7a4e978a6..e8e31f41cb64ad 100644
--- a/src/js_stream.cc
+++ b/src/js_stream.cc
@@ -181,7 +181,7 @@ void JSStream::DoAfterWrite(const FunctionCallbackInfo<Value>& args) {
   ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());
   ASSIGN_OR_RETURN_UNWRAP(&w, args[0].As<Object>());
 
-  wrap->OnAfterWrite(w);
+  w->Done(0);
 }
 
 
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 89d68de88f8cfe..0747789786b028 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -975,9 +975,6 @@ inline void Http2Session::SetChunksSinceLastWrite(size_t n) {
 
 WriteWrap* Http2Session::AllocateSend() {
   HandleScope scope(env()->isolate());
-  auto AfterWrite = [](WriteWrap* req, int status) {
-    req->Dispose();
-  };
   Local<Object> obj =
       env()->write_wrap_constructor_function()
           ->NewInstance(env()->context()).ToLocalChecked();
@@ -987,7 +984,7 @@ WriteWrap* Http2Session::AllocateSend() {
           session(),
           NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
   // Max frame size + 9 bytes for the header
-  return WriteWrap::New(env(), obj, stream_, AfterWrite, size + 9);
+  return WriteWrap::New(env(), obj, stream_, size + 9);
 }
 
 void Http2Session::Send(WriteWrap* req, char* buf, size_t length) {
diff --git a/src/stream_base-inl.h b/src/stream_base-inl.h
index eacdb3832c0662..901e07c1e96a25 100644
--- a/src/stream_base-inl.h
+++ b/src/stream_base-inl.h
@@ -144,15 +144,19 @@ void StreamBase::JSMethod(const FunctionCallbackInfo<Value>& args) {
 }
 
 
+inline void ShutdownWrap::OnDone(int status) {
+  stream()->AfterShutdown(this, status);
+}
+
+
 WriteWrap* WriteWrap::New(Environment* env,
                           Local<Object> obj,
                           StreamBase* wrap,
-                          DoneCb cb,
                           size_t extra) {
   size_t storage_size = ROUND_UP(sizeof(WriteWrap), kAlignSize) + extra;
   char* storage = new char[storage_size];
 
-  return new(storage) WriteWrap(env, obj, wrap, cb, storage_size);
+  return new(storage) WriteWrap(env, obj, wrap, storage_size);
 }
 
 
@@ -172,6 +176,10 @@ size_t WriteWrap::ExtraSize() const {
   return storage_size_ - ROUND_UP(sizeof(*this), kAlignSize);
 }
 
+inline void WriteWrap::OnDone(int status) {
+  stream()->AfterWrite(this, status);
+}
+
 }  // namespace node
 
 #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/stream_base.cc b/src/stream_base.cc
index 34564cadbe777a..4d9e1dfc6b2dba 100644
--- a/src/stream_base.cc
+++ b/src/stream_base.cc
@@ -55,8 +55,7 @@ int StreamBase::Shutdown(const FunctionCallbackInfo<Value>& args) {
   AsyncHooks::DefaultTriggerAsyncIdScope(env, wrap->get_async_id());
   ShutdownWrap* req_wrap = new ShutdownWrap(env,
                                             req_wrap_obj,
-                                            this,
-                                            AfterShutdown);
+                                            this);
 
   int err = DoShutdown(req_wrap);
   if (err)
@@ -66,7 +65,6 @@ int StreamBase::Shutdown(const FunctionCallbackInfo<Value>& args) {
 
 
 void StreamBase::AfterShutdown(ShutdownWrap* req_wrap, int status) {
-  StreamBase* wrap = req_wrap->wrap();
   Environment* env = req_wrap->env();
 
   // The wrap and request objects should still be there.
@@ -78,7 +76,7 @@ void StreamBase::AfterShutdown(ShutdownWrap* req_wrap, int status) {
   Local<Object> req_wrap_obj = req_wrap->object();
   Local<Value> argv[3] = {
     Integer::New(env->isolate(), status),
-    wrap->GetObject(),
+    GetObject(),
     req_wrap_obj
   };
 
@@ -159,8 +157,7 @@ int StreamBase::Writev(const FunctionCallbackInfo<Value>& args) {
     CHECK_NE(wrap, nullptr);
     AsyncHooks::DefaultTriggerAsyncIdScope trigger_scope(env,
                                                          wrap->get_async_id());
-    req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite,
-                              storage_size);
+    req_wrap = WriteWrap::New(env, req_wrap_obj, this, storage_size);
   }
 
   offset = 0;
@@ -252,7 +249,7 @@ int StreamBase::WriteBuffer(const FunctionCallbackInfo<Value>& args) {
     CHECK_NE(wrap, nullptr);
     AsyncHooks::DefaultTriggerAsyncIdScope trigger_scope(env,
                                                          wrap->get_async_id());
-    req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite);
+    req_wrap = WriteWrap::New(env, req_wrap_obj, this);
   }
 
   err = DoWrite(req_wrap, bufs, count, nullptr);
@@ -338,8 +335,7 @@ int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args) {
     CHECK_NE(wrap, nullptr);
     AsyncHooks::DefaultTriggerAsyncIdScope trigger_scope(env,
                                                          wrap->get_async_id());
-    req_wrap = WriteWrap::New(env, req_wrap_obj, this, AfterWrite,
-                              storage_size);
+    req_wrap = WriteWrap::New(env, req_wrap_obj, this, storage_size);
   }
 
   data = req_wrap->Extra();
@@ -401,7 +397,6 @@ int StreamBase::WriteString(const FunctionCallbackInfo<Value>& args) {
 
 
 void StreamBase::AfterWrite(WriteWrap* req_wrap, int status) {
-  StreamBase* wrap = req_wrap->wrap();
   Environment* env = req_wrap->env();
 
   HandleScope handle_scope(env->isolate());
@@ -413,19 +408,19 @@ void StreamBase::AfterWrite(WriteWrap* req_wrap, int status) {
   // Unref handle property
   Local<Object> req_wrap_obj = req_wrap->object();
   req_wrap_obj->Delete(env->context(), env->handle_string()).FromJust();
-  wrap->OnAfterWrite(req_wrap);
+  OnAfterWrite(req_wrap, status);
 
   Local<Value> argv[] = {
     Integer::New(env->isolate(), status),
-    wrap->GetObject(),
+    GetObject(),
     req_wrap_obj,
     Undefined(env->isolate())
   };
 
-  const char* msg = wrap->Error();
+  const char* msg = Error();
   if (msg != nullptr) {
     argv[3] = OneByteString(env->isolate(), msg);
-    wrap->ClearError();
+    ClearError();
   }
 
   if (req_wrap_obj->Has(env->context(), env->oncomplete_string()).FromJust())
diff --git a/src/stream_base.h b/src/stream_base.h
index 94e4bfd73961da..926a3132925c01 100644
--- a/src/stream_base.h
+++ b/src/stream_base.h
@@ -16,27 +16,27 @@ namespace node {
 // Forward declarations
 class StreamBase;
 
-template <class Req>
+template<typename Base>
 class StreamReq {
  public:
-  typedef void (*DoneCb)(Req* req, int status);
-
-  explicit StreamReq(DoneCb cb) : cb_(cb) {
+  explicit StreamReq(StreamBase* stream) : stream_(stream) {
   }
 
   inline void Done(int status, const char* error_str = nullptr) {
-    Req* req = static_cast<Req*>(this);
+    Base* req = static_cast<Base*>(this);
     Environment* env = req->env();
     if (error_str != nullptr) {
       req->object()->Set(env->error_string(),
                          OneByteString(env->isolate(), error_str));
     }
 
-    cb_(req, status);
+    req->OnDone(status);
   }
 
+  inline StreamBase* stream() const { return stream_; }
+
  private:
-  DoneCb cb_;
+  StreamBase* const stream_;
 };
 
 class ShutdownWrap : public ReqWrap<uv_shutdown_t>,
@@ -44,11 +44,9 @@ class ShutdownWrap : public ReqWrap<uv_shutdown_t>,
  public:
   ShutdownWrap(Environment* env,
                v8::Local<v8::Object> req_wrap_obj,
-               StreamBase* wrap,
-               DoneCb cb)
+               StreamBase* stream)
       : ReqWrap(env, req_wrap_obj, AsyncWrap::PROVIDER_SHUTDOWNWRAP),
-        StreamReq<ShutdownWrap>(cb),
-        wrap_(wrap) {
+        StreamReq<ShutdownWrap>(stream) {
     Wrap(req_wrap_obj, this);
   }
 
@@ -60,27 +58,22 @@ class ShutdownWrap : public ReqWrap<uv_shutdown_t>,
     return ContainerOf(&ShutdownWrap::req_, req);
   }
 
-  inline StreamBase* wrap() const { return wrap_; }
   size_t self_size() const override { return sizeof(*this); }
 
- private:
-  StreamBase* const wrap_;
+  inline void OnDone(int status);  // Just calls stream()->AfterShutdown()
 };
 
-class WriteWrap: public ReqWrap<uv_write_t>,
-                 public StreamReq<WriteWrap> {
+class WriteWrap : public ReqWrap<uv_write_t>,
+                  public StreamReq<WriteWrap> {
  public:
   static inline WriteWrap* New(Environment* env,
                                v8::Local<v8::Object> obj,
-                               StreamBase* wrap,
-                               DoneCb cb,
+                               StreamBase* stream,
                                size_t extra = 0);
   inline void Dispose();
   inline char* Extra(size_t offset = 0);
   inline size_t ExtraSize() const;
 
-  inline StreamBase* wrap() const { return wrap_; }
-
   size_t self_size() const override { return storage_size_; }
 
   static WriteWrap* from_req(uv_write_t* req) {
@@ -91,24 +84,22 @@ class WriteWrap: public ReqWrap<uv_write_t>,
 
   WriteWrap(Environment* env,
             v8::Local<v8::Object> obj,
-            StreamBase* wrap,
-            DoneCb cb)
+            StreamBase* stream)
       : ReqWrap(env, obj, AsyncWrap::PROVIDER_WRITEWRAP),
-        StreamReq<WriteWrap>(cb),
-        wrap_(wrap),
+        StreamReq<WriteWrap>(stream),
         storage_size_(0) {
     Wrap(obj, this);
   }
 
+  inline void OnDone(int status);  // Just calls stream()->AfterWrite()
+
  protected:
   WriteWrap(Environment* env,
             v8::Local<v8::Object> obj,
-            StreamBase* wrap,
-            DoneCb cb,
+            StreamBase* stream,
             size_t storage_size)
       : ReqWrap(env, obj, AsyncWrap::PROVIDER_WRITEWRAP),
-        StreamReq<WriteWrap>(cb),
-        wrap_(wrap),
+        StreamReq<WriteWrap>(stream),
         storage_size_(storage_size) {
     Wrap(obj, this);
   }
@@ -129,7 +120,6 @@ class WriteWrap: public ReqWrap<uv_write_t>,
   // WriteWrap. Ensure this never happens.
   void operator delete(void* ptr) { UNREACHABLE(); }
 
-  StreamBase* const wrap_;
   const size_t storage_size_;
 };
 
@@ -151,7 +141,7 @@ class StreamResource {
     void* ctx;
   };
 
-  typedef void (*AfterWriteCb)(WriteWrap* w, void* ctx);
+  typedef void (*AfterWriteCb)(WriteWrap* w, int status, void* ctx);
   typedef void (*AllocCb)(size_t size, uv_buf_t* buf, void* ctx);
   typedef void (*ReadCb)(ssize_t nread,
                          const uv_buf_t* buf,
@@ -176,9 +166,9 @@ class StreamResource {
   virtual void ClearError();
 
   // Events
-  inline void OnAfterWrite(WriteWrap* w) {
+  inline void OnAfterWrite(WriteWrap* w, int status) {
     if (!after_write_cb_.is_empty())
-      after_write_cb_.fn(w, after_write_cb_.ctx);
+      after_write_cb_.fn(w, status, after_write_cb_.ctx);
   }
 
   inline void OnAlloc(size_t size, uv_buf_t* buf) {
@@ -208,14 +198,12 @@ class StreamResource {
   inline Callback<ReadCb> read_cb() { return read_cb_; }
   inline Callback<DestructCb> destruct_cb() { return destruct_cb_; }
 
- private:
+ protected:
   Callback<AfterWriteCb> after_write_cb_;
   Callback<AllocCb> alloc_cb_;
   Callback<ReadCb> read_cb_;
   Callback<DestructCb> destruct_cb_;
   uint64_t bytes_read_;
-
-  friend class StreamBase;
 };
 
 class StreamBase : public StreamResource {
@@ -257,6 +245,10 @@ class StreamBase : public StreamResource {
                 v8::Local<v8::Object> buf,
                 v8::Local<v8::Object> handle);
 
+  // These are called by the respective {Write,Shutdown}Wrap class.
+  virtual void AfterShutdown(ShutdownWrap* req, int status);
+  virtual void AfterWrite(WriteWrap* req, int status);
+
  protected:
   explicit StreamBase(Environment* env) : env_(env), consumed_(false) {
   }
@@ -267,10 +259,6 @@ class StreamBase : public StreamResource {
   virtual AsyncWrap* GetAsyncWrap() = 0;
   virtual v8::Local<v8::Object> GetObject();
 
-  // Libuv callbacks
-  static void AfterShutdown(ShutdownWrap* req, int status);
-  static void AfterWrite(WriteWrap* req, int status);
-
   // JS Methods
   int ReadStart(const v8::FunctionCallbackInfo<v8::Value>& args);
   int ReadStop(const v8::FunctionCallbackInfo<v8::Value>& args);
diff --git a/src/stream_wrap.cc b/src/stream_wrap.cc
index 8516e39f295f03..9ff6a8b69a3689 100644
--- a/src/stream_wrap.cc
+++ b/src/stream_wrap.cc
@@ -92,7 +92,6 @@ LibuvStreamWrap::LibuvStreamWrap(Environment* env,
                  provider),
       StreamBase(env),
       stream_(stream) {
-  set_after_write_cb({ OnAfterWriteImpl, this });
   set_alloc_cb({ OnAllocImpl, this });
   set_read_cb({ OnReadImpl, this });
 }
@@ -299,13 +298,13 @@ void LibuvStreamWrap::SetBlocking(const FunctionCallbackInfo<Value>& args) {
 
 int LibuvStreamWrap::DoShutdown(ShutdownWrap* req_wrap) {
   int err;
-  err = uv_shutdown(req_wrap->req(), stream(), AfterShutdown);
+  err = uv_shutdown(req_wrap->req(), stream(), AfterUvShutdown);
   req_wrap->Dispatched();
   return err;
 }
 
 
-void LibuvStreamWrap::AfterShutdown(uv_shutdown_t* req, int status) {
+void LibuvStreamWrap::AfterUvShutdown(uv_shutdown_t* req, int status) {
   ShutdownWrap* req_wrap = ShutdownWrap::from_req(req);
   CHECK_NE(req_wrap, nullptr);
   HandleScope scope(req_wrap->env()->isolate());
@@ -360,9 +359,9 @@ int LibuvStreamWrap::DoWrite(WriteWrap* w,
                         uv_stream_t* send_handle) {
   int r;
   if (send_handle == nullptr) {
-    r = uv_write(w->req(), stream(), bufs, count, AfterWrite);
+    r = uv_write(w->req(), stream(), bufs, count, AfterUvWrite);
   } else {
-    r = uv_write2(w->req(), stream(), bufs, count, send_handle, AfterWrite);
+    r = uv_write2(w->req(), stream(), bufs, count, send_handle, AfterUvWrite);
   }
 
   if (!r) {
@@ -383,7 +382,7 @@ int LibuvStreamWrap::DoWrite(WriteWrap* w,
 }
 
 
-void LibuvStreamWrap::AfterWrite(uv_write_t* req, int status) {
+void LibuvStreamWrap::AfterUvWrite(uv_write_t* req, int status) {
   WriteWrap* req_wrap = WriteWrap::from_req(req);
   CHECK_NE(req_wrap, nullptr);
   HandleScope scope(req_wrap->env()->isolate());
@@ -392,9 +391,9 @@ void LibuvStreamWrap::AfterWrite(uv_write_t* req, int status) {
 }
 
 
-void LibuvStreamWrap::OnAfterWriteImpl(WriteWrap* w, void* ctx) {
-  LibuvStreamWrap* wrap = static_cast<LibuvStreamWrap*>(ctx);
-  wrap->UpdateWriteQueueSize();
+void LibuvStreamWrap::AfterWrite(WriteWrap* w, int status) {
+  StreamBase::AfterWrite(w, status);
+  UpdateWriteQueueSize();
 }
 
 }  // namespace node
diff --git a/src/stream_wrap.h b/src/stream_wrap.h
index 43df504e81b86e..df7349b093f3c2 100644
--- a/src/stream_wrap.h
+++ b/src/stream_wrap.h
@@ -103,17 +103,18 @@ class LibuvStreamWrap : public HandleWrap, public StreamBase {
   static void OnRead(uv_stream_t* handle,
                      ssize_t nread,
                      const uv_buf_t* buf);
-  static void AfterWrite(uv_write_t* req, int status);
-  static void AfterShutdown(uv_shutdown_t* req, int status);
+  static void AfterUvWrite(uv_write_t* req, int status);
+  static void AfterUvShutdown(uv_shutdown_t* req, int status);
 
   // Resource interface implementation
-  static void OnAfterWriteImpl(WriteWrap* w, void* ctx);
   static void OnAllocImpl(size_t size, uv_buf_t* buf, void* ctx);
   static void OnReadImpl(ssize_t nread,
                          const uv_buf_t* buf,
                          uv_handle_type pending,
                          void* ctx);
 
+  void AfterWrite(WriteWrap* req_wrap, int status) override;
+
   uv_stream_t* const stream_;
 };
 
diff --git a/src/tls_wrap.cc b/src/tls_wrap.cc
index 3f5ed2c57580ff..c15cf166fb29d8 100644
--- a/src/tls_wrap.cc
+++ b/src/tls_wrap.cc
@@ -328,8 +328,7 @@ void TLSWrap::EncOut() {
           ->NewInstance(env()->context()).ToLocalChecked();
   WriteWrap* write_req = WriteWrap::New(env(),
                                         req_wrap_obj,
-                                        this,
-                                        EncOutCb);
+                                        stream_);
 
   uv_buf_t buf[arraysize(data)];
   for (size_t i = 0; i < count; i++)
@@ -346,34 +345,31 @@ void TLSWrap::EncOut() {
 }
 
 
-void TLSWrap::EncOutCb(WriteWrap* req_wrap, int status) {
-  TLSWrap* wrap = req_wrap->wrap()->Cast<TLSWrap>();
-  req_wrap->Dispose();
-
+void TLSWrap::EncOutAfterWrite(WriteWrap* req_wrap, int status) {
   // We should not be getting here after `DestroySSL`, because all queued writes
   // must be invoked with UV_ECANCELED
-  CHECK_NE(wrap->ssl_, nullptr);
+  CHECK_NE(ssl_, nullptr);
 
   // Handle error
   if (status) {
     // Ignore errors after shutdown
-    if (wrap->shutdown_)
+    if (shutdown_)
       return;
 
     // Notify about error
-    wrap->InvokeQueued(status);
+    InvokeQueued(status);
     return;
   }
 
   // Commit
-  crypto::NodeBIO::FromBIO(wrap->enc_out_)->Read(nullptr, wrap->write_size_);
+  crypto::NodeBIO::FromBIO(enc_out_)->Read(nullptr, write_size_);
 
   // Ensure that the progress will be made and `InvokeQueued` will be called.
-  wrap->ClearIn();
+  ClearIn();
 
   // Try writing more data
-  wrap->write_size_ = 0;
-  wrap->EncOut();
+  write_size_ = 0;
+  EncOut();
 }
 
 
@@ -681,9 +677,9 @@ int TLSWrap::DoWrite(WriteWrap* w,
 }
 
 
-void TLSWrap::OnAfterWriteImpl(WriteWrap* w, void* ctx) {
+void TLSWrap::OnAfterWriteImpl(WriteWrap* w, int status, void* ctx) {
   TLSWrap* wrap = static_cast<TLSWrap*>(ctx);
-  wrap->UpdateWriteQueueSize();
+  wrap->EncOutAfterWrite(w, status);
 }
 
 
diff --git a/src/tls_wrap.h b/src/tls_wrap.h
index bd5a4d4028a408..96820e9b7201b1 100644
--- a/src/tls_wrap.h
+++ b/src/tls_wrap.h
@@ -112,7 +112,7 @@ class TLSWrap : public AsyncWrap,
   static void SSLInfoCallback(const SSL* ssl_, int where, int ret);
   void InitSSL();
   void EncOut();
-  static void EncOutCb(WriteWrap* req_wrap, int status);
+  void EncOutAfterWrite(WriteWrap* req_wrap, int status);
   bool ClearIn();
   void ClearOut();
   void MakePending();
@@ -135,7 +135,7 @@ class TLSWrap : public AsyncWrap,
   uint32_t UpdateWriteQueueSize(uint32_t write_queue_size = 0);
 
   // Resource implementation
-  static void OnAfterWriteImpl(WriteWrap* w, void* ctx);
+  static void OnAfterWriteImpl(WriteWrap* w, int status, void* ctx);
   static void OnAllocImpl(size_t size, uv_buf_t* buf, void* ctx);
   static void OnReadImpl(ssize_t nread,
                          const uv_buf_t* buf,

From 16a3ad0f7c07ce2ee5220a94b608f6a0e02945e2 Mon Sep 17 00:00:00 2001
From: Jure Triglav <juretriglav@gmail.com>
Date: Wed, 13 Dec 2017 22:35:32 +0000
Subject: [PATCH 02/77] src: replace SetAccessor w/ SetAccessorProperty

PR-URL: https://github.com/nodejs/node/pull/17665
Fixes: https://github.com/nodejs/node/issues/17636
Refs: https://github.com/nodejs/node/pull/16482
Refs: https://github.com/nodejs/node/pull/16860
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
---
 src/node_crypto.cc                            | 81 ++++++++++---------
 src/node_crypto.h                             |  9 +--
 src/node_perf.cc                              | 64 +++++++++++----
 src/stream_base-inl.h                         | 73 +++++++++--------
 src/stream_base.h                             |  9 +--
 src/udp_wrap.cc                               | 23 ++++--
 src/udp_wrap.h                                |  3 +-
 test/parallel/test-accessor-properties.js     | 77 ++++++++++++++++++
 .../test-stream-base-prototype-accessors.js   | 27 -------
 9 files changed, 231 insertions(+), 135 deletions(-)
 create mode 100644 test/parallel/test-accessor-properties.js
 delete mode 100644 test/parallel/test-stream-base-prototype-accessors.js

diff --git a/src/node_crypto.cc b/src/node_crypto.cc
index 580d6a4f4d55bc..615e281a313812 100644
--- a/src/node_crypto.cc
+++ b/src/node_crypto.cc
@@ -79,7 +79,6 @@ static const int X509_NAME_FLAGS = ASN1_STRFLGS_ESC_CTRL
 namespace node {
 namespace crypto {
 
-using v8::AccessorSignature;
 using v8::Array;
 using v8::Boolean;
 using v8::Context;
@@ -102,8 +101,8 @@ using v8::Object;
 using v8::ObjectTemplate;
 using v8::Persistent;
 using v8::PropertyAttribute;
-using v8::PropertyCallbackInfo;
 using v8::ReadOnly;
+using v8::Signature;
 using v8::String;
 using v8::Value;
 
@@ -481,14 +480,18 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
   t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kTicketKeyIVIndex"),
          Integer::NewFromUnsigned(env->isolate(), kTicketKeyIVIndex));
 
-  t->PrototypeTemplate()->SetAccessor(
+  Local<FunctionTemplate> ctx_getter_templ =
+      FunctionTemplate::New(env->isolate(),
+                            CtxGetter,
+                            env->as_external(),
+                            Signature::New(env->isolate(), t));
+
+
+  t->PrototypeTemplate()->SetAccessorProperty(
       FIXED_ONE_BYTE_STRING(env->isolate(), "_external"),
-      CtxGetter,
-      nullptr,
-      env->as_external(),
-      DEFAULT,
-      static_cast<PropertyAttribute>(ReadOnly | DontDelete),
-      AccessorSignature::New(env->isolate(), t));
+      ctx_getter_templ,
+      Local<FunctionTemplate>(),
+      static_cast<PropertyAttribute>(ReadOnly | DontDelete));
 
   target->Set(secureContextString, t->GetFunction());
   env->set_secure_context_constructor_template(t);
@@ -1457,8 +1460,7 @@ int SecureContext::TicketCompatibilityCallback(SSL* ssl,
 #endif
 
 
-void SecureContext::CtxGetter(Local<String> property,
-                              const PropertyCallbackInfo<Value>& info) {
+void SecureContext::CtxGetter(const FunctionCallbackInfo<Value>& info) {
   SecureContext* sc;
   ASSIGN_OR_RETURN_UNWRAP(&sc, info.This());
   Local<External> ext = External::New(info.GetIsolate(), sc->ctx_);
@@ -1528,14 +1530,17 @@ void SSLWrap<Base>::AddMethods(Environment* env, Local<FunctionTemplate> t) {
   env->SetProtoMethod(t, "getALPNNegotiatedProtocol", GetALPNNegotiatedProto);
   env->SetProtoMethod(t, "setALPNProtocols", SetALPNProtocols);
 
-  t->PrototypeTemplate()->SetAccessor(
+  Local<FunctionTemplate> ssl_getter_templ =
+      FunctionTemplate::New(env->isolate(),
+                            SSLGetter,
+                            env->as_external(),
+                            Signature::New(env->isolate(), t));
+
+  t->PrototypeTemplate()->SetAccessorProperty(
       FIXED_ONE_BYTE_STRING(env->isolate(), "_external"),
-      SSLGetter,
-      nullptr,
-      env->as_external(),
-      DEFAULT,
-      static_cast<PropertyAttribute>(ReadOnly | DontDelete),
-      AccessorSignature::New(env->isolate(), t));
+      ssl_getter_templ,
+      Local<FunctionTemplate>(),
+      static_cast<PropertyAttribute>(ReadOnly | DontDelete));
 }
 
 
@@ -2696,8 +2701,7 @@ void SSLWrap<Base>::CertCbDone(const FunctionCallbackInfo<Value>& args) {
 
 
 template <class Base>
-void SSLWrap<Base>::SSLGetter(Local<String> property,
-                              const PropertyCallbackInfo<Value>& info) {
+void SSLWrap<Base>::SSLGetter(const FunctionCallbackInfo<Value>& info) {
   Base* base;
   ASSIGN_OR_RETURN_UNWRAP(&base, info.This());
   SSL* ssl = base->ssl_;
@@ -4694,14 +4698,17 @@ void DiffieHellman::Initialize(Environment* env, Local<Object> target) {
   env->SetProtoMethod(t, "setPublicKey", SetPublicKey);
   env->SetProtoMethod(t, "setPrivateKey", SetPrivateKey);
 
-  t->InstanceTemplate()->SetAccessor(
+  Local<FunctionTemplate> verify_error_getter_templ =
+      FunctionTemplate::New(env->isolate(),
+                            DiffieHellman::VerifyErrorGetter,
+                            env->as_external(),
+                            Signature::New(env->isolate(), t));
+
+  t->InstanceTemplate()->SetAccessorProperty(
       env->verify_error_string(),
-      DiffieHellman::VerifyErrorGetter,
-      nullptr,
-      env->as_external(),
-      DEFAULT,
-      attributes,
-      AccessorSignature::New(env->isolate(), t));
+      verify_error_getter_templ,
+      Local<FunctionTemplate>(),
+      attributes);
 
   target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "DiffieHellman"),
               t->GetFunction());
@@ -4716,14 +4723,17 @@ void DiffieHellman::Initialize(Environment* env, Local<Object> target) {
   env->SetProtoMethod(t2, "getPublicKey", GetPublicKey);
   env->SetProtoMethod(t2, "getPrivateKey", GetPrivateKey);
 
-  t2->InstanceTemplate()->SetAccessor(
+  Local<FunctionTemplate> verify_error_getter_templ2 =
+      FunctionTemplate::New(env->isolate(),
+                            DiffieHellman::VerifyErrorGetter,
+                            env->as_external(),
+                            Signature::New(env->isolate(), t2));
+
+  t2->InstanceTemplate()->SetAccessorProperty(
       env->verify_error_string(),
-      DiffieHellman::VerifyErrorGetter,
-      nullptr,
-      env->as_external(),
-      DEFAULT,
-      attributes,
-      AccessorSignature::New(env->isolate(), t2));
+      verify_error_getter_templ2,
+      Local<FunctionTemplate>(),
+      attributes);
 
   target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "DiffieHellmanGroup"),
               t2->GetFunction());
@@ -5037,8 +5047,7 @@ void DiffieHellman::SetPrivateKey(const FunctionCallbackInfo<Value>& args) {
 }
 
 
-void DiffieHellman::VerifyErrorGetter(Local<String> property,
-                                      const PropertyCallbackInfo<Value>& args) {
+void DiffieHellman::VerifyErrorGetter(const FunctionCallbackInfo<Value>& args) {
   HandleScope scope(args.GetIsolate());
 
   DiffieHellman* diffieHellman;
diff --git a/src/node_crypto.h b/src/node_crypto.h
index 41261910b94018..79e358aebe2ac3 100644
--- a/src/node_crypto.h
+++ b/src/node_crypto.h
@@ -141,8 +141,7 @@ class SecureContext : public BaseObject {
       const v8::FunctionCallbackInfo<v8::Value>& args);
   static void EnableTicketKeyCallback(
       const v8::FunctionCallbackInfo<v8::Value>& args);
-  static void CtxGetter(v8::Local<v8::String> property,
-                        const v8::PropertyCallbackInfo<v8::Value>& info);
+  static void CtxGetter(const v8::FunctionCallbackInfo<v8::Value>& info);
 
   template <bool primary>
   static void GetCertificate(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -322,8 +321,7 @@ class SSLWrap {
                                 void* arg);
   static int TLSExtStatusCallback(SSL* s, void* arg);
   static int SSLCertCallback(SSL* s, void* arg);
-  static void SSLGetter(v8::Local<v8::String> property,
-                        const v8::PropertyCallbackInfo<v8::Value>& info);
+  static void SSLGetter(const v8::FunctionCallbackInfo<v8::Value>& info);
 
   void DestroySSL();
   void WaitForCertCb(CertCb cb, void* arg);
@@ -689,8 +687,7 @@ class DiffieHellman : public BaseObject {
   static void SetPublicKey(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void SetPrivateKey(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void VerifyErrorGetter(
-      v8::Local<v8::String> property,
-      const v8::PropertyCallbackInfo<v8::Value>& args);
+      const v8::FunctionCallbackInfo<v8::Value>& args);
 
   DiffieHellman(Environment* env, v8::Local<v8::Object> wrap)
       : BaseObject(env, wrap),
diff --git a/src/node_perf.cc b/src/node_perf.cc
index 02145eeffdba12..fc793a9c8c5b6a 100644
--- a/src/node_perf.cc
+++ b/src/node_perf.cc
@@ -19,7 +19,7 @@ using v8::Local;
 using v8::Name;
 using v8::Object;
 using v8::ObjectTemplate;
-using v8::PropertyCallbackInfo;
+using v8::Signature;
 using v8::String;
 using v8::Value;
 
@@ -121,8 +121,7 @@ void Measure(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(obj);
 }
 
-void GetPerformanceEntryName(const Local<String> prop,
-                             const PropertyCallbackInfo<Value>& info) {
+void GetPerformanceEntryName(const FunctionCallbackInfo<Value>& info) {
   Isolate* isolate = info.GetIsolate();
   PerformanceEntry* entry;
   ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
@@ -130,8 +129,7 @@ void GetPerformanceEntryName(const Local<String> prop,
     String::NewFromUtf8(isolate, entry->name().c_str(), String::kNormalString));
 }
 
-void GetPerformanceEntryType(const Local<String> prop,
-                             const PropertyCallbackInfo<Value>& info) {
+void GetPerformanceEntryType(const FunctionCallbackInfo<Value>& info) {
   Isolate* isolate = info.GetIsolate();
   PerformanceEntry* entry;
   ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
@@ -139,15 +137,13 @@ void GetPerformanceEntryType(const Local<String> prop,
     String::NewFromUtf8(isolate, entry->type().c_str(), String::kNormalString));
 }
 
-void GetPerformanceEntryStartTime(const Local<String> prop,
-                                  const PropertyCallbackInfo<Value>& info) {
+void GetPerformanceEntryStartTime(const FunctionCallbackInfo<Value>& info) {
   PerformanceEntry* entry;
   ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
   info.GetReturnValue().Set(entry->startTime());
 }
 
-void GetPerformanceEntryDuration(const Local<String> prop,
-                                 const PropertyCallbackInfo<Value>& info) {
+void GetPerformanceEntryDuration(const FunctionCallbackInfo<Value>& info) {
   PerformanceEntry* entry;
   ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
   info.GetReturnValue().Set(entry->duration());
@@ -335,14 +331,50 @@ void Init(Local<Object> target,
   Local<FunctionTemplate> pe = env->NewFunctionTemplate(PerformanceEntry::New);
   pe->InstanceTemplate()->SetInternalFieldCount(1);
   pe->SetClassName(performanceEntryString);
+
+  Local<Signature> signature = Signature::New(env->isolate(), pe);
+
+  Local<FunctionTemplate> get_performance_entry_name_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetPerformanceEntryName,
+                            env->as_external(),
+                            signature);
+
+  Local<FunctionTemplate> get_performance_entry_type_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetPerformanceEntryType,
+                            env->as_external(),
+                            signature);
+
+  Local<FunctionTemplate> get_performance_entry_start_time_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetPerformanceEntryStartTime,
+                            env->as_external(),
+                            signature);
+
+  Local<FunctionTemplate> get_performance_entry_duration_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetPerformanceEntryDuration,
+                            env->as_external(),
+                            signature);
+
   Local<ObjectTemplate> ot = pe->InstanceTemplate();
-  ot->SetAccessor(env->name_string(), GetPerformanceEntryName);
-  ot->SetAccessor(FIXED_ONE_BYTE_STRING(isolate, "entryType"),
-                  GetPerformanceEntryType);
-  ot->SetAccessor(FIXED_ONE_BYTE_STRING(isolate, "startTime"),
-                  GetPerformanceEntryStartTime);
-  ot->SetAccessor(FIXED_ONE_BYTE_STRING(isolate, "duration"),
-                  GetPerformanceEntryDuration);
+  ot->SetAccessorProperty(env->name_string(),
+                          get_performance_entry_name_templ,
+                          Local<FunctionTemplate>());
+
+  ot->SetAccessorProperty(FIXED_ONE_BYTE_STRING(isolate, "entryType"),
+                          get_performance_entry_type_templ,
+                          Local<FunctionTemplate>());
+
+  ot->SetAccessorProperty(FIXED_ONE_BYTE_STRING(isolate, "startTime"),
+                          get_performance_entry_start_time_templ,
+                          Local<FunctionTemplate>());
+
+  ot->SetAccessorProperty(FIXED_ONE_BYTE_STRING(isolate, "duration"),
+                          get_performance_entry_duration_templ,
+                          Local<FunctionTemplate>());
+
   Local<Function> fn = pe->GetFunction();
   target->Set(performanceEntryString, fn);
   env->set_performance_entry_template(fn);
diff --git a/src/stream_base-inl.h b/src/stream_base-inl.h
index 901e07c1e96a25..cdcff67cc55e66 100644
--- a/src/stream_base-inl.h
+++ b/src/stream_base-inl.h
@@ -11,7 +11,7 @@
 
 namespace node {
 
-using v8::AccessorSignature;
+using v8::Signature;
 using v8::External;
 using v8::FunctionCallbackInfo;
 using v8::FunctionTemplate;
@@ -34,31 +34,41 @@ void StreamBase::AddMethods(Environment* env,
   enum PropertyAttribute attributes =
       static_cast<PropertyAttribute>(
           v8::ReadOnly | v8::DontDelete | v8::DontEnum);
-  Local<AccessorSignature> signature =
-      AccessorSignature::New(env->isolate(), t);
-  t->PrototypeTemplate()->SetAccessor(env->fd_string(),
-                                      GetFD<Base>,
-                                      nullptr,
-                                      env->as_external(),
-                                      v8::DEFAULT,
-                                      attributes,
-                                      signature);
-
-  t->PrototypeTemplate()->SetAccessor(env->external_stream_string(),
-                                      GetExternal<Base>,
-                                      nullptr,
-                                      env->as_external(),
-                                      v8::DEFAULT,
-                                      attributes,
-                                      signature);
-
-  t->PrototypeTemplate()->SetAccessor(env->bytes_read_string(),
-                                      GetBytesRead<Base>,
-                                      nullptr,
-                                      env->as_external(),
-                                      v8::DEFAULT,
-                                      attributes,
-                                      signature);
+
+  Local<Signature> signature = Signature::New(env->isolate(), t);
+
+  Local<FunctionTemplate> get_fd_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetFD<Base>,
+                            env->as_external(),
+                            signature);
+
+  Local<FunctionTemplate> get_external_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetExternal<Base>,
+                            env->as_external(),
+                            signature);
+
+  Local<FunctionTemplate> get_bytes_read_templ =
+      FunctionTemplate::New(env->isolate(),
+                            GetBytesRead<Base>,
+                            env->as_external(),
+                            signature);
+
+  t->PrototypeTemplate()->SetAccessorProperty(env->fd_string(),
+                                              get_fd_templ,
+                                              Local<FunctionTemplate>(),
+                                              attributes);
+
+  t->PrototypeTemplate()->SetAccessorProperty(env->external_stream_string(),
+                                              get_external_templ,
+                                              Local<FunctionTemplate>(),
+                                              attributes);
+
+  t->PrototypeTemplate()->SetAccessorProperty(env->bytes_read_string(),
+                                              get_bytes_read_templ,
+                                              Local<FunctionTemplate>(),
+                                              attributes);
 
   env->SetProtoMethod(t, "readStart", JSMethod<Base, &StreamBase::ReadStart>);
   env->SetProtoMethod(t, "readStop", JSMethod<Base, &StreamBase::ReadStop>);
@@ -85,8 +95,7 @@ void StreamBase::AddMethods(Environment* env,
 
 
 template <class Base>
-void StreamBase::GetFD(Local<String> key,
-                       const PropertyCallbackInfo<Value>& args) {
+void StreamBase::GetFD(const FunctionCallbackInfo<Value>& args) {
   // Mimic implementation of StreamBase::GetFD() and UDPWrap::GetFD().
   Base* handle;
   ASSIGN_OR_RETURN_UNWRAP(&handle,
@@ -100,10 +109,8 @@ void StreamBase::GetFD(Local<String> key,
   args.GetReturnValue().Set(wrap->GetFD());
 }
 
-
 template <class Base>
-void StreamBase::GetBytesRead(Local<String> key,
-                              const PropertyCallbackInfo<Value>& args) {
+void StreamBase::GetBytesRead(const FunctionCallbackInfo<Value>& args) {
   // The handle instance hasn't been set. So no bytes could have been read.
   Base* handle;
   ASSIGN_OR_RETURN_UNWRAP(&handle,
@@ -115,10 +122,8 @@ void StreamBase::GetBytesRead(Local<String> key,
   args.GetReturnValue().Set(static_cast<double>(wrap->bytes_read_));
 }
 
-
 template <class Base>
-void StreamBase::GetExternal(Local<String> key,
-                             const PropertyCallbackInfo<Value>& args) {
+void StreamBase::GetExternal(const FunctionCallbackInfo<Value>& args) {
   Base* handle;
   ASSIGN_OR_RETURN_UNWRAP(&handle, args.This());
 
diff --git a/src/stream_base.h b/src/stream_base.h
index 926a3132925c01..8c0a302d76dd2d 100644
--- a/src/stream_base.h
+++ b/src/stream_base.h
@@ -269,16 +269,13 @@ class StreamBase : public StreamResource {
   int WriteString(const v8::FunctionCallbackInfo<v8::Value>& args);
 
   template <class Base>
-  static void GetFD(v8::Local<v8::String> key,
-                    const v8::PropertyCallbackInfo<v8::Value>& args);
+  static void GetFD(const v8::FunctionCallbackInfo<v8::Value>& args);
 
   template <class Base>
-  static void GetExternal(v8::Local<v8::String> key,
-                          const v8::PropertyCallbackInfo<v8::Value>& args);
+  static void GetExternal(const v8::FunctionCallbackInfo<v8::Value>& args);
 
   template <class Base>
-  static void GetBytesRead(v8::Local<v8::String> key,
-                           const v8::PropertyCallbackInfo<v8::Value>& args);
+  static void GetBytesRead(const v8::FunctionCallbackInfo<v8::Value>& args);
 
   template <class Base,
             int (StreamBase::*Method)(
diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc
index 784b36fce95b21..5b3b7f9abc9bad 100644
--- a/src/udp_wrap.cc
+++ b/src/udp_wrap.cc
@@ -42,7 +42,7 @@ using v8::Integer;
 using v8::Local;
 using v8::Object;
 using v8::PropertyAttribute;
-using v8::PropertyCallbackInfo;
+using v8::Signature;
 using v8::String;
 using v8::Uint32;
 using v8::Undefined;
@@ -111,12 +111,19 @@ void UDPWrap::Initialize(Local<Object> target,
 
   enum PropertyAttribute attributes =
       static_cast<PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
-  t->PrototypeTemplate()->SetAccessor(env->fd_string(),
-                                      UDPWrap::GetFD,
-                                      nullptr,
-                                      env->as_external(),
-                                      v8::DEFAULT,
-                                      attributes);
+
+  Local<Signature> signature = Signature::New(env->isolate(), t);
+
+  Local<FunctionTemplate> get_fd_templ =
+      FunctionTemplate::New(env->isolate(),
+                            UDPWrap::GetFD,
+                            env->as_external(),
+                            signature);
+
+  t->PrototypeTemplate()->SetAccessorProperty(env->fd_string(),
+                                              get_fd_templ,
+                                              Local<FunctionTemplate>(),
+                                              attributes);
 
   env->SetProtoMethod(t, "bind", Bind);
   env->SetProtoMethod(t, "send", Send);
@@ -164,7 +171,7 @@ void UDPWrap::New(const FunctionCallbackInfo<Value>& args) {
 }
 
 
-void UDPWrap::GetFD(Local<String>, const PropertyCallbackInfo<Value>& args) {
+void UDPWrap::GetFD(const FunctionCallbackInfo<Value>& args) {
   int fd = UV_EBADF;
 #if !defined(_WIN32)
   UDPWrap* wrap = Unwrap<UDPWrap>(args.This());
diff --git a/src/udp_wrap.h b/src/udp_wrap.h
index f4cf3ad7f566df..15d46b3ebb8e90 100644
--- a/src/udp_wrap.h
+++ b/src/udp_wrap.h
@@ -41,8 +41,7 @@ class UDPWrap: public HandleWrap {
   static void Initialize(v8::Local<v8::Object> target,
                          v8::Local<v8::Value> unused,
                          v8::Local<v8::Context> context);
-  static void GetFD(v8::Local<v8::String>,
-                    const v8::PropertyCallbackInfo<v8::Value>&);
+  static void GetFD(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Bind(const v8::FunctionCallbackInfo<v8::Value>& args);
   static void Send(const v8::FunctionCallbackInfo<v8::Value>& args);
diff --git a/test/parallel/test-accessor-properties.js b/test/parallel/test-accessor-properties.js
new file mode 100644
index 00000000000000..478b1c55e93fdb
--- /dev/null
+++ b/test/parallel/test-accessor-properties.js
@@ -0,0 +1,77 @@
+'use strict';
+
+require('../common');
+
+// This tests that the accessor properties do not raise assertions
+// when called with incompatible receivers.
+
+const assert = require('assert');
+
+// Objects that call StreamBase::AddMethods, when setting up
+// their prototype
+const TTY = process.binding('tty_wrap').TTY;
+const UDP = process.binding('udp_wrap').UDP;
+
+// There are accessor properties in crypto too
+const crypto = process.binding('crypto');
+
+{
+  // Should throw instead of raise assertions
+  assert.throws(() => {
+    TTY.prototype.bytesRead;
+  }, TypeError);
+
+  assert.throws(() => {
+    TTY.prototype.fd;
+  }, TypeError);
+
+  assert.throws(() => {
+    TTY.prototype._externalStream;
+  }, TypeError);
+
+  assert.throws(() => {
+    UDP.prototype.fd;
+  }, TypeError);
+
+  assert.throws(() => {
+    crypto.SecureContext.prototype._external;
+  }, TypeError);
+
+  assert.throws(() => {
+    crypto.Connection.prototype._external;
+  }, TypeError);
+
+
+  // Should not throw for Object.getOwnPropertyDescriptor
+  assert.strictEqual(
+    typeof Object.getOwnPropertyDescriptor(TTY.prototype, 'bytesRead'),
+    'object'
+  );
+
+  assert.strictEqual(
+    typeof Object.getOwnPropertyDescriptor(TTY.prototype, 'fd'),
+    'object'
+  );
+
+  assert.strictEqual(
+    typeof Object.getOwnPropertyDescriptor(TTY.prototype, '_externalStream'),
+    'object'
+  );
+
+  assert.strictEqual(
+    typeof Object.getOwnPropertyDescriptor(UDP.prototype, 'fd'),
+    'object'
+  );
+
+  assert.strictEqual(
+    typeof Object.getOwnPropertyDescriptor(
+      crypto.SecureContext.prototype, '_external'),
+    'object'
+  );
+
+  assert.strictEqual(
+    typeof Object.getOwnPropertyDescriptor(
+      crypto.Connection.prototype, '_external'),
+    'object'
+  );
+}
diff --git a/test/parallel/test-stream-base-prototype-accessors.js b/test/parallel/test-stream-base-prototype-accessors.js
deleted file mode 100644
index f9e12582a098d8..00000000000000
--- a/test/parallel/test-stream-base-prototype-accessors.js
+++ /dev/null
@@ -1,27 +0,0 @@
-'use strict';
-
-require('../common');
-
-// This tests that the prototype accessors added by StreamBase::AddMethods
-// do not raise assersions when called with incompatible receivers.
-
-const assert = require('assert');
-
-// Or anything that calls StreamBase::AddMethods when setting up its prototype
-const TTY = process.binding('tty_wrap').TTY;
-
-// Should throw instead of raise assertions
-{
-  const msg = /TypeError: Method \w+ called on incompatible receiver/;
-  assert.throws(() => {
-    TTY.prototype.bytesRead;
-  }, msg);
-
-  assert.throws(() => {
-    TTY.prototype.fd;
-  }, msg);
-
-  assert.throws(() => {
-    TTY.prototype._externalStream;
-  }, msg);
-}

From c4572ccf2802dd7d2fe92113ea47b4c047429ca3 Mon Sep 17 00:00:00 2001
From: Daniel Bevenius <daniel.bevenius@gmail.com>
Date: Tue, 26 Dec 2017 09:58:43 +0100
Subject: [PATCH 03/77] test: add hasCrypto when using binding('crypto')

Currently, when configured --without-ssl tests that use
process.binding('crypto') fail with the following error:

=== release test-accessor-properties ===
Path: parallel/test-accessor-properties
node/test/parallel/test-accessor-properties.js:16
const crypto = process.binding('crypto');
                       ^

Error: No such module: crypto
    at Object.<anonymous> (test-accessor-properties.js:16:24)
    at Module._compile (module.js:660:30)
    at Object.Module._extensions..js (module.js:671:10)
    at Module.load (module.js:577:32)
    at tryModuleLoad (module.js:517:12)
    at Function.Module._load (module.js:509:3)
    at Function.Module.runMain (module.js:701:10)
    at startup (bootstrap_node.js:194:16)
    at bootstrap_node.js:645:3

This commit adds a hasCrypto check.

PR-URL: https://github.com/nodejs/node/pull/17867
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 test/parallel/test-accessor-properties.js | 49 ++++++++++++-----------
 1 file changed, 25 insertions(+), 24 deletions(-)

diff --git a/test/parallel/test-accessor-properties.js b/test/parallel/test-accessor-properties.js
index 478b1c55e93fdb..13535ceda9667f 100644
--- a/test/parallel/test-accessor-properties.js
+++ b/test/parallel/test-accessor-properties.js
@@ -1,6 +1,6 @@
 'use strict';
 
-require('../common');
+const common = require('../common');
 
 // This tests that the accessor properties do not raise assertions
 // when called with incompatible receivers.
@@ -12,9 +12,6 @@ const assert = require('assert');
 const TTY = process.binding('tty_wrap').TTY;
 const UDP = process.binding('udp_wrap').UDP;
 
-// There are accessor properties in crypto too
-const crypto = process.binding('crypto');
-
 {
   // Should throw instead of raise assertions
   assert.throws(() => {
@@ -33,15 +30,6 @@ const crypto = process.binding('crypto');
     UDP.prototype.fd;
   }, TypeError);
 
-  assert.throws(() => {
-    crypto.SecureContext.prototype._external;
-  }, TypeError);
-
-  assert.throws(() => {
-    crypto.Connection.prototype._external;
-  }, TypeError);
-
-
   // Should not throw for Object.getOwnPropertyDescriptor
   assert.strictEqual(
     typeof Object.getOwnPropertyDescriptor(TTY.prototype, 'bytesRead'),
@@ -63,15 +51,28 @@ const crypto = process.binding('crypto');
     'object'
   );
 
-  assert.strictEqual(
-    typeof Object.getOwnPropertyDescriptor(
-      crypto.SecureContext.prototype, '_external'),
-    'object'
-  );
-
-  assert.strictEqual(
-    typeof Object.getOwnPropertyDescriptor(
-      crypto.Connection.prototype, '_external'),
-    'object'
-  );
+  if (common.hasCrypto) { // eslint-disable-line crypto-check
+    // There are accessor properties in crypto too
+    const crypto = process.binding('crypto');
+
+    assert.throws(() => {
+      crypto.SecureContext.prototype._external;
+    }, TypeError);
+
+    assert.throws(() => {
+      crypto.Connection.prototype._external;
+    }, TypeError);
+
+    assert.strictEqual(
+      typeof Object.getOwnPropertyDescriptor(
+        crypto.SecureContext.prototype, '_external'),
+      'object'
+    );
+
+    assert.strictEqual(
+      typeof Object.getOwnPropertyDescriptor(
+        crypto.Connection.prototype, '_external'),
+      'object'
+    );
+  }
 }

From 9557fb99fe4d5d71f5cdc03870209a73d9dad629 Mon Sep 17 00:00:00 2001
From: Rich Trott <rtrott@gmail.com>
Date: Tue, 17 Oct 2017 16:59:30 -0700
Subject: [PATCH 04/77] test: make test-tls-external-accessor agnostic
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Remove reliance on V8-specific error messages in
test/parallel/test-tls-external-accessor.js.

Check that the error is a `TypeError`.

The test should now be successful without modification using ChakraCore.

PR-URL: https://github.com/nodejs/node/pull/16272
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Yuta Hiroto <hello@about-hiroppy.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
---
 test/parallel/test-tls-external-accessor.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/parallel/test-tls-external-accessor.js b/test/parallel/test-tls-external-accessor.js
index 2d7b1f62b98977..33d371923a600c 100644
--- a/test/parallel/test-tls-external-accessor.js
+++ b/test/parallel/test-tls-external-accessor.js
@@ -11,12 +11,12 @@ const tls = require('tls');
 {
   const pctx = tls.createSecureContext().context;
   const cctx = Object.create(pctx);
-  assert.throws(() => cctx._external, /incompatible receiver/);
+  assert.throws(() => cctx._external, TypeError);
   pctx._external;
 }
 {
   const pctx = tls.createSecurePair().credentials.context;
   const cctx = Object.create(pctx);
-  assert.throws(() => cctx._external, /incompatible receiver/);
+  assert.throws(() => cctx._external, TypeError);
   pctx._external;
 }

From d5ddbd926bb4cfa3bbf4ebfb33923065ccb30016 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Thu, 21 Dec 2017 14:29:05 -0800
Subject: [PATCH 05/77] perf_hooks: refactor internals

Refactor and simplify the perf_hooks native internals.

PR-URL: https://github.com/nodejs/node/pull/17822
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 src/node_perf.cc | 258 ++++++++++++++++++++---------------------------
 src/node_perf.h  | 134 ++++++++----------------
 2 files changed, 150 insertions(+), 242 deletions(-)

diff --git a/src/node_perf.cc b/src/node_perf.cc
index fc793a9c8c5b6a..c13aea2317a110 100644
--- a/src/node_perf.cc
+++ b/src/node_perf.cc
@@ -17,9 +17,8 @@ using v8::Integer;
 using v8::Isolate;
 using v8::Local;
 using v8::Name;
+using v8::Number;
 using v8::Object;
-using v8::ObjectTemplate;
-using v8::Signature;
 using v8::String;
 using v8::Value;
 
@@ -30,37 +29,79 @@ uint64_t performance_v8_start;
 uint64_t performance_last_gc_start_mark_ = 0;
 v8::GCType performance_last_gc_type_ = v8::GCType::kGCTypeAll;
 
+// Initialize the performance entry object properties
+inline void InitObject(const PerformanceEntry& entry, Local<Object> obj) {
+  Environment* env = entry.env();
+  Isolate* isolate = env->isolate();
+  Local<Context> context = env->context();
+  v8::PropertyAttribute attr =
+      static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
+  obj->DefineOwnProperty(context,
+                         env->name_string(),
+                         String::NewFromUtf8(isolate,
+                                             entry.name().c_str(),
+                                             String::kNormalString),
+                         attr).FromJust();
+  obj->DefineOwnProperty(context,
+                         FIXED_ONE_BYTE_STRING(isolate, "entryType"),
+                         String::NewFromUtf8(isolate,
+                                             entry.type().c_str(),
+                                             String::kNormalString),
+                         attr).FromJust();
+  obj->DefineOwnProperty(context,
+                         FIXED_ONE_BYTE_STRING(isolate, "startTime"),
+                         Number::New(isolate, entry.startTime()),
+                         attr).FromJust();
+  obj->DefineOwnProperty(context,
+                         FIXED_ONE_BYTE_STRING(isolate, "duration"),
+                         Number::New(isolate, entry.duration()),
+                         attr).FromJust();
+}
+
+// Create a new PerformanceEntry object
+const Local<Object> PerformanceEntry::ToObject() const {
+  Local<Object> obj =
+      env_->performance_entry_template()
+          ->NewInstance(env_->context()).ToLocalChecked();
+  InitObject(*this, obj);
+  return obj;
+}
+
+// Allow creating a PerformanceEntry object from JavaScript
 void PerformanceEntry::New(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Isolate* isolate = env->isolate();
   Utf8Value name(isolate, args[0]);
   Utf8Value type(isolate, args[1]);
   uint64_t now = PERFORMANCE_NOW();
-  new PerformanceEntry(env, args.This(), *name, *type, now, now);
+  PerformanceEntry entry(env, *name, *type, now, now);
+  Local<Object> obj = args.This();
+  InitObject(entry, obj);
+  PerformanceEntry::Notify(env, entry.kind(), obj);
 }
 
-void PerformanceEntry::NotifyObservers(Environment* env,
-                                       PerformanceEntry* entry) {
+// Pass the PerformanceEntry object to the PerformanceObservers
+inline void PerformanceEntry::Notify(Environment* env,
+                                     PerformanceEntryType type,
+                                     Local<Value> object) {
+  Context::Scope scope(env->context());
   AliasedBuffer<uint32_t, v8::Uint32Array>& observers =
       env->performance_state()->observers;
-  PerformanceEntryType type = ToPerformanceEntryTypeEnum(entry->type().c_str());
-  if (type == NODE_PERFORMANCE_ENTRY_TYPE_INVALID ||
-      !observers[type]) {
-    return;
+  if (observers != nullptr &&
+      type != NODE_PERFORMANCE_ENTRY_TYPE_INVALID &&
+      observers[type]) {
+    node::MakeCallback(env->isolate(),
+                       env->process_object(),
+                       env->performance_entry_callback(),
+                       1, &object);
   }
-  Local<Context> context = env->context();
-  Isolate* isolate = env->isolate();
-  Local<Value> argv = entry->object();
-  env->performance_entry_callback()->Call(context,
-                                          v8::Undefined(isolate),
-                                          1, &argv).ToLocalChecked();
 }
 
+// Create a User Timing Mark
 void Mark(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
-  Local<Context> context = env->context();
-  Isolate* isolate = env->isolate();
-  Utf8Value name(isolate, args[0]);
+  HandleScope scope(env->isolate());
+  Utf8Value name(env->isolate(), args[0]);
   uint64_t now = PERFORMANCE_NOW();
   auto marks = env->performance_marks();
   (*marks)[*name] = now;
@@ -68,25 +109,27 @@ void Mark(const FunctionCallbackInfo<Value>& args) {
   // TODO(jasnell): Once Tracing API is fully implemented, this should
   // record a trace event also.
 
-  Local<Function> fn = env->performance_entry_template();
-  Local<Object> obj = fn->NewInstance(context).ToLocalChecked();
-  new PerformanceEntry(env, obj, *name, "mark", now, now);
+  PerformanceEntry entry(env, *name, "mark", now, now);
+  Local<Object> obj = entry.ToObject();
+  PerformanceEntry::Notify(env, entry.kind(), obj);
   args.GetReturnValue().Set(obj);
 }
 
+
 inline uint64_t GetPerformanceMark(Environment* env, std::string name) {
   auto marks = env->performance_marks();
   auto res = marks->find(name);
   return res != marks->end() ? res->second : 0;
 }
 
+// Create a User Timing Measure. A Measure is a PerformanceEntry that
+// measures the duration between two distinct user timing marks
 void Measure(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
-  Local<Context> context = env->context();
-  Isolate* isolate = env->isolate();
-  Utf8Value name(isolate, args[0]);
-  Utf8Value startMark(isolate, args[1]);
-  Utf8Value endMark(isolate, args[2]);
+  HandleScope scope(env->isolate());
+  Utf8Value name(env->isolate(), args[0]);
+  Utf8Value startMark(env->isolate(), args[1]);
+  Utf8Value endMark(env->isolate(), args[2]);
 
   AliasedBuffer<double, v8::Float64Array>& milestones =
       env->performance_state()->milestones;
@@ -114,41 +157,13 @@ void Measure(const FunctionCallbackInfo<Value>& args) {
   // TODO(jasnell): Once Tracing API is fully implemented, this should
   // record a trace event also.
 
-  Local<Function> fn = env->performance_entry_template();
-  Local<Object> obj = fn->NewInstance(context).ToLocalChecked();
-  new PerformanceEntry(env, obj, *name, "measure",
-                       startTimestamp, endTimestamp);
+  PerformanceEntry entry(env, *name, "measure", startTimestamp, endTimestamp);
+  Local<Object> obj = entry.ToObject();
+  PerformanceEntry::Notify(env, entry.kind(), obj);
   args.GetReturnValue().Set(obj);
 }
 
-void GetPerformanceEntryName(const FunctionCallbackInfo<Value>& info) {
-  Isolate* isolate = info.GetIsolate();
-  PerformanceEntry* entry;
-  ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
-  info.GetReturnValue().Set(
-    String::NewFromUtf8(isolate, entry->name().c_str(), String::kNormalString));
-}
-
-void GetPerformanceEntryType(const FunctionCallbackInfo<Value>& info) {
-  Isolate* isolate = info.GetIsolate();
-  PerformanceEntry* entry;
-  ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
-  info.GetReturnValue().Set(
-    String::NewFromUtf8(isolate, entry->type().c_str(), String::kNormalString));
-}
-
-void GetPerformanceEntryStartTime(const FunctionCallbackInfo<Value>& info) {
-  PerformanceEntry* entry;
-  ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
-  info.GetReturnValue().Set(entry->startTime());
-}
-
-void GetPerformanceEntryDuration(const FunctionCallbackInfo<Value>& info) {
-  PerformanceEntry* entry;
-  ASSIGN_OR_RETURN_UNWRAP(&entry, info.Holder());
-  info.GetReturnValue().Set(entry->duration());
-}
-
+// Allows specific Node.js lifecycle milestones to be set from JavaScript
 void MarkMilestone(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -162,45 +177,36 @@ void MarkMilestone(const FunctionCallbackInfo<Value>& args) {
   }
 }
 
+
 void SetupPerformanceObservers(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   CHECK(args[0]->IsFunction());
   env->set_performance_entry_callback(args[0].As<Function>());
 }
 
-void PerformanceGCCallback(uv_async_t* handle) {
-  PerformanceEntry::Data* data =
-      static_cast<PerformanceEntry::Data*>(handle->data);
-  Environment* env = data->env();
-  Isolate* isolate = env->isolate();
-  HandleScope scope(isolate);
+// Creates a GC Performance Entry and passes it to observers
+void PerformanceGCCallback(Environment* env, void* ptr) {
+  GCPerformanceEntry* entry = static_cast<GCPerformanceEntry*>(ptr);
+  HandleScope scope(env->isolate());
   Local<Context> context = env->context();
-  Context::Scope context_scope(context);
-  Local<Function> fn;
-  Local<Object> obj;
-  PerformanceGCKind kind = static_cast<PerformanceGCKind>(data->data());
 
   AliasedBuffer<uint32_t, v8::Uint32Array>& observers =
       env->performance_state()->observers;
-  if (!observers[NODE_PERFORMANCE_ENTRY_TYPE_GC]) {
-    goto cleanup;
+  if (observers[NODE_PERFORMANCE_ENTRY_TYPE_GC]) {
+    Local<Object> obj = entry->ToObject();
+    v8::PropertyAttribute attr =
+        static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
+    obj->DefineOwnProperty(context,
+                           FIXED_ONE_BYTE_STRING(env->isolate(), "kind"),
+                           Integer::New(env->isolate(), entry->gckind()),
+                           attr).FromJust();
+    PerformanceEntry::Notify(env, entry->kind(), obj);
   }
 
-  fn = env->performance_entry_template();
-  obj = fn->NewInstance(context).ToLocalChecked();
-  obj->Set(context,
-           FIXED_ONE_BYTE_STRING(isolate, "kind"),
-           Integer::New(isolate, kind)).FromJust();
-  new PerformanceEntry(env, obj, data);
-
- cleanup:
-  delete data;
-  auto closeCB = [](uv_handle_t* handle) {
-    delete reinterpret_cast<uv_async_t*>(handle);
-  };
-  uv_close(reinterpret_cast<uv_handle_t*>(handle), closeCB);
+  delete entry;
 }
 
+// Marks the start of a GC cycle
 void MarkGarbageCollectionStart(Isolate* isolate,
                                 v8::GCType type,
                                 v8::GCCallbackFlags flags) {
@@ -208,28 +214,27 @@ void MarkGarbageCollectionStart(Isolate* isolate,
   performance_last_gc_type_ = type;
 }
 
+// Marks the end of a GC cycle
 void MarkGarbageCollectionEnd(Isolate* isolate,
                               v8::GCType type,
                               v8::GCCallbackFlags flags,
                               void* data) {
   Environment* env = static_cast<Environment*>(data);
-  uv_async_t* async = new uv_async_t();  // coverity[leaked_storage]
-  if (uv_async_init(env->event_loop(), async, PerformanceGCCallback))
-    return delete async;
-  uv_unref(reinterpret_cast<uv_handle_t*>(async));
-  async->data =
-      new PerformanceEntry::Data(env, "gc", "gc",
-                                 performance_last_gc_start_mark_,
-                                 PERFORMANCE_NOW(), type);
-  CHECK_EQ(0, uv_async_send(async));
+  env->SetImmediate(PerformanceGCCallback,
+                    new GCPerformanceEntry(env,
+                                           static_cast<PerformanceGCKind>(type),
+                                           performance_last_gc_start_mark_,
+                                           PERFORMANCE_NOW()));
 }
 
+
 inline void SetupGarbageCollectionTracking(Environment* env) {
   env->isolate()->AddGCPrologueCallback(MarkGarbageCollectionStart);
   env->isolate()->AddGCEpilogueCallback(MarkGarbageCollectionEnd,
                                         static_cast<void*>(env));
 }
 
+// Gets the name of a function
 inline Local<Value> GetName(Local<Function> fn) {
   Local<Value> val = fn->GetDebugName();
   if (val.IsEmpty() || val->IsUndefined()) {
@@ -241,6 +246,9 @@ inline Local<Value> GetName(Local<Function> fn) {
   return val;
 }
 
+// Executes a wrapped Function and captures timing information, causing a
+// Function PerformanceEntry to be emitted to PerformanceObservers after
+// execution.
 void TimerFunctionCall(const FunctionCallbackInfo<Value>& args) {
   Isolate* isolate = args.GetIsolate();
   HandleScope scope(isolate);
@@ -250,9 +258,8 @@ void TimerFunctionCall(const FunctionCallbackInfo<Value>& args) {
   size_t count = args.Length();
   size_t idx;
   std::vector<Local<Value>> call_args;
-  for (size_t i = 0; i < count; ++i) {
+  for (size_t i = 0; i < count; ++i)
     call_args.push_back(args[i]);
-  }
 
   Utf8Value name(isolate, GetName(fn));
 
@@ -289,15 +296,14 @@ void TimerFunctionCall(const FunctionCallbackInfo<Value>& args) {
   if (!observers[NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION])
     return;
 
-  Local<Function> ctor = env->performance_entry_template();
-  v8::MaybeLocal<Object> instance = ctor->NewInstance(context);
-  Local<Object> obj = instance.ToLocalChecked();
-  for (idx = 0; idx < count; idx++) {
-    obj->Set(context, idx, args[idx]).ToChecked();
-  }
-  new PerformanceEntry(env, obj, *name, "function", start, end);
+  PerformanceEntry entry(env, *name, "function", start, end);
+  Local<Object> obj = entry.ToObject();
+  for (idx = 0; idx < count; idx++)
+    obj->Set(context, idx, args[idx]).FromJust();
+  PerformanceEntry::Notify(env, entry.kind(), obj);
 }
 
+// Wraps a Function in a TimerFunctionCall
 void Timerify(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -310,6 +316,7 @@ void Timerify(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(wrap);
 }
 
+
 void Init(Local<Object> target,
           Local<Value> unused,
           Local<Context> context) {
@@ -328,55 +335,10 @@ void Init(Local<Object> target,
   Local<String> performanceEntryString =
       FIXED_ONE_BYTE_STRING(isolate, "PerformanceEntry");
 
-  Local<FunctionTemplate> pe = env->NewFunctionTemplate(PerformanceEntry::New);
-  pe->InstanceTemplate()->SetInternalFieldCount(1);
+  Local<FunctionTemplate> pe = FunctionTemplate::New(isolate);
   pe->SetClassName(performanceEntryString);
-
-  Local<Signature> signature = Signature::New(env->isolate(), pe);
-
-  Local<FunctionTemplate> get_performance_entry_name_templ =
-      FunctionTemplate::New(env->isolate(),
-                            GetPerformanceEntryName,
-                            env->as_external(),
-                            signature);
-
-  Local<FunctionTemplate> get_performance_entry_type_templ =
-      FunctionTemplate::New(env->isolate(),
-                            GetPerformanceEntryType,
-                            env->as_external(),
-                            signature);
-
-  Local<FunctionTemplate> get_performance_entry_start_time_templ =
-      FunctionTemplate::New(env->isolate(),
-                            GetPerformanceEntryStartTime,
-                            env->as_external(),
-                            signature);
-
-  Local<FunctionTemplate> get_performance_entry_duration_templ =
-      FunctionTemplate::New(env->isolate(),
-                            GetPerformanceEntryDuration,
-                            env->as_external(),
-                            signature);
-
-  Local<ObjectTemplate> ot = pe->InstanceTemplate();
-  ot->SetAccessorProperty(env->name_string(),
-                          get_performance_entry_name_templ,
-                          Local<FunctionTemplate>());
-
-  ot->SetAccessorProperty(FIXED_ONE_BYTE_STRING(isolate, "entryType"),
-                          get_performance_entry_type_templ,
-                          Local<FunctionTemplate>());
-
-  ot->SetAccessorProperty(FIXED_ONE_BYTE_STRING(isolate, "startTime"),
-                          get_performance_entry_start_time_templ,
-                          Local<FunctionTemplate>());
-
-  ot->SetAccessorProperty(FIXED_ONE_BYTE_STRING(isolate, "duration"),
-                          get_performance_entry_duration_templ,
-                          Local<FunctionTemplate>());
-
   Local<Function> fn = pe->GetFunction();
-  target->Set(performanceEntryString, fn);
+  target->Set(context, performanceEntryString, fn).FromJust();
   env->set_performance_entry_template(fn);
 
   env->SetMethod(target, "mark", Mark);
diff --git a/src/node_perf.h b/src/node_perf.h
index ba7a326471d695..f67917066f2f57 100644
--- a/src/node_perf.h
+++ b/src/node_perf.h
@@ -42,120 +42,51 @@ static inline PerformanceEntryType ToPerformanceEntryTypeEnum(
 NODE_EXTERN inline void MarkPerformanceMilestone(
     Environment* env,
     PerformanceMilestone milestone) {
-    env->performance_state()->milestones[milestone] = PERFORMANCE_NOW();
-  }
+  env->performance_state()->milestones[milestone] = PERFORMANCE_NOW();
+}
 
-class PerformanceEntry : public BaseObject {
+class PerformanceEntry {
  public:
-  // Used for temporary storage of performance entry details when the
-  // object cannot be created immediately.
-  class Data {
-   public:
-    Data(
-      Environment* env,
-      const char* name,
-      const char* type,
-      uint64_t startTime,
-      uint64_t endTime,
-      int data = 0) :
-      env_(env),
-      name_(name),
-      type_(type),
-      startTime_(startTime),
-      endTime_(endTime),
-      data_(data) {}
-
-    Environment* env() const {
-      return env_;
-    }
-
-    const std::string& name() const {
-      return name_;
-    }
-
-    const std::string& type() const {
-      return type_;
-    }
-
-    uint64_t startTime() const {
-      return startTime_;
-    }
-
-    uint64_t endTime() const {
-      return endTime_;
-    }
-
-    int data() const {
-      return data_;
-    }
-
-   private:
-    Environment* const env_;
-    const std::string name_;
-    const std::string type_;
-    const uint64_t startTime_;
-    const uint64_t endTime_;
-    const int data_;
-  };
-
-  static void NotifyObservers(Environment* env, PerformanceEntry* entry);
+  static inline void Notify(Environment* env,
+                            PerformanceEntryType type,
+                            Local<Value> object);
 
   static void New(const FunctionCallbackInfo<Value>& args);
 
   PerformanceEntry(Environment* env,
-                   Local<Object> wrap,
                    const char* name,
                    const char* type,
                    uint64_t startTime,
-                   uint64_t endTime) :
-                   BaseObject(env, wrap),
-                   name_(name),
-                   type_(type),
-                   startTime_(startTime),
-                   endTime_(endTime) {
-    MakeWeak<PerformanceEntry>(this);
-    NotifyObservers(env, this);
-  }
+                   uint64_t endTime) : env_(env),
+                                       name_(name),
+                                       type_(type),
+                                       startTime_(startTime),
+                                       endTime_(endTime) { }
 
-  PerformanceEntry(Environment* env,
-                   Local<Object> wrap,
-                   Data* data) :
-                   BaseObject(env, wrap),
-                   name_(data->name()),
-                   type_(data->type()),
-                   startTime_(data->startTime()),
-                   endTime_(data->endTime()) {
-    MakeWeak<PerformanceEntry>(this);
-    NotifyObservers(env, this);
-  }
+  virtual ~PerformanceEntry() { }
 
-  ~PerformanceEntry() {}
+  virtual const Local<Object> ToObject() const;
 
-  const std::string& name() const {
-    return name_;
-  }
+  Environment* env() const { return env_; }
 
-  const std::string& type() const {
-    return type_;
-  }
+  const std::string& name() const { return name_; }
 
-  double startTime() const {
-    return startTime_ / 1e6;
-  }
+  const std::string& type() const { return type_; }
 
-  double duration() const {
-    return durationNano() / 1e6;
+  PerformanceEntryType kind() {
+    return ToPerformanceEntryTypeEnum(type().c_str());
   }
 
-  uint64_t startTimeNano() const {
-    return startTime_;
-  }
+  double startTime() const { return startTime_ / 1e6; }
 
-  uint64_t durationNano() const {
-    return endTime_ - startTime_;
-  }
+  double duration() const { return durationNano() / 1e6; }
+
+  uint64_t startTimeNano() const { return startTime_; }
+
+  uint64_t durationNano() const { return endTime_ - startTime_; }
 
  private:
+  Environment* env_;
   const std::string name_;
   const std::string type_;
   const uint64_t startTime_;
@@ -169,6 +100,21 @@ enum PerformanceGCKind {
   NODE_PERFORMANCE_GC_WEAKCB = GCType::kGCTypeProcessWeakCallbacks
 };
 
+class GCPerformanceEntry : public PerformanceEntry {
+ public:
+  GCPerformanceEntry(Environment* env,
+                     PerformanceGCKind gckind,
+                     uint64_t startTime,
+                     uint64_t endTime) :
+                         PerformanceEntry(env, "gc", "gc", startTime, endTime),
+                         gckind_(gckind) { }
+
+  PerformanceGCKind gckind() const { return gckind_; }
+
+ private:
+  PerformanceGCKind gckind_;
+};
+
 }  // namespace performance
 }  // namespace node
 

From ed53cb7995aabfaf5ded222ee9446866fb8cbaea Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sun, 17 Dec 2017 18:20:19 +0100
Subject: [PATCH 06/77] http2: simplify onSelectPadding
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

`OnCallbackPadding` on the native side already clamps
the return value into the right range, so there’s not need
to also do that on the JS side.

Also, use `>>> 0` instead of `| 0` to get an uint32, since
the communication with C++ land happens through an Uint32Array.

PR-URL: https://github.com/nodejs/node/pull/17717
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 lib/internal/http2/core.js | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index cce54ef377fe0c..72d43049645b2a 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -353,11 +353,7 @@ function onSelectPadding(fn) {
   return function getPadding() {
     const frameLen = paddingBuffer[PADDING_BUF_FRAME_LENGTH];
     const maxFramePayloadLen = paddingBuffer[PADDING_BUF_MAX_PAYLOAD_LENGTH];
-    paddingBuffer[PADDING_BUF_RETURN_VALUE] =
-        Math.min(maxFramePayloadLen,
-                 Math.max(frameLen,
-                          fn(frameLen,
-                             maxFramePayloadLen) | 0));
+    paddingBuffer[PADDING_BUF_RETURN_VALUE] = fn(frameLen, maxFramePayloadLen);
   };
 }
 

From 14d0bab4437fd8d6967d8f382bf4569bde954fce Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Thu, 28 Dec 2017 15:16:37 -0800
Subject: [PATCH 07/77] deps: update nghttp2 to 1.29.0

PR-URL: https://github.com/nodejs/node/pull/17908
Refs: https://github.com/nodejs/node/issues/17746
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 deps/nghttp2/lib/CMakeLists.txt               |  4 ++
 deps/nghttp2/lib/includes/nghttp2/nghttp2.h   | 63 +++++++++++++++++--
 .../nghttp2/lib/includes/nghttp2/nghttp2ver.h |  4 +-
 deps/nghttp2/lib/nghttp2_buf.h                |  2 +-
 deps/nghttp2/lib/nghttp2_callbacks.c          |  5 ++
 deps/nghttp2/lib/nghttp2_callbacks.h          |  1 +
 deps/nghttp2/lib/nghttp2_frame.h              |  4 +-
 deps/nghttp2/lib/nghttp2_hd.h                 |  6 +-
 deps/nghttp2/lib/nghttp2_helper.c             |  3 +
 deps/nghttp2/lib/nghttp2_outbound_item.h      |  2 +-
 deps/nghttp2/lib/nghttp2_pq.h                 |  6 +-
 deps/nghttp2/lib/nghttp2_queue.h              |  4 +-
 deps/nghttp2/lib/nghttp2_session.c            | 51 ++++++++-------
 deps/nghttp2/lib/nghttp2_session.h            |  6 +-
 deps/nghttp2/lib/nghttp2_stream.c             | 12 ++--
 15 files changed, 128 insertions(+), 45 deletions(-)

diff --git a/deps/nghttp2/lib/CMakeLists.txt b/deps/nghttp2/lib/CMakeLists.txt
index 7ef37ed85cc628..0846d06789a0f1 100644
--- a/deps/nghttp2/lib/CMakeLists.txt
+++ b/deps/nghttp2/lib/CMakeLists.txt
@@ -44,6 +44,10 @@ set_target_properties(nghttp2 PROPERTIES
   VERSION ${LT_VERSION} SOVERSION ${LT_SOVERSION}
   C_VISIBILITY_PRESET hidden
 )
+target_include_directories(nghttp2 INTERFACE
+    "${CMAKE_CURRENT_BINARY_DIR}/includes"
+    "${CMAKE_CURRENT_SOURCE_DIR}/includes"
+    )
 
 if(HAVE_CUNIT)
   # Static library (for unittests because of symbol visibility)
diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
index 5696a2ef633653..13cda9f29e28f5 100644
--- a/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
+++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2.h
@@ -387,6 +387,11 @@ typedef enum {
    * Indicates that a processing was canceled.
    */
   NGHTTP2_ERR_CANCEL = -535,
+  /**
+   * When a local endpoint expects to receive SETTINGS frame, it
+   * receives an other type of frame.
+   */
+  NGHTTP2_ERR_SETTINGS_EXPECTED = -536,
   /**
    * The errors < :enum:`NGHTTP2_ERR_FATAL` mean that the library is
    * under unexpected condition and processing was terminated (e.g.,
@@ -1987,6 +1992,9 @@ typedef ssize_t (*nghttp2_pack_extension_callback)(nghttp2_session *session,
  * of length |len|.  |len| does not include the sentinel NULL
  * character.
  *
+ * This function is deprecated.  The new application should use
+ * :type:`nghttp2_error_callback2`.
+ *
  * The format of error message may change between nghttp2 library
  * versions.  The application should not depend on the particular
  * format.
@@ -2003,6 +2011,33 @@ typedef ssize_t (*nghttp2_pack_extension_callback)(nghttp2_session *session,
 typedef int (*nghttp2_error_callback)(nghttp2_session *session, const char *msg,
                                       size_t len, void *user_data);
 
+/**
+ * @functypedef
+ *
+ * Callback function invoked when library provides the error code, and
+ * message.  This callback is solely for debugging purpose.
+ * |lib_error_code| is one of error code defined in
+ * :enum:`nghttp2_error`.  The |msg| is typically NULL-terminated
+ * string of length |len|, and intended for human consumption.  |len|
+ * does not include the sentinel NULL character.
+ *
+ * The format of error message may change between nghttp2 library
+ * versions.  The application should not depend on the particular
+ * format.
+ *
+ * Normally, application should return 0 from this callback.  If fatal
+ * error occurred while doing something in this callback, application
+ * should return :enum:`NGHTTP2_ERR_CALLBACK_FAILURE`.  In this case,
+ * library will return immediately with return value
+ * :enum:`NGHTTP2_ERR_CALLBACK_FAILURE`.  Currently, if nonzero value
+ * is returned from this callback, they are treated as
+ * :enum:`NGHTTP2_ERR_CALLBACK_FAILURE`, but application should not
+ * rely on this details.
+ */
+typedef int (*nghttp2_error_callback2)(nghttp2_session *session,
+                                       int lib_error_code, const char *msg,
+                                       size_t len, void *user_data);
+
 struct nghttp2_session_callbacks;
 
 /**
@@ -2267,10 +2302,30 @@ nghttp2_session_callbacks_set_on_extension_chunk_recv_callback(
  *
  * Sets callback function invoked when library tells error message to
  * the application.
+ *
+ * This function is deprecated.  The new application should use
+ * `nghttp2_session_callbacks_set_error_callback2()`.
+ *
+ * If both :type:`nghttp2_error_callback` and
+ * :type:`nghttp2_error_callback2` are set, the latter takes
+ * precedence.
  */
 NGHTTP2_EXTERN void nghttp2_session_callbacks_set_error_callback(
     nghttp2_session_callbacks *cbs, nghttp2_error_callback error_callback);
 
+/**
+ * @function
+ *
+ * Sets callback function invoked when library tells error code, and
+ * message to the application.
+ *
+ * If both :type:`nghttp2_error_callback` and
+ * :type:`nghttp2_error_callback2` are set, the latter takes
+ * precedence.
+ */
+NGHTTP2_EXTERN void nghttp2_session_callbacks_set_error_callback2(
+    nghttp2_session_callbacks *cbs, nghttp2_error_callback2 error_callback2);
+
 /**
  * @functypedef
  *
@@ -4702,8 +4757,8 @@ nghttp2_hd_deflate_change_table_size(nghttp2_hd_deflater *deflater,
  *
  * After this function returns, it is safe to delete the |nva|.
  *
- * This function returns 0 if it succeeds, or one of the following
- * negative error codes:
+ * This function returns the number of bytes written to |buf| if it
+ * succeeds, or one of the following negative error codes:
  *
  * :enum:`NGHTTP2_ERR_NOMEM`
  *     Out of memory.
@@ -4734,8 +4789,8 @@ NGHTTP2_EXTERN ssize_t nghttp2_hd_deflate_hd(nghttp2_hd_deflater *deflater,
  *
  * After this function returns, it is safe to delete the |nva|.
  *
- * This function returns 0 if it succeeds, or one of the following
- * negative error codes:
+ * This function returns the number of bytes written to |vec| if it
+ * succeeds, or one of the following negative error codes:
  *
  * :enum:`NGHTTP2_ERR_NOMEM`
  *     Out of memory.
diff --git a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h
index 38c48bf041f1e8..455706a5868b3a 100644
--- a/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h
+++ b/deps/nghttp2/lib/includes/nghttp2/nghttp2ver.h
@@ -29,7 +29,7 @@
  * @macro
  * Version number of the nghttp2 library release
  */
-#define NGHTTP2_VERSION "1.25.0"
+#define NGHTTP2_VERSION "1.29.0"
 
 /**
  * @macro
@@ -37,6 +37,6 @@
  * release. This is a 24 bit number with 8 bits for major number, 8 bits
  * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
  */
-#define NGHTTP2_VERSION_NUM 0x011900
+#define NGHTTP2_VERSION_NUM 0x011d00
 
 #endif /* NGHTTP2VER_H */
diff --git a/deps/nghttp2/lib/nghttp2_buf.h b/deps/nghttp2/lib/nghttp2_buf.h
index 06ab1e4c630cc3..9f484a221acb5f 100644
--- a/deps/nghttp2/lib/nghttp2_buf.h
+++ b/deps/nghttp2/lib/nghttp2_buf.h
@@ -398,7 +398,7 @@ int nghttp2_bufs_advance(nghttp2_bufs *bufs);
 void nghttp2_bufs_seek_last_present(nghttp2_bufs *bufs);
 
 /*
- * Returns nonzero if bufs->cur->next is not emtpy.
+ * Returns nonzero if bufs->cur->next is not empty.
  */
 int nghttp2_bufs_next_present(nghttp2_bufs *bufs);
 
diff --git a/deps/nghttp2/lib/nghttp2_callbacks.c b/deps/nghttp2/lib/nghttp2_callbacks.c
index b6cf5957f01b59..3c38214859b17a 100644
--- a/deps/nghttp2/lib/nghttp2_callbacks.c
+++ b/deps/nghttp2/lib/nghttp2_callbacks.c
@@ -168,3 +168,8 @@ void nghttp2_session_callbacks_set_error_callback(
     nghttp2_session_callbacks *cbs, nghttp2_error_callback error_callback) {
   cbs->error_callback = error_callback;
 }
+
+void nghttp2_session_callbacks_set_error_callback2(
+    nghttp2_session_callbacks *cbs, nghttp2_error_callback2 error_callback2) {
+  cbs->error_callback2 = error_callback2;
+}
diff --git a/deps/nghttp2/lib/nghttp2_callbacks.h b/deps/nghttp2/lib/nghttp2_callbacks.h
index 5967524e0c6493..b607bbb58b8e3d 100644
--- a/deps/nghttp2/lib/nghttp2_callbacks.h
+++ b/deps/nghttp2/lib/nghttp2_callbacks.h
@@ -119,6 +119,7 @@ struct nghttp2_session_callbacks {
   nghttp2_unpack_extension_callback unpack_extension_callback;
   nghttp2_on_extension_chunk_recv_callback on_extension_chunk_recv_callback;
   nghttp2_error_callback error_callback;
+  nghttp2_error_callback2 error_callback2;
 };
 
 #endif /* NGHTTP2_CALLBACKS_H */
diff --git a/deps/nghttp2/lib/nghttp2_frame.h b/deps/nghttp2/lib/nghttp2_frame.h
index 891289f61bf5e7..35ca214a4a7a59 100644
--- a/deps/nghttp2/lib/nghttp2_frame.h
+++ b/deps/nghttp2/lib/nghttp2_frame.h
@@ -70,7 +70,9 @@
 #define NGHTTP2_MAX_PADLEN 256
 
 /* Union of extension frame payload */
-typedef union { nghttp2_ext_altsvc altsvc; } nghttp2_ext_frame_payload;
+typedef union {
+  nghttp2_ext_altsvc altsvc;
+} nghttp2_ext_frame_payload;
 
 void nghttp2_frame_pack_frame_hd(uint8_t *buf, const nghttp2_frame_hd *hd);
 
diff --git a/deps/nghttp2/lib/nghttp2_hd.h b/deps/nghttp2/lib/nghttp2_hd.h
index 458edafe4d5847..760bfbc357efdc 100644
--- a/deps/nghttp2/lib/nghttp2_hd.h
+++ b/deps/nghttp2/lib/nghttp2_hd.h
@@ -211,7 +211,9 @@ typedef struct {
 
 #define HD_MAP_SIZE 128
 
-typedef struct { nghttp2_hd_entry *table[HD_MAP_SIZE]; } nghttp2_hd_map;
+typedef struct {
+  nghttp2_hd_entry *table[HD_MAP_SIZE];
+} nghttp2_hd_map;
 
 struct nghttp2_hd_deflater {
   nghttp2_hd_context ctx;
@@ -313,7 +315,7 @@ void nghttp2_hd_deflate_free(nghttp2_hd_deflater *deflater);
  *
  * This function expands |bufs| as necessary to store the result. If
  * buffers is full and the process still requires more space, this
- * funtion fails and returns NGHTTP2_ERR_HEADER_COMP.
+ * function fails and returns NGHTTP2_ERR_HEADER_COMP.
  *
  * After this function returns, it is safe to delete the |nva|.
  *
diff --git a/deps/nghttp2/lib/nghttp2_helper.c b/deps/nghttp2/lib/nghttp2_helper.c
index b00c9073a92a13..3b282c7301f95b 100644
--- a/deps/nghttp2/lib/nghttp2_helper.c
+++ b/deps/nghttp2/lib/nghttp2_helper.c
@@ -322,6 +322,9 @@ const char *nghttp2_strerror(int error_code) {
     return "Internal error";
   case NGHTTP2_ERR_CANCEL:
     return "Cancel";
+  case NGHTTP2_ERR_SETTINGS_EXPECTED:
+    return "When a local endpoint expects to receive SETTINGS frame, it "
+           "receives an other type of frame";
   case NGHTTP2_ERR_NOMEM:
     return "Out of memory";
   case NGHTTP2_ERR_CALLBACK_FAILURE:
diff --git a/deps/nghttp2/lib/nghttp2_outbound_item.h b/deps/nghttp2/lib/nghttp2_outbound_item.h
index 8bda776bfe2728..89a8a92668dd5c 100644
--- a/deps/nghttp2/lib/nghttp2_outbound_item.h
+++ b/deps/nghttp2/lib/nghttp2_outbound_item.h
@@ -112,7 +112,7 @@ struct nghttp2_outbound_item {
   nghttp2_ext_frame_payload ext_frame_payload;
   nghttp2_aux_data aux_data;
   /* The priority used in priority comparion.  Smaller is served
-     ealier.  For PING, SETTINGS and non-DATA frames (excluding
+     earlier.  For PING, SETTINGS and non-DATA frames (excluding
      response HEADERS frame) have dedicated cycle value defined above.
      For DATA frame, cycle is computed by taking into account of
      effective weight and frame payload length previously sent, so
diff --git a/deps/nghttp2/lib/nghttp2_pq.h b/deps/nghttp2/lib/nghttp2_pq.h
index 1426bef760132c..71cf96a14e0c77 100644
--- a/deps/nghttp2/lib/nghttp2_pq.h
+++ b/deps/nghttp2/lib/nghttp2_pq.h
@@ -35,7 +35,9 @@
 
 /* Implementation of priority queue */
 
-typedef struct { size_t index; } nghttp2_pq_entry;
+typedef struct {
+  size_t index;
+} nghttp2_pq_entry;
 
 typedef struct {
   /* The pointer to the pointer to the item stored */
@@ -71,7 +73,7 @@ void nghttp2_pq_free(nghttp2_pq *pq);
 /*
  * Adds |item| to the priority queue |pq|.
  *
- * This function returns 0 if it succeds, or one of the following
+ * This function returns 0 if it succeeds, or one of the following
  * negative error codes:
  *
  * NGHTTP2_ERR_NOMEM
diff --git a/deps/nghttp2/lib/nghttp2_queue.h b/deps/nghttp2/lib/nghttp2_queue.h
index d872b07bde961c..c7eb753ca92182 100644
--- a/deps/nghttp2/lib/nghttp2_queue.h
+++ b/deps/nghttp2/lib/nghttp2_queue.h
@@ -36,7 +36,9 @@ typedef struct nghttp2_queue_cell {
   struct nghttp2_queue_cell *next;
 } nghttp2_queue_cell;
 
-typedef struct { nghttp2_queue_cell *front, *back; } nghttp2_queue;
+typedef struct {
+  nghttp2_queue_cell *front, *back;
+} nghttp2_queue;
 
 void nghttp2_queue_init(nghttp2_queue *queue);
 void nghttp2_queue_free(nghttp2_queue *queue);
diff --git a/deps/nghttp2/lib/nghttp2_session.c b/deps/nghttp2/lib/nghttp2_session.c
index 4bc94cbb1982ad..b14ed77a25c293 100644
--- a/deps/nghttp2/lib/nghttp2_session.c
+++ b/deps/nghttp2/lib/nghttp2_session.c
@@ -148,14 +148,16 @@ static int check_ext_type_set(const uint8_t *ext_types, uint8_t type) {
 }
 
 static int session_call_error_callback(nghttp2_session *session,
-                                       const char *fmt, ...) {
+                                       int lib_error_code, const char *fmt,
+                                       ...) {
   size_t bufsize;
   va_list ap;
   char *buf;
   int rv;
   nghttp2_mem *mem;
 
-  if (!session->callbacks.error_callback) {
+  if (!session->callbacks.error_callback &&
+      !session->callbacks.error_callback2) {
     return 0;
   }
 
@@ -189,8 +191,13 @@ static int session_call_error_callback(nghttp2_session *session,
     return 0;
   }
 
-  rv = session->callbacks.error_callback(session, buf, (size_t)rv,
-                                         session->user_data);
+  if (session->callbacks.error_callback2) {
+    rv = session->callbacks.error_callback2(session, lib_error_code, buf,
+                                            (size_t)rv, session->user_data);
+  } else {
+    rv = session->callbacks.error_callback(session, buf, (size_t)rv,
+                                           session->user_data);
+  }
 
   nghttp2_mem_free(mem, buf);
 
@@ -541,9 +548,8 @@ static int session_new(nghttp2_session **session_ptr,
   if (nghttp2_enable_strict_preface) {
     nghttp2_inbound_frame *iframe = &(*session_ptr)->iframe;
 
-    if (server &&
-        ((*session_ptr)->opt_flags & NGHTTP2_OPTMASK_NO_RECV_CLIENT_MAGIC) ==
-            0) {
+    if (server && ((*session_ptr)->opt_flags &
+                   NGHTTP2_OPTMASK_NO_RECV_CLIENT_MAGIC) == 0) {
       iframe->state = NGHTTP2_IB_READ_CLIENT_MAGIC;
       iframe->payloadleft = NGHTTP2_CLIENT_MAGIC_LEN;
     } else {
@@ -2183,7 +2189,7 @@ static int session_prep_frame(nghttp2_session *session,
        closed. */
     stream = nghttp2_session_get_stream(session, frame->hd.stream_id);
 
-    /* predicte should fail if stream is NULL. */
+    /* predicate should fail if stream is NULL. */
     rv = session_predicate_push_promise_send(session, stream);
     if (rv != 0) {
       return rv;
@@ -2411,19 +2417,16 @@ static int session_close_stream_on_goaway(nghttp2_session *session,
   nghttp2_stream *stream, *next_stream;
   nghttp2_close_stream_on_goaway_arg arg = {session, NULL, last_stream_id,
                                             incoming};
-  uint32_t error_code;
 
   rv = nghttp2_map_each(&session->streams, find_stream_on_goaway_func, &arg);
   assert(rv == 0);
 
-  error_code =
-      session->server && incoming ? NGHTTP2_REFUSED_STREAM : NGHTTP2_CANCEL;
-
   stream = arg.head;
   while (stream) {
     next_stream = stream->closed_next;
     stream->closed_next = NULL;
-    rv = nghttp2_session_close_stream(session, stream->stream_id, error_code);
+    rv = nghttp2_session_close_stream(session, stream->stream_id,
+                                      NGHTTP2_REFUSED_STREAM);
 
     /* stream may be deleted here */
 
@@ -3608,7 +3611,7 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
                    nv.name->base, (int)nv.value->len, nv.value->base);
 
             rv2 = session_call_error_callback(
-                session,
+                session, NGHTTP2_ERR_HTTP_HEADER,
                 "Ignoring received invalid HTTP header field: frame type: "
                 "%u, stream: %d, name: [%.*s], value: [%.*s]",
                 frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
@@ -3626,8 +3629,9 @@ static int inflate_header_block(nghttp2_session *session, nghttp2_frame *frame,
                  nv.name->base, (int)nv.value->len, nv.value->base);
 
           rv = session_call_error_callback(
-              session, "Invalid HTTP header field was received: frame type: "
-                       "%u, stream: %d, name: [%.*s], value: [%.*s]",
+              session, NGHTTP2_ERR_HTTP_HEADER,
+              "Invalid HTTP header field was received: frame type: "
+              "%u, stream: %d, name: [%.*s], value: [%.*s]",
               frame->hd.type, frame->hd.stream_id, (int)nv.name->len,
               nv.name->base, (int)nv.value->len, nv.value->base);
 
@@ -3781,7 +3785,7 @@ int nghttp2_session_on_request_headers_received(nghttp2_session *session,
         session, frame, NGHTTP2_ERR_PROTO, "request HEADERS: stream_id == 0");
   }
 
-  /* If client recieves idle stream from server, it is invalid
+  /* If client receives idle stream from server, it is invalid
      regardless stream ID is even or odd.  This is because client is
      not expected to receive request from server. */
   if (!session->server) {
@@ -5345,9 +5349,10 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in,
         iframe->state = NGHTTP2_IB_IGN_ALL;
 
         rv = session_call_error_callback(
-            session, "Remote peer returned unexpected data while we expected "
-                     "SETTINGS frame.  Perhaps, peer does not support HTTP/2 "
-                     "properly.");
+            session, NGHTTP2_ERR_SETTINGS_EXPECTED,
+            "Remote peer returned unexpected data while we expected "
+            "SETTINGS frame.  Perhaps, peer does not support HTTP/2 "
+            "properly.");
 
         if (nghttp2_is_fatal(rv)) {
           return rv;
@@ -5588,13 +5593,13 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in,
         if (iframe->payloadleft) {
           nghttp2_settings_entry *min_header_table_size_entry;
 
-          /* We allocate iv with addtional one entry, to store the
+          /* We allocate iv with additional one entry, to store the
              minimum header table size. */
           iframe->max_niv =
               iframe->frame.hd.length / NGHTTP2_FRAME_SETTINGS_ENTRY_LENGTH + 1;
 
-          iframe->iv = nghttp2_mem_malloc(
-              mem, sizeof(nghttp2_settings_entry) * iframe->max_niv);
+          iframe->iv = nghttp2_mem_malloc(mem, sizeof(nghttp2_settings_entry) *
+                                                   iframe->max_niv);
 
           if (!iframe->iv) {
             return NGHTTP2_ERR_NOMEM;
diff --git a/deps/nghttp2/lib/nghttp2_session.h b/deps/nghttp2/lib/nghttp2_session.h
index 3e1467f6a356d7..c7cb27d77c1e25 100644
--- a/deps/nghttp2/lib/nghttp2_session.h
+++ b/deps/nghttp2/lib/nghttp2_session.h
@@ -319,7 +319,7 @@ struct nghttp2_session {
   uint8_t pending_enable_push;
   /* Nonzero if the session is server side. */
   uint8_t server;
-  /* Flags indicating GOAWAY is sent and/or recieved. The flags are
+  /* Flags indicating GOAWAY is sent and/or received. The flags are
      composed by bitwise OR-ing nghttp2_goaway_flag. */
   uint8_t goaway_flags;
   /* This flag is used to reduce excessive queuing of WINDOW_UPDATE to
@@ -722,7 +722,7 @@ int nghttp2_session_on_goaway_received(nghttp2_session *session,
                                        nghttp2_frame *frame);
 
 /*
- * Called when WINDOW_UPDATE is recieved, assuming |frame| is properly
+ * Called when WINDOW_UPDATE is received, assuming |frame| is properly
  * initialized.
  *
  * This function returns 0 if it succeeds, or one of the following
@@ -737,7 +737,7 @@ int nghttp2_session_on_window_update_received(nghttp2_session *session,
                                               nghttp2_frame *frame);
 
 /*
- * Called when ALTSVC is recieved, assuming |frame| is properly
+ * Called when ALTSVC is received, assuming |frame| is properly
  * initialized.
  *
  * This function returns 0 if it succeeds, or one of the following
diff --git a/deps/nghttp2/lib/nghttp2_stream.c b/deps/nghttp2/lib/nghttp2_stream.c
index 8dee6ef660983c..eccd3174ef7bda 100644
--- a/deps/nghttp2/lib/nghttp2_stream.c
+++ b/deps/nghttp2/lib/nghttp2_stream.c
@@ -366,8 +366,9 @@ static void check_queued(nghttp2_stream *stream) {
         }
       }
       if (queued == 0) {
-        fprintf(stderr, "stream(%p)=%d, stream->queued == 1, and "
-                        "!stream_active(), but no descendants is queued\n",
+        fprintf(stderr,
+                "stream(%p)=%d, stream->queued == 1, and "
+                "!stream_active(), but no descendants is queued\n",
                 stream, stream->stream_id);
         assert(0);
       }
@@ -378,9 +379,10 @@ static void check_queued(nghttp2_stream *stream) {
     }
   } else {
     if (stream_active(stream) || !nghttp2_pq_empty(&stream->obq)) {
-      fprintf(stderr, "stream(%p) = %d, stream->queued == 0, but "
-                      "stream_active(stream) == %d and "
-                      "nghttp2_pq_size(&stream->obq) = %zu\n",
+      fprintf(stderr,
+              "stream(%p) = %d, stream->queued == 0, but "
+              "stream_active(stream) == %d and "
+              "nghttp2_pq_size(&stream->obq) = %zu\n",
               stream, stream->stream_id, stream_active(stream),
               nghttp2_pq_size(&stream->obq));
       assert(0);

From 584379f7f140e12cfea921c5ad51f4aa41ed7415 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Tue, 21 Nov 2017 12:38:27 +0100
Subject: [PATCH 08/77] src: add optional keep-alive object to SetImmediate

Adds the possibility to keep a strong persistent reference to
a JS object while a `SetImmediate()` call is in effect.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17183
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 src/env-inl.h | 11 +++++++++--
 src/env.cc    |  2 ++
 src/env.h     |  7 ++++++-
 3 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/src/env-inl.h b/src/env-inl.h
index 956153bb965f44..4b6f147f64778c 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -541,8 +541,15 @@ Environment::scheduled_immediate_count() {
   return scheduled_immediate_count_;
 }
 
-void Environment::SetImmediate(native_immediate_callback cb, void* data) {
-  native_immediate_callbacks_.push_back({ cb, data });
+void Environment::SetImmediate(native_immediate_callback cb,
+                               void* data,
+                               v8::Local<v8::Object> obj) {
+  native_immediate_callbacks_.push_back({
+    cb,
+    data,
+    std::unique_ptr<v8::Persistent<v8::Object>>(
+        obj.IsEmpty() ? nullptr : new v8::Persistent<v8::Object>(isolate_, obj))
+  });
   if (scheduled_immediate_count_[0] == 0)
     ActivateImmediateCheck();
   scheduled_immediate_count_[0] = scheduled_immediate_count_[0] + 1;
diff --git a/src/env.cc b/src/env.cc
index 8c3b43d2102cb1..e105fcd7c57ef1 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -224,6 +224,8 @@ void Environment::RunAndClearNativeImmediates() {
     native_immediate_callbacks_.swap(list);
     for (const auto& cb : list) {
       cb.cb_(this, cb.data_);
+      if (cb.keep_alive_)
+        cb.keep_alive_->Reset();
     }
 
 #ifdef DEBUG
diff --git a/src/env.h b/src/env.h
index 6113e6d2de26ea..0d231ba447da3e 100644
--- a/src/env.h
+++ b/src/env.h
@@ -686,7 +686,11 @@ class Environment {
   bool EmitNapiWarning();
 
   typedef void (*native_immediate_callback)(Environment* env, void* data);
-  inline void SetImmediate(native_immediate_callback cb, void* data);
+  // cb will be called as cb(env, data) on the next event loop iteration.
+  // obj will be kept alive between now and after the callback has run.
+  inline void SetImmediate(native_immediate_callback cb,
+                           void* data,
+                           v8::Local<v8::Object> obj = v8::Local<v8::Object>());
   // This needs to be available for the JS-land setImmediate().
   void ActivateImmediateCheck();
 
@@ -754,6 +758,7 @@ class Environment {
   struct NativeImmediateCallback {
     native_immediate_callback cb_;
     void* data_;
+    std::unique_ptr<v8::Persistent<v8::Object>> keep_alive_;
   };
   std::vector<NativeImmediateCallback> native_immediate_callbacks_;
   void RunAndClearNativeImmediates();

From e725a4ae8f09a3407511d8e4581e81a53706a1d5 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Tue, 21 Nov 2017 12:39:55 +0100
Subject: [PATCH 09/77] http2: don't call into JS from GC

Calling into JS land from GC is not allowed, so delay
the resolution of pending pings when a session is destroyed.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17183
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 src/node_http2.cc | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 0747789786b028..2ed5cbb4a3dc59 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -406,7 +406,11 @@ void Http2Session::Close() {
 
   while (!outstanding_pings_.empty()) {
     Http2Session::Http2Ping* ping = PopPing();
-    ping->Done(false);
+    // Since this method may be called from GC, calling into JS directly
+    // is not allowed.
+    env()->SetImmediate([](Environment* env, void* data) {
+      static_cast<Http2Session::Http2Ping*>(data)->Done(false);
+    }, static_cast<void*>(ping));
   }
 
   Stop();

From 2d389f953a680218ce17d6d4eec80c1731312439 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Tue, 21 Nov 2017 12:42:54 +0100
Subject: [PATCH 10/77] http2: only schedule write when necessary

Introduce an `Http2Scope` class that, when it goes out of scope,
checks whether a write to the network is desired by nghttp2.
If that is the case, schedule a write using `SetImmediate()`
rather than a custom per-session libuv handle.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17183
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 src/node_http2.cc                             | 101 +++++++++++-------
 src/node_http2.h                              |  22 +++-
 ...-http2-session-gc-while-write-scheduled.js |  32 ++++++
 3 files changed, 114 insertions(+), 41 deletions(-)
 create mode 100644 test/parallel/test-http2-session-gc-while-write-scheduled.js

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 2ed5cbb4a3dc59..f8b530c20a16cf 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -27,6 +27,26 @@ const Http2Session::Callbacks Http2Session::callback_struct_saved[2] = {
     Callbacks(false),
     Callbacks(true)};
 
+Http2Scope::Http2Scope(Http2Stream* stream) : Http2Scope(stream->session()) {}
+
+Http2Scope::Http2Scope(Http2Session* session) {
+  if (session->flags_ & (SESSION_STATE_HAS_SCOPE |
+                         SESSION_STATE_WRITE_SCHEDULED)) {
+    // There is another scope further below on the stack, or it is already
+    // known that a write is scheduled. In either case, there is nothing to do.
+    return;
+  }
+  session->flags_ |= SESSION_STATE_HAS_SCOPE;
+  session_ = session;
+}
+
+Http2Scope::~Http2Scope() {
+  if (session_ == nullptr)
+    return;
+
+  session_->flags_ &= ~SESSION_STATE_HAS_SCOPE;
+  session_->MaybeScheduleWrite();
+}
 
 Http2Options::Http2Options(Environment* env) {
   nghttp2_option_new(&options_);
@@ -346,8 +366,6 @@ Http2Session::Http2Session(Environment* env,
   // be catching before it gets this far. Either way, crash if this
   // fails.
   CHECK_EQ(fn(&session_, callbacks, this, *opts), 0);
-
-  Start();
 }
 
 
@@ -356,40 +374,6 @@ Http2Session::~Http2Session() {
   Close();
 }
 
-// For every node::Http2Session instance, there is a uv_prepare_t handle
-// whose callback is triggered on every tick of the event loop. When
-// run, nghttp2 is prompted to send any queued data it may have stored.
-// TODO(jasnell): Currently, this creates one uv_prepare_t per Http2Session,
-//                we should investigate to see if it's faster to create a
-//                single uv_prepare_t for all Http2Sessions, then iterate
-//                over each.
-void Http2Session::Start() {
-  prep_ = new uv_prepare_t();
-  uv_prepare_init(env()->event_loop(), prep_);
-  prep_->data = static_cast<void*>(this);
-  uv_prepare_start(prep_, [](uv_prepare_t* t) {
-    Http2Session* session = static_cast<Http2Session*>(t->data);
-    HandleScope scope(session->env()->isolate());
-    Context::Scope context_scope(session->env()->context());
-
-    // Sending data may call arbitrary JS code, so keep track of
-    // async context.
-    InternalCallbackScope callback_scope(session);
-    session->SendPendingData();
-  });
-}
-
-// Stop the uv_prep_t from further activity, destroy the handle
-void Http2Session::Stop() {
-  DEBUG_HTTP2SESSION(this, "stopping uv_prep_t handle");
-  CHECK_EQ(uv_prepare_stop(prep_), 0);
-  auto prep_close = [](uv_handle_t* handle) {
-    delete reinterpret_cast<uv_prepare_t*>(handle);
-  };
-  uv_close(reinterpret_cast<uv_handle_t*>(prep_), prep_close);
-  prep_ = nullptr;
-}
-
 
 void Http2Session::Close() {
   DEBUG_HTTP2SESSION(this, "closing session");
@@ -412,8 +396,6 @@ void Http2Session::Close() {
       static_cast<Http2Session::Http2Ping*>(data)->Done(false);
     }, static_cast<void*>(ping));
   }
-
-  Stop();
 }
 
 
@@ -484,6 +466,7 @@ inline void Http2Session::SubmitShutdownNotice() {
 inline void Http2Session::Settings(const nghttp2_settings_entry iv[],
                                    size_t niv) {
   DEBUG_HTTP2SESSION2(this, "submitting %d settings", niv);
+  Http2Scope h2scope(this);
   // This will fail either if the system is out of memory, or if the settings
   // values are not within the appropriate range. We should be catching the
   // latter before it gets this far so crash in either case.
@@ -736,7 +719,8 @@ Http2Stream::SubmitTrailers::SubmitTrailers(
 
 
 inline void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers,
-                                                 size_t length) const {
+                                                size_t length) const {
+  Http2Scope h2scope(session_);
   if (length == 0)
     return;
   DEBUG_HTTP2SESSION2(session_, "sending trailers for stream %d, count: %d",
@@ -891,14 +875,37 @@ inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
   MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
 }
 
+void Http2Session::MaybeScheduleWrite() {
+  CHECK_EQ(flags_ & SESSION_STATE_WRITE_SCHEDULED, 0);
+  if (session_ != nullptr && nghttp2_session_want_write(session_)) {
+    flags_ |= SESSION_STATE_WRITE_SCHEDULED;
+    env()->SetImmediate([](Environment* env, void* data) {
+      Http2Session* session = static_cast<Http2Session*>(data);
+      if (session->session_ == nullptr ||
+          !(session->flags_ & SESSION_STATE_WRITE_SCHEDULED)) {
+        // This can happen e.g. when a stream was reset before this turn
+        // of the event loop, in which case SendPendingData() is called early,
+        // or the session was destroyed in the meantime.
+        return;
+      }
+
+      // Sending data may call arbitrary JS code, so keep track of
+      // async context.
+      InternalCallbackScope callback_scope(session);
+      session->SendPendingData();
+    }, static_cast<void*>(this), object());
+  }
+}
+
 
-inline void Http2Session::SendPendingData() {
+void Http2Session::SendPendingData() {
   DEBUG_HTTP2SESSION(this, "sending pending data");
   // Do not attempt to send data on the socket if the destroying flag has
   // been set. That means everything is shutting down and the socket
   // will not be usable.
   if (IsDestroying())
     return;
+  flags_ &= ~SESSION_STATE_WRITE_SCHEDULED;
 
   WriteWrap* req = nullptr;
   char* dest = nullptr;
@@ -963,6 +970,7 @@ inline Http2Stream* Http2Session::SubmitRequest(
     int32_t* ret,
     int options) {
   DEBUG_HTTP2SESSION(this, "submitting request");
+  Http2Scope h2scope(this);
   Http2Stream* stream = nullptr;
   Http2Stream::Provider::Stream prov(options);
   *ret = nghttp2_submit_request(session_, prispec, nva, len, *prov, nullptr);
@@ -1019,6 +1027,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
                                     uv_handle_type pending,
                                     void* ctx) {
   Http2Session* session = static_cast<Http2Session*>(ctx);
+  Http2Scope h2scope(session);
   if (nread < 0) {
     uv_buf_t tmp_buf;
     tmp_buf.base = nullptr;
@@ -1184,6 +1193,7 @@ inline void Http2Stream::Close(int32_t code) {
 
 
 inline void Http2Stream::Shutdown() {
+  Http2Scope h2scope(this);
   flags_ |= NGHTTP2_STREAM_FLAG_SHUT;
   CHECK_NE(nghttp2_session_resume_data(session_->session(), id_),
            NGHTTP2_ERR_NOMEM);
@@ -1198,6 +1208,7 @@ int Http2Stream::DoShutdown(ShutdownWrap* req_wrap) {
 }
 
 inline void Http2Stream::Destroy() {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "destroying stream");
   // Do nothing if this stream instance is already destroyed
   if (IsDestroyed())
@@ -1249,6 +1260,7 @@ void Http2Stream::OnDataChunk(
 
 
 inline void Http2Stream::FlushDataChunks() {
+  Http2Scope h2scope(this);
   if (!data_chunks_.empty()) {
     uv_buf_t buf = data_chunks_.front();
     data_chunks_.pop();
@@ -1266,6 +1278,7 @@ inline void Http2Stream::FlushDataChunks() {
 inline int Http2Stream::SubmitResponse(nghttp2_nv* nva,
                                        size_t len,
                                        int options) {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "submitting response");
   if (options & STREAM_OPTION_GET_TRAILERS)
     flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS;
@@ -1286,6 +1299,7 @@ inline int Http2Stream::SubmitFile(int fd,
                                    int64_t offset,
                                    int64_t length,
                                    int options) {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "submitting file");
   if (options & STREAM_OPTION_GET_TRAILERS)
     flags_ |= NGHTTP2_STREAM_FLAG_TRAILERS;
@@ -1302,6 +1316,7 @@ inline int Http2Stream::SubmitFile(int fd,
 
 // Submit informational headers for a stream.
 inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM2(this, "sending %d informational headers", len);
   int ret = nghttp2_submit_headers(session_->session(),
                                    NGHTTP2_FLAG_NONE,
@@ -1314,6 +1329,7 @@ inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
 
 inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
                                        bool silent) {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "sending priority spec");
   int ret = silent ?
       nghttp2_session_change_stream_priority(session_->session(),
@@ -1327,6 +1343,7 @@ inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
 
 
 inline int Http2Stream::SubmitRstStream(const uint32_t code) {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM2(this, "sending rst-stream with code %d", code);
   session_->SendPendingData();
   CHECK_EQ(nghttp2_submit_rst_stream(session_->session(),
@@ -1342,6 +1359,7 @@ inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva,
                                                    size_t len,
                                                    int32_t* ret,
                                                    int options) {
+  Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "sending push promise");
   *ret = nghttp2_submit_push_promise(session_->session(), NGHTTP2_FLAG_NONE,
                                      id_, nva, len, nullptr);
@@ -1381,6 +1399,7 @@ inline int Http2Stream::Write(nghttp2_stream_write_t* req,
                               const uv_buf_t bufs[],
                               unsigned int nbufs,
                               nghttp2_stream_write_cb cb) {
+  Http2Scope h2scope(this);
   if (!IsWritable()) {
     if (cb != nullptr)
       cb(req, UV_EOF);
@@ -1764,6 +1783,7 @@ void Http2Session::Goaway(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+  Http2Scope h2scope(session);
 
   uint32_t errorCode = args[0]->Uint32Value(context).ToChecked();
   int32_t lastStreamID = args[1]->Int32Value(context).ToChecked();
@@ -2039,6 +2059,7 @@ void Http2Session::Http2Ping::Send(uint8_t* payload) {
     memcpy(&data, &startTime_, arraysize(data));
     payload = data;
   }
+  Http2Scope h2scope(session_);
   CHECK_EQ(nghttp2_submit_ping(**session_, NGHTTP2_FLAG_NONE, payload), 0);
 }
 
diff --git a/src/node_http2.h b/src/node_http2.h
index 429fbdcdf05e65..9960361f79c3ff 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -416,7 +416,9 @@ const char* nghttp2_errname(int rv) {
 
 enum session_state_flags {
   SESSION_STATE_NONE = 0x0,
-  SESSION_STATE_DESTROYING = 0x1
+  SESSION_STATE_DESTROYING = 0x1,
+  SESSION_STATE_HAS_SCOPE = 0x2,
+  SESSION_STATE_WRITE_SCHEDULED = 0x4
 };
 
 // This allows for 4 default-sized frames with their frame headers
@@ -428,6 +430,19 @@ typedef uint32_t(*get_setting)(nghttp2_session* session,
 class Http2Session;
 class Http2Stream;
 
+// This scope should be present when any call into nghttp2 that may schedule
+// data to be written to the underlying transport is made, and schedules
+// such a write automatically once the scope is exited.
+class Http2Scope {
+ public:
+  explicit Http2Scope(Http2Stream* stream);
+  explicit Http2Scope(Http2Session* session);
+  ~Http2Scope();
+
+ private:
+  Http2Session* session_ = nullptr;
+};
+
 // The Http2Options class is used to parse the options object passed in to
 // a Http2Session object and convert those into an appropriate nghttp2_option
 // struct. This is the primary mechanism by which the Http2Session object is
@@ -815,6 +830,9 @@ class Http2Session : public AsyncWrap {
   inline void MarkDestroying() { flags_ |= SESSION_STATE_DESTROYING; }
   inline bool IsDestroying() { return flags_ & SESSION_STATE_DESTROYING; }
 
+  // Schedule a write if nghttp2 indicates it wants to write to the socket.
+  void MaybeScheduleWrite();
+
   // Returns pointer to the stream, or nullptr if stream does not exist
   inline Http2Stream* FindStream(int32_t id);
 
@@ -1004,6 +1022,8 @@ class Http2Session : public AsyncWrap {
 
   size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
   std::queue<Http2Ping*> outstanding_pings_;
+
+  friend class Http2Scope;
 };
 
 class Http2Session::Http2Ping : public AsyncWrap {
diff --git a/test/parallel/test-http2-session-gc-while-write-scheduled.js b/test/parallel/test-http2-session-gc-while-write-scheduled.js
new file mode 100644
index 00000000000000..bb23760cebf967
--- /dev/null
+++ b/test/parallel/test-http2-session-gc-while-write-scheduled.js
@@ -0,0 +1,32 @@
+// Flags: --expose-gc
+
+'use strict';
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const http2 = require('http2');
+const makeDuplexPair = require('../common/duplexpair');
+
+// This tests that running garbage collection while an Http2Session has
+// a write *scheduled*, it will survive that garbage collection.
+
+{
+  // This creates a session and schedules a write (for the settings frame).
+  let client = http2.connect('http://localhost:80', {
+    createConnection: common.mustCall(() => makeDuplexPair().clientSide)
+  });
+
+  // First, wait for any nextTicks() and their responses
+  // from the `connect()` call to run.
+  tick(10, () => {
+    // This schedules a write.
+    client.settings(http2.getDefaultSettings());
+    client = null;
+    global.gc();
+  });
+}
+
+function tick(n, cb) {
+  if (n--) setImmediate(tick, n, cb);
+  else cb();
+}

From 367b848bab28906ac25813de8d3d759db5da34d9 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 27 Nov 2017 13:20:29 -0800
Subject: [PATCH 11/77] http2: be sure to destroy the Http2Stream

PR-URL: https://github.com/nodejs/node/pull/17406
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
---
 src/node_http2.cc | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index f8b530c20a16cf..1ce3c5cb46e231 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -619,13 +619,16 @@ inline int Http2Session::OnStreamClose(nghttp2_session* handle,
   if (stream != nullptr) {
     stream->Close(code);
     // It is possible for the stream close to occur before the stream is
-    // ever passed on to the javascript side. If that happens, ignore this.
+    // ever passed on to the javascript side. If that happens, skip straight
+    // to destroying the stream
     Local<Value> fn =
         stream->object()->Get(context, env->onstreamclose_string())
             .ToLocalChecked();
     if (fn->IsFunction()) {
       Local<Value> argv[1] = { Integer::NewFromUnsigned(isolate, code) };
       stream->MakeCallback(fn.As<Function>(), arraysize(argv), argv);
+    } else {
+      stream->Destroy();
     }
   }
   return 0;

From 988f11fe06cecb05e5c825a4911c01b917167a17 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Tue, 12 Dec 2017 11:34:17 -0800
Subject: [PATCH 12/77] http2: cleanup Http2Stream/Http2Session destroy

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17406
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>

This is a significant cleanup and refactoring of the
cleanup/close/destroy logic for Http2Stream and Http2Session.
There are significant changes here in the timing and ordering
of cleanup logic, JS apis. and various related necessary edits.
---
 doc/api/errors.md                             |   23 +-
 doc/api/http2.md                              |  312 ++--
 lib/internal/errors.js                        |   13 +-
 lib/internal/http2/compat.js                  |   32 +-
 lib/internal/http2/core.js                    | 1649 +++++++++--------
 src/node_http2.cc                             |  650 +++++--
 src/node_http2.h                              |   42 +-
 .../test-http2-client-http1-server.js         |   11 +-
 test/parallel/test-http2-client-data-end.js   |   45 +-
 test/parallel/test-http2-client-destroy.js    |  212 +--
 .../test-http2-client-onconnect-errors.js     |   19 +-
 test/parallel/test-http2-client-port-80.js    |    8 +-
 ...st-http2-client-priority-before-connect.js |   28 +-
 .../test-http2-client-promisify-connect.js    |    3 +-
 ...est-http2-client-request-options-errors.js |   45 +-
 ...t-http2-client-rststream-before-connect.js |   26 +-
 .../test-http2-client-set-priority.js         |   20 +-
 ...st-http2-client-settings-before-connect.js |   75 +-
 ...st-http2-client-shutdown-before-connect.js |   12 +-
 .../test-http2-client-socket-destroy.js       |   31 +-
 ...p2-client-stream-destroy-before-connect.js |   23 +-
 .../test-http2-client-unescaped-path.js       |   15 +-
 test/parallel/test-http2-client-upload.js     |   20 +-
 .../test-http2-client-write-before-connect.js |   37 +-
 test/parallel/test-http2-compat-errors.js     |   26 +-
 ...test-http2-compat-expect-continue-check.js |   47 +-
 .../test-http2-compat-expect-continue.js      |   17 +-
 .../test-http2-compat-expect-handling.js      |    2 +-
 .../test-http2-compat-method-connect.js       |    2 +-
 .../test-http2-compat-serverrequest-end.js    |   15 +-
 ...test-http2-compat-serverrequest-headers.js |    2 +-
 .../test-http2-compat-serverrequest-pause.js  |    2 +-
 .../test-http2-compat-serverrequest-pipe.js   |    2 +-
 ...t-http2-compat-serverrequest-settimeout.js |    3 +-
 ...est-http2-compat-serverrequest-trailers.js |    2 +-
 .../test-http2-compat-serverrequest.js        |    2 +-
 .../test-http2-compat-serverresponse-close.js |   19 +-
 ...ompat-serverresponse-createpushresponse.js |    4 +-
 ...est-http2-compat-serverresponse-destroy.js |   86 +-
 .../test-http2-compat-serverresponse-drain.js |    2 +-
 .../test-http2-compat-serverresponse-end.js   |   18 +-
 ...st-http2-compat-serverresponse-finished.js |    2 +-
 ...ttp2-compat-serverresponse-flushheaders.js |    2 +-
 ...at-serverresponse-headers-after-destroy.js |    6 +-
 ...est-http2-compat-serverresponse-headers.js |    2 +-
 ...-http2-compat-serverresponse-settimeout.js |    3 +-
 ...-http2-compat-serverresponse-statuscode.js |    2 +-
 ...rverresponse-statusmessage-property-set.js |    2 +-
 ...t-serverresponse-statusmessage-property.js |    2 +-
 ...tp2-compat-serverresponse-statusmessage.js |    2 +-
 ...st-http2-compat-serverresponse-trailers.js |    2 +-
 ...http2-compat-serverresponse-write-no-cb.js |   65 +-
 ...t-http2-compat-serverresponse-writehead.js |    4 +-
 test/parallel/test-http2-compat-socket-set.js |    2 +-
 test/parallel/test-http2-compat-socket.js     |    2 +-
 test/parallel/test-http2-connect-method.js    |    9 +-
 test/parallel/test-http2-connect.js           |    8 +-
 test/parallel/test-http2-cookies.js           |    2 +-
 .../test-http2-create-client-connect.js       |   12 +-
 .../test-http2-create-client-session.js       |   26 +-
 ...test-http2-createsecureserver-nooptions.js |    8 +-
 test/parallel/test-http2-createwritereq.js    |    2 +-
 test/parallel/test-http2-date-header.js       |    2 +-
 test/parallel/test-http2-dont-lose-data.js    |   58 +
 test/parallel/test-http2-dont-override.js     |    2 +-
 .../test-http2-generic-streams-sendfile.js    |    6 +-
 test/parallel/test-http2-goaway-opaquedata.js |   23 +-
 test/parallel/test-http2-head-request.js      |    2 +-
 test/parallel/test-http2-https-fallback.js    |    2 +-
 .../test-http2-info-headers-errors.js         |   27 +-
 test/parallel/test-http2-info-headers.js      |    2 +-
 .../test-http2-invalidargtypes-errors.js      |   44 +-
 .../test-http2-max-concurrent-streams.js      |   72 +-
 test/parallel/test-http2-methods.js           |    2 +-
 ...t-http2-misbehaving-flow-control-paused.js |   20 +-
 .../test-http2-misbehaving-flow-control.js    |   36 +-
 .../test-http2-misused-pseudoheaders.js       |   31 +-
 .../test-http2-multi-content-length.js        |   41 +-
 test/parallel/test-http2-multiheaders-raw.js  |    2 +-
 test/parallel/test-http2-multiheaders.js      |    2 +-
 test/parallel/test-http2-multiplex.js         |   16 +-
 test/parallel/test-http2-no-more-streams.js   |    2 +-
 ...-http2-options-max-headers-block-length.js |   41 +-
 ...test-http2-options-max-reserved-streams.js |   55 +-
 test/parallel/test-http2-padding-callback.js  |    2 +-
 test/parallel/test-http2-ping.js              |    2 +-
 test/parallel/test-http2-pipe.js              |   31 +-
 test/parallel/test-http2-priority-event.js    |    2 +-
 test/parallel/test-http2-respond-errors.js    |    7 +-
 test/parallel/test-http2-respond-file-204.js  |    2 +-
 test/parallel/test-http2-respond-file-304.js  |    2 +-
 test/parallel/test-http2-respond-file-404.js  |    2 +-
 .../test-http2-respond-file-compat.js         |    2 +-
 .../test-http2-respond-file-error-dir.js      |    8 +-
 .../test-http2-respond-file-errors.js         |   40 +-
 .../test-http2-respond-file-fd-errors.js      |   43 +-
 .../test-http2-respond-file-fd-invalid.js     |    2 +-
 .../test-http2-respond-file-fd-range.js       |   23 +-
 test/parallel/test-http2-respond-file-fd.js   |    2 +-
 test/parallel/test-http2-respond-file-push.js |    5 +-
 .../parallel/test-http2-respond-file-range.js |    2 +-
 test/parallel/test-http2-respond-file.js      |    2 +-
 test/parallel/test-http2-respond-no-data.js   |    2 +-
 .../test-http2-respond-with-fd-errors.js      |    8 +-
 .../parallel/test-http2-response-splitting.js |    2 +-
 test/parallel/test-http2-rststream-errors.js  |   95 -
 test/parallel/test-http2-serve-file.js        |    2 +-
 test/parallel/test-http2-server-errors.js     |   11 -
 .../test-http2-server-http1-client.js         |    9 +-
 .../test-http2-server-push-disabled.js        |    2 +-
 ...st-http2-server-push-stream-errors-args.js |    2 +-
 .../test-http2-server-push-stream-errors.js   |   41 +-
 .../test-http2-server-push-stream-head.js     |   23 +-
 .../parallel/test-http2-server-push-stream.js |    5 +-
 .../test-http2-server-rst-before-respond.js   |   12 +-
 test/parallel/test-http2-server-rst-stream.js |   23 +-
 .../test-http2-server-sessionerror.js         |   48 +
 test/parallel/test-http2-server-set-header.js |    2 +-
 ...st-http2-server-shutdown-before-respond.js |   16 +-
 ...st-http2-server-shutdown-options-errors.js |   90 +-
 .../test-http2-server-shutdown-redundant.js   |   29 +-
 .../test-http2-server-socket-destroy.js       |   30 +-
 .../parallel/test-http2-server-socketerror.js |   56 -
 ...est-http2-server-stream-session-destroy.js |   91 +-
 test/parallel/test-http2-server-timeout.js    |   10 +-
 test/parallel/test-http2-session-settings.js  |  105 +-
 .../test-http2-session-stream-state.js        |    4 +-
 test/parallel/test-http2-shutdown-errors.js   |   76 -
 test/parallel/test-http2-single-headers.js    |   47 +-
 test/parallel/test-http2-socket-proxy.js      |   13 +-
 .../test-http2-status-code-invalid.js         |    2 +-
 test/parallel/test-http2-status-code.js       |    2 +-
 test/parallel/test-http2-stream-client.js     |    2 +-
 .../test-http2-stream-destroy-event-order.js  |   15 +-
 test/parallel/test-http2-timeouts.js          |    2 +-
 test/parallel/test-http2-too-large-headers.js |    2 +-
 test/parallel/test-http2-too-many-headers.js  |    2 +-
 test/parallel/test-http2-too-many-settings.js |    4 +-
 test/parallel/test-http2-trailers.js          |    2 +-
 test/parallel/test-http2-window-size.js       |    2 +-
 test/parallel/test-http2-write-callbacks.js   |    2 +-
 .../parallel/test-http2-write-empty-string.js |    2 +-
 test/parallel/test-http2-zero-length-write.js |    3 +-
 test/sequential/test-http2-session-timeout.js |    2 +-
 .../test-http2-timeout-large-write-file.js    |    2 +-
 .../test-http2-timeout-large-write.js         |    2 +-
 146 files changed, 2640 insertions(+), 2696 deletions(-)
 create mode 100644 test/parallel/test-http2-dont-lose-data.js
 delete mode 100644 test/parallel/test-http2-rststream-errors.js
 create mode 100644 test/parallel/test-http2-server-sessionerror.js
 delete mode 100644 test/parallel/test-http2-server-socketerror.js
 delete mode 100644 test/parallel/test-http2-shutdown-errors.js

diff --git a/doc/api/errors.md b/doc/api/errors.md
index e895accada72f2..9f487381309b96 100755
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -638,6 +638,11 @@ Status code was outside the regular status code range (100-999).
 The `Trailer` header was set even though the transfer encoding does not support
 that.
 
+<a id="ERR_HTTP2_ALREADY_SHUTDOWN"></a>
+### ERR_HTTP2_ALREADY_SHUTDOWN
+
+Occurs with multiple attempts to shutdown an HTTP/2 session.
+
 <a id="ERR_HTTP2_CONNECT_AUTHORITY"></a>
 ### ERR_HTTP2_CONNECT_AUTHORITY
 
@@ -661,6 +666,12 @@ forbidden.
 
 A failure occurred sending an individual frame on the HTTP/2 session.
 
+<a id="ERR_HTTP2_GOAWAY_SESSION"></a>
+### ERR_HTTP2_GOAWAY_SESSION
+
+New HTTP/2 Streams may not be opened after the `Http2Session` has received a
+`GOAWAY` frame from the connected peer.
+
 <a id="ERR_HTTP2_HEADER_REQUIRED"></a>
 ### ERR_HTTP2_HEADER_REQUIRED
 
@@ -800,6 +811,11 @@ client.
 An attempt was made to use the `Http2Stream.prototype.responseWithFile()` API to
 send something other than a regular file.
 
+<a id="ERR_HTTP2_SESSION_ERROR"></a>
+### ERR_HTTP2_SESSION_ERROR
+
+The `Http2Session` closed with a non-zero error code.
+
 <a id="ERR_HTTP2_SOCKET_BOUND"></a>
 ### ERR_HTTP2_SOCKET_BOUND
 
@@ -817,10 +833,11 @@ Use of the `101` Informational status code is forbidden in HTTP/2.
 An invalid HTTP status code has been specified. Status codes must be an integer
 between `100` and `599` (inclusive).
 
-<a id="ERR_HTTP2_STREAM_CLOSED"></a>
-### ERR_HTTP2_STREAM_CLOSED
+<a id="ERR_HTTP2_STREAM_CANCEL"></a>
+### ERR_HTTP2_STREAM_CANCEL
 
-An action was performed on an HTTP/2 Stream that had already been closed.
+An `Http2Stream` was destroyed before any data was transmitted to the connected
+peer.
 
 <a id="ERR_HTTP2_STREAM_ERROR"></a>
 ### ERR_HTTP2_STREAM_ERROR
diff --git a/doc/api/http2.md b/doc/api/http2.md
index 756fb6d041320d..7cb43babc3b7dd 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -67,8 +67,8 @@ const fs = require('fs');
 const client = http2.connect('https://localhost:8443', {
   ca: fs.readFileSync('localhost-cert.pem')
 });
-client.on('socketError', (err) => console.error(err));
 client.on('error', (err) => console.error(err));
+client.on('socketError', (err) => console.error(err));
 
 const req = client.request({ ':path': '/' });
 
@@ -83,7 +83,7 @@ let data = '';
 req.on('data', (chunk) => { data += chunk; });
 req.on('end', () => {
   console.log(`\n${data}`);
-  client.destroy();
+  client.close();
 });
 req.end();
 ```
@@ -127,7 +127,7 @@ solely on the API of the `Http2Session`.
 added: v8.4.0
 -->
 
-The `'close'` event is emitted once the `Http2Session` has been terminated.
+The `'close'` event is emitted once the `Http2Session` has been destroyed.
 
 #### Event: 'connect'
 <!-- YAML
@@ -234,19 +234,13 @@ of the stream.
 
 ```js
 const http2 = require('http2');
-const {
-  HTTP2_HEADER_METHOD,
-  HTTP2_HEADER_PATH,
-  HTTP2_HEADER_STATUS,
-  HTTP2_HEADER_CONTENT_TYPE
-} = http2.constants;
 session.on('stream', (stream, headers, flags) => {
-  const method = headers[HTTP2_HEADER_METHOD];
-  const path = headers[HTTP2_HEADER_PATH];
+  const method = headers[':method'];
+  const path = headers[':path'];
   // ...
   stream.respond({
-    [HTTP2_HEADER_STATUS]: 200,
-    [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+    ':status': 200,
+    'content-type': 'text/plain'
   });
   stream.write('hello ');
   stream.end('world');
@@ -275,19 +269,6 @@ server.on('stream', (stream, headers) => {
 server.listen(80);
 ```
 
-#### Event: 'socketError'
-<!-- YAML
-added: v8.4.0
--->
-
-The `'socketError'` event is emitted when an `'error'` is emitted on the
-`Socket` instance bound to the `Http2Session`. If this event is not handled,
-the `'error'` event will be re-emitted on the `Socket`.
-
-For `ServerHttp2Session` instances, a `'socketError'` event listener is always
-registered that will, by default, forward the event on to the owning
-`Http2Server` instance if no additional handlers are registered.
-
 #### Event: 'timeout'
 <!-- YAML
 added: v8.4.0
@@ -302,16 +283,53 @@ session.setTimeout(2000);
 session.on('timeout', () => { /** .. **/ });
 ```
 
-#### http2session.destroy()
+#### http2session.close([callback])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `callback` {Function}
+
+Gracefully closes the `Http2Session`, allowing any existing streams to
+complete on their own and preventing new `Http2Stream` instances from being
+created. Once closed, `http2session.destroy()` *might* be called if there
+are no open `Http2Stream` instances.
+
+If specified, the `callback` function is registered as a handler for the
+`'close'` event.
+
+#### http2session.closed
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Will be `true` if this `Http2Session` instance has been closed, otherwise
+`false`.
+
+#### http2session.destroy([error,][code])
 <!-- YAML
 added: v8.4.0
 -->
 
+* `error` {Error} An `Error` object if the `Http2Session` is being destroyed
+  due to an error.
+* `code` {number} The HTTP/2 error code to send in the final `GOAWAY` frame.
+  If unspecified, and `error` is not undefined, the default is `INTERNAL_ERROR`,
+  otherwise defaults to `NO_ERROR`.
 * Returns: {undefined}
 
 Immediately terminates the `Http2Session` and the associated `net.Socket` or
 `tls.TLSSocket`.
 
+Once destroyed, the `Http2Session` will emit the `'close'` event. If `error`
+is not undefined, an `'error'` event will be emitted immediately after the
+`'close'` event.
+
+If there are any remaining open `Http2Streams` associatd with the
+`Http2Session`, those will also be destroyed.
+
 #### http2session.destroyed
 <!-- YAML
 added: v8.4.0
@@ -322,6 +340,19 @@ added: v8.4.0
 Will be `true` if this `Http2Session` instance has been destroyed and must no
 longer be used, otherwise `false`.
 
+#### http2session.goaway([code, [lastStreamID, [opaqueData]]])
+<!-- YAML
+added: REPLACEME
+-->
+
+* `code` {number} An HTTP/2 error code
+* `lastStreamID` {number} The numeric ID of the last processed `Http2Stream`
+* `opaqueData` {Buffer|TypedArray|DataView} A `TypedArray` or `DataView`
+  instance containing additional data to be carried within the GOAWAY frame.
+
+Transmits a `GOAWAY` frame to the connected peer *without* shutting down the
+`Http2Session`.
+
 #### http2session.localSettings
 <!-- YAML
 added: v8.4.0
@@ -449,6 +480,12 @@ the trailing header fields to send to the peer.
 will be emitted if the `getTrailers` callback attempts to set such header
 fields.
 
+The the `:method` and `:path` pseudoheaders are not specified within `headers`,
+they respectively default to:
+
+* `:method` = `'GET'`
+* `:path` = `/`
+
 #### http2session.setTimeout(msecs, callback)
 <!-- YAML
 added: v8.4.0
@@ -462,19 +499,18 @@ Used to set a callback function that is called when there is no activity on
 the `Http2Session` after `msecs` milliseconds. The given `callback` is
 registered as a listener on the `'timeout'` event.
 
-#### http2session.shutdown(options[, callback])
+#### http2session.close(options[, callback])
 <!-- YAML
 added: v8.4.0
 -->
 
 * `options` {Object}
-  * `graceful` {boolean} `true` to attempt a polite shutdown of the
-    `Http2Session`.
   * `errorCode` {number} The HTTP/2 [error code][] to return. Note that this is
     *not* the same thing as an HTTP Response Status Code. **Default:** `0x00`
     (No Error).
   * `lastStreamID` {number} The Stream ID of the last successfully processed
-    `Http2Stream` on this `Http2Session`.
+    `Http2Stream` on this `Http2Session`. If unspecified, will default to the
+    ID of the most recently received stream.
   * `opaqueData` {Buffer|Uint8Array} A `Buffer` or `Uint8Array` instance
     containing arbitrary additional data to send to the peer upon disconnection.
     This is used, typically, to provide additional data for debugging failures,
@@ -487,19 +523,16 @@ Attempts to shutdown this `Http2Session` using HTTP/2 defined procedures.
 If specified, the given `callback` function will be invoked once the shutdown
 process has completed.
 
-Note that calling `http2session.shutdown()` does *not* destroy the session or
-tear down the `Socket` connection. It merely prompts both sessions to begin
-preparing to cease activity.
-
-During a "graceful" shutdown, the session will first send a `GOAWAY` frame to
-the connected peer identifying the last processed stream as 2<sup>32</sup>-1.
+If the `Http2Session` instance is a server-side session and the `errorCode`
+option is `0x00` (No Error), a "graceful" shutdown will be initiated. During a
+"graceful" shutdown, the session will first send a `GOAWAY` frame to
+the connected peer identifying the last processed stream as 2<sup>31</sup>-1.
 Then, on the next tick of the event loop, a second `GOAWAY` frame identifying
 the most recently processed stream identifier is sent. This process allows the
 remote peer to begin preparing for the connection to be terminated.
 
 ```js
-session.shutdown({
-  graceful: true,
+session.close({
   opaqueData: Buffer.from('add some debugging data here')
 }, () => session.destroy());
 ```
@@ -639,7 +672,7 @@ is not yet ready for use.
 All [`Http2Stream`][] instances are destroyed either when:
 
 * An `RST_STREAM` frame for the stream is received by the connected peer.
-* The `http2stream.rstStream()` methods is called.
+* The `http2stream.close()` method is called.
 * The `http2stream.destroy()` or `http2session.destroy()` methods are called.
 
 When an `Http2Stream` instance is destroyed, an attempt will be made to send an
@@ -732,6 +765,29 @@ added: v8.4.0
 Set to `true` if the `Http2Stream` instance was aborted abnormally. When set,
 the `'aborted'` event will have been emitted.
 
+#### http2stream.close(code[, callback])
+<!-- YAML
+added: v8.4.0
+-->
+
+* code {number} Unsigned 32-bit integer identifying the error code. **Default:**
+  `http2.constant.NGHTTP2_NO_ERROR` (`0x00`)
+* `callback` {Function} An optional function registered to listen for the
+  `'close'` event.
+* Returns: {undefined}
+
+Closes the `Http2Stream` instance by sending an `RST_STREAM` frame to the
+connected HTTP/2 peer.
+
+#### http2stream.closed
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Set to `true` if the `Http2Stream` instance has been closed.
+
 #### http2stream.destroyed
 <!-- YAML
 added: v8.4.0
@@ -742,6 +798,16 @@ added: v8.4.0
 Set to `true` if the `Http2Stream` instance has been destroyed and is no longer
 usable.
 
+#### http2stream.pending
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean}
+
+Set to `true` if the `Http2Stream` instance has not yet been assigned a
+numeric stream identifier.
+
 #### http2stream.priority(options)
 <!-- YAML
 added: v8.4.0
@@ -772,66 +838,9 @@ added: v8.4.0
 
 Set to the `RST_STREAM` [error code][] reported when the `Http2Stream` is
 destroyed after either receiving an `RST_STREAM` frame from the connected peer,
-calling `http2stream.rstStream()`, or `http2stream.destroy()`. Will be
+calling `http2stream.close()`, or `http2stream.destroy()`. Will be
 `undefined` if the `Http2Stream` has not been closed.
 
-#### http2stream.rstStream(code)
-<!-- YAML
-added: v8.4.0
--->
-
-* code {number} Unsigned 32-bit integer identifying the error code. **Default:**
-  `http2.constant.NGHTTP2_NO_ERROR` (`0x00`)
-* Returns: {undefined}
-
-Sends an `RST_STREAM` frame to the connected HTTP/2 peer, causing this
-`Http2Stream` to be closed on both sides using [error code][] `code`.
-
-#### http2stream.rstWithNoError()
-<!-- YAML
-added: v8.4.0
--->
-
-* Returns: {undefined}
-
-Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error).
-
-#### http2stream.rstWithProtocolError()
-<!-- YAML
-added: v8.4.0
--->
-
-* Returns: {undefined}
-
-Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error).
-
-#### http2stream.rstWithCancel()
-<!-- YAML
-added: v8.4.0
--->
-
-* Returns: {undefined}
-
-Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel).
-
-#### http2stream.rstWithRefuse()
-<!-- YAML
-added: v8.4.0
--->
-
-* Returns: {undefined}
-
-Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream).
-
-#### http2stream.rstWithInternalError()
-<!-- YAML
-added: v8.4.0
--->
-
-* Returns: {undefined}
-
-Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error).
-
 #### http2stream.session
 <!-- YAML
 added: v8.4.0
@@ -854,11 +863,11 @@ added: v8.4.0
 ```js
 const http2 = require('http2');
 const client = http2.connect('http://example.org:8000');
-
+const { NGHTTP2_CANCEL } = http2.constants;
 const req = client.request({ ':path': '/' });
 
 // Cancel the stream if there's no activity after 5 seconds
-req.setTimeout(5000, () => req.rstWithCancel());
+req.setTimeout(5000, () => req.rstStream(NGHTTP2_CANCEL));
 ```
 
 #### http2stream.state
@@ -1282,17 +1291,50 @@ added: v8.4.0
 * Extends: {net.Server}
 
 In `Http2Server`, there is no `'clientError'` event as there is in
-HTTP1. However, there are `'socketError'`, `'sessionError'`, and
-`'streamError'`, for error happened on the socket, session, or stream
-respectively.
+HTTP1. However, there are `'socketError'`, `'sessionError'`,  and
+`'streamError'`, for errors emitted on the socket, `Http2Session`, or
+`Http2Stream`.
+
+#### Event: 'checkContinue'
+<!-- YAML
+added: v8.5.0
+-->
 
-#### Event: 'socketError'
+* `request` {http2.Http2ServerRequest}
+* `response` {http2.Http2ServerResponse}
+
+If a [`'request'`][] listener is registered or [`http2.createServer()`][] is
+supplied a callback function, the `'checkContinue'` event is emitted each time
+a request with an HTTP `Expect: 100-continue` is received. If this event is
+not listened for, the server will automatically respond with a status
+`100 Continue` as appropriate.
+
+Handling this event involves calling [`response.writeContinue()`][] if the client
+should continue to send the request body, or generating an appropriate HTTP
+response (e.g. 400 Bad Request) if the client should not continue to send the
+request body.
+
+Note that when this event is emitted and handled, the [`'request'`][] event will
+not be emitted.
+
+#### Event: 'request'
 <!-- YAML
 added: v8.4.0
 -->
 
-The `'socketError'` event is emitted when a `'socketError'` event is emitted by
-an `Http2Session` associated with the server.
+* `request` {http2.Http2ServerRequest}
+* `response` {http2.Http2ServerResponse}
+
+Emitted each time there is a request. Note that there may be multiple requests
+per session. See the [Compatibility API][].
+
+#### Event: 'session'
+<!-- YAML
+added: v8.4.0
+-->
+
+The `'session'` event is emitted when a new `Http2Session` is created by the
+`Http2Server`.
 
 #### Event: 'sessionError'
 <!-- YAML
@@ -1300,16 +1342,13 @@ added: v8.4.0
 -->
 
 The `'sessionError'` event is emitted when an `'error'` event is emitted by
-an `Http2Session` object. If no listener is registered for this event, an
-`'error'` event is emitted.
+an `Http2Session` object associated with the `Http2Server`.
 
 #### Event: 'streamError'
 <!-- YAML
 added: v8.5.0
 -->
 
-* `socket` {ServerHttp2Stream}
-
 If an `ServerHttp2Stream` emits an `'error'` event, it will be forwarded here.
 The stream will already be destroyed when this event is triggered.
 
@@ -1344,17 +1383,6 @@ server.on('stream', (stream, headers, flags) => {
 });
 ```
 
-#### Event: 'request'
-<!-- YAML
-added: v8.4.0
--->
-
-* `request` {http2.Http2ServerRequest}
-* `response` {http2.Http2ServerResponse}
-
-Emitted each time there is a request. Note that there may be multiple requests
-per session. See the [Compatibility API][].
-
 #### Event: 'timeout'
 <!-- YAML
 added: v8.4.0
@@ -1363,28 +1391,6 @@ added: v8.4.0
 The `'timeout'` event is emitted when there is no activity on the Server for
 a given number of milliseconds set using `http2server.setTimeout()`.
 
-#### Event: 'checkContinue'
-<!-- YAML
-added: v8.5.0
--->
-
-* `request` {http2.Http2ServerRequest}
-* `response` {http2.Http2ServerResponse}
-
-If a [`'request'`][] listener is registered or [`http2.createServer()`][] is
-supplied a callback function, the `'checkContinue'` event is emitted each time
-a request with an HTTP `Expect: 100-continue` is received. If this event is
-not listened for, the server will automatically respond with a status
-`100 Continue` as appropriate.
-
-Handling this event involves calling [`response.writeContinue()`][] if the client
-should continue to send the request body, or generating an appropriate HTTP
-response (e.g. 400 Bad Request) if the client should not continue to send the
-request body.
-
-Note that when this event is emitted and handled, the [`'request'`][] event will
-not be emitted.
-
 ### Class: Http2SecureServer
 <!-- YAML
 added: v8.4.0
@@ -1398,16 +1404,7 @@ added: v8.4.0
 -->
 
 The `'sessionError'` event is emitted when an `'error'` event is emitted by
-an `Http2Session` object. If no listener is registered for this event, an
-`'error'` event is emitted on the `Http2Session` instance instead.
-
-#### Event: 'socketError'
-<!-- YAML
-added: v8.4.0
--->
-
-The `'socketError'` event is emitted when a `'socketError'` event is emitted by
-an `Http2Session` associated with the server.
+an `Http2Session` object associated with the `Http2SecureServer`.
 
 #### Event: 'unknownProtocol'
 <!-- YAML
@@ -1701,7 +1698,7 @@ const client = http2.connect('https://localhost:1234');
 
 /** use the client **/
 
-client.destroy();
+client.close();
 ```
 
 ### http2.constants
@@ -1957,6 +1954,7 @@ An HTTP/2 CONNECT proxy:
 
 ```js
 const http2 = require('http2');
+const { NGHTTP2_REFUSED_STREAM } = http2.constants;
 const net = require('net');
 const { URL } = require('url');
 
@@ -1964,7 +1962,7 @@ const proxy = http2.createServer();
 proxy.on('stream', (stream, headers) => {
   if (headers[':method'] !== 'CONNECT') {
     // Only accept CONNECT requests
-    stream.rstWithRefused();
+    stream.close(NGHTTP2_REFUSED_STREAM);
     return;
   }
   const auth = new URL(`tcp://${headers[':authority']}`);
@@ -1976,7 +1974,7 @@ proxy.on('stream', (stream, headers) => {
     stream.pipe(socket);
   });
   socket.on('error', (error) => {
-    stream.rstStream(http2.constants.NGHTTP2_CONNECT_ERROR);
+    stream.close(http2.constants.NGHTTP2_CONNECT_ERROR);
   });
 });
 
@@ -2005,7 +2003,7 @@ req.setEncoding('utf8');
 req.on('data', (chunk) => data += chunk);
 req.on('end', () => {
   console.log(`The server says: ${data}`);
-  client.destroy();
+  client.close();
 });
 req.end('Jane');
 ```
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index ea5526c795343c..c1a1f16213a043 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -150,6 +150,8 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA',
 E('ERR_ENCODING_NOT_SUPPORTED',
   (enc) => `The "${enc}" encoding is not supported`);
 E('ERR_FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
+E('ERR_HTTP2_ALREADY_SHUTDOWN',
+  'Http2Session is already shutdown or destroyed');
 E('ERR_HTTP2_CONNECT_AUTHORITY',
   ':authority header is required for CONNECT requests');
 E('ERR_HTTP2_CONNECT_PATH',
@@ -164,6 +166,8 @@ E('ERR_HTTP2_FRAME_ERROR',
     msg += ` with code ${code}`;
     return msg;
   });
+E('ERR_HTTP2_GOAWAY_SESSION',
+  'New streams cannot be created after receiving a GOAWAY');
 E('ERR_HTTP2_HEADERS_AFTER_RESPOND',
   'Cannot specify additional headers after response initiated');
 E('ERR_HTTP2_HEADERS_OBJECT', 'Headers must be an object');
@@ -202,15 +206,14 @@ E('ERR_HTTP2_PING_LENGTH', 'HTTP2 ping payload must be 8 bytes');
 E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', 'Cannot set HTTP/2 pseudo-headers');
 E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams');
 E('ERR_HTTP2_SEND_FILE', 'Only regular files can be sent');
+E('ERR_HTTP2_SESSION_ERROR', 'Session closed with error code %s');
 E('ERR_HTTP2_SOCKET_BOUND',
   'The socket is already bound to an Http2Session');
 E('ERR_HTTP2_STATUS_101',
   'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2');
-E('ERR_HTTP2_STATUS_INVALID',
-  (code) => `Invalid status code: ${code}`);
-E('ERR_HTTP2_STREAM_CLOSED', 'The stream is already closed');
-E('ERR_HTTP2_STREAM_ERROR',
-  (code) => `Stream closed with error code ${code}`);
+E('ERR_HTTP2_STATUS_INVALID', 'Invalid status code: %s');
+E('ERR_HTTP2_STREAM_CANCEL', 'The pending stream has been canceled');
+E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s');
 E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', 'A stream cannot depend on itself');
 E('ERR_HTTP2_UNSUPPORTED_PROTOCOL',
   (protocol) => `protocol "${protocol}" is unsupported.`);
diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js
index ec1f0ba64eff0a..b5dd81c80f4038 100644
--- a/lib/internal/http2/compat.js
+++ b/lib/internal/http2/compat.js
@@ -126,14 +126,11 @@ function onStreamAbortedRequest() {
   const request = this[kRequest];
   if (request !== undefined && request[kState].closed === false) {
     request.emit('aborted');
-    request.emit('close');
   }
 }
 
 function onStreamAbortedResponse() {
-  const response = this[kResponse];
-  if (response !== undefined && response[kState].closed === false)
-    response.emit('close');
+  // non-op for now
 }
 
 function resumeStream(stream) {
@@ -234,9 +231,7 @@ class Http2ServerRequest extends Readable {
     stream.on('end', onStreamEnd);
     stream.on('error', onStreamError);
     stream.on('aborted', onStreamAbortedRequest);
-    const onfinish = this[kFinish].bind(this);
-    stream.on('close', onfinish);
-    stream.on('finish', onfinish);
+    stream.on('close', this[kFinish].bind(this));
     this.on('pause', onRequestPause);
     this.on('resume', onRequestResume);
   }
@@ -297,7 +292,7 @@ class Http2ServerRequest extends Readable {
       state.didRead = true;
       process.nextTick(resumeStream, this[kStream]);
     } else {
-      this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
+      this.emit('error', new errors.Error('ERR_HTTP2_INVALID_STREAM'));
     }
   }
 
@@ -345,6 +340,7 @@ class Http2ServerRequest extends Readable {
     // dump it for compatibility with http1
     if (!state.didRead && !this._readableState.resumeScheduled)
       this.resume();
+    this.emit('close');
   }
 }
 
@@ -366,9 +362,7 @@ class Http2ServerResponse extends Stream {
     this.writable = true;
     stream.on('drain', onStreamDrain);
     stream.on('aborted', onStreamAbortedResponse);
-    const onfinish = this[kFinish].bind(this);
-    stream.on('close', onfinish);
-    stream.on('finish', onfinish);
+    stream.on('close', this[kFinish].bind(this));
   }
 
   // User land modules such as finalhandler just check truthiness of this
@@ -520,7 +514,7 @@ class Http2ServerResponse extends Stream {
     const state = this[kState];
 
     if (state.closed)
-      throw new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+      throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
     if (this[kStream].headersSent)
       throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
 
@@ -550,7 +544,7 @@ class Http2ServerResponse extends Stream {
     }
 
     if (this[kState].closed) {
-      const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+      const err = new errors.Error('ERR_HTTP2_INVALID_STREAM');
       if (typeof cb === 'function')
         process.nextTick(cb, err);
       else
@@ -620,12 +614,15 @@ class Http2ServerResponse extends Stream {
     if (typeof callback !== 'function')
       throw new errors.TypeError('ERR_INVALID_CALLBACK');
     if (this[kState].closed) {
-      process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
+      process.nextTick(callback, new errors.Error('ERR_HTTP2_INVALID_STREAM'));
       return;
     }
-    this[kStream].pushStream(headers, {}, function(stream, headers, options) {
-      const response = new Http2ServerResponse(stream);
-      callback(null, response);
+    this[kStream].pushStream(headers, {}, (err, stream, headers, options) => {
+      if (err) {
+        callback(err);
+        return;
+      }
+      callback(null, new Http2ServerResponse(stream));
     });
   }
 
@@ -649,6 +646,7 @@ class Http2ServerResponse extends Stream {
     this[kProxySocket] = null;
     stream[kResponse] = undefined;
     this.emit('finish');
+    this.emit('close');
   }
 
   // TODO doesn't support callbacks
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 72d43049645b2a..252e2280317e7a 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -66,10 +66,10 @@ const TLSServer = tls.Server;
 const kInspect = require('internal/util').customInspectSymbol;
 
 const kAuthority = Symbol('authority');
-const kDestroySocket = Symbol('destroy-socket');
 const kHandle = Symbol('handle');
 const kID = Symbol('id');
 const kInit = Symbol('init');
+const kMaybeDestroy = Symbol('maybe-destroy');
 const kLocalSettings = Symbol('local-settings');
 const kOptions = Symbol('options');
 const kOwner = Symbol('owner');
@@ -84,7 +84,6 @@ const kType = Symbol('type');
 const kUpdateTimer = Symbol('update-timer');
 
 const kDefaultSocketTimeout = 2 * 60 * 1000;
-const kRenegTest = /TLS session renegotiation disabled for this socket/;
 
 const {
   paddingBuffer,
@@ -95,14 +94,13 @@ const {
 
 const {
   NGHTTP2_CANCEL,
+  NGHTTP2_REFUSED_STREAM,
   NGHTTP2_DEFAULT_WEIGHT,
   NGHTTP2_FLAG_END_STREAM,
   NGHTTP2_HCAT_PUSH_RESPONSE,
   NGHTTP2_HCAT_RESPONSE,
   NGHTTP2_INTERNAL_ERROR,
   NGHTTP2_NO_ERROR,
-  NGHTTP2_PROTOCOL_ERROR,
-  NGHTTP2_REFUSED_STREAM,
   NGHTTP2_SESSION_CLIENT,
   NGHTTP2_SESSION_SERVER,
   NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
@@ -139,6 +137,18 @@ const {
   STREAM_OPTION_GET_TRAILERS
 } = constants;
 
+const STREAM_FLAGS_PENDING = 0x0;
+const STREAM_FLAGS_READY = 0x1;
+const STREAM_FLAGS_CLOSED = 0x2;
+const STREAM_FLAGS_HEADERS_SENT = 0x4;
+const STREAM_FLAGS_HEAD_REQUEST = 0x8;
+const STREAM_FLAGS_ABORTED = 0x10;
+
+const SESSION_FLAGS_PENDING = 0x0;
+const SESSION_FLAGS_READY = 0x1;
+const SESSION_FLAGS_CLOSED = 0x2;
+const SESSION_FLAGS_DESTROYED = 0x4;
+
 // Top level to avoid creating a closure
 function emit(self, ...args) {
   self.emit(...args);
@@ -150,12 +160,15 @@ function emit(self, ...args) {
 // event. If the stream is not new, emit the 'headers' event to pass
 // the block of headers on.
 function onSessionHeaders(handle, id, cat, flags, headers) {
-  const owner = this[kOwner];
-  const type = owner[kType];
-  owner[kUpdateTimer]();
+  const session = this[kOwner];
+  if (session.destroyed)
+    return;
+
+  const type = session[kType];
+  session[kUpdateTimer]();
   debug(`Http2Stream ${id} [Http2Session ` +
         `${sessionName(type)}]: headers received`);
-  const streams = owner[kState].streams;
+  const streams = session[kState].streams;
 
   const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM);
   let stream = streams.get(id);
@@ -164,21 +177,28 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
   const obj = toHeaderObject(headers);
 
   if (stream === undefined) {
+    if (session.closed) {
+      // we are not accepting any new streams at this point. This callback
+      // should not be invoked at this point in time, but just in case it is,
+      // refuse the stream using an RST_STREAM and destroy the handle.
+      handle.rstStream(NGHTTP2_REFUSED_STREAM);
+      handle.destroy();
+      return;
+    }
     const opts = { readable: !endOfStream };
-    // owner[kType] can be only one of two possible values
+    // session[kType] can be only one of two possible values
     if (type === NGHTTP2_SESSION_SERVER) {
-      stream = new ServerHttp2Stream(owner, handle, id, opts, obj);
+      stream = new ServerHttp2Stream(session, handle, id, opts, obj);
       if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
         // For head requests, there must not be a body...
         // end the writable side immediately.
         stream.end();
-        stream[kState].headRequest = true;
+        stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST;
       }
     } else {
-      stream = new ClientHttp2Stream(owner, handle, id, opts);
+      stream = new ClientHttp2Stream(session, handle, id, opts);
     }
-    streams.set(id, stream);
-    process.nextTick(emit, owner, 'stream', stream, obj, flags, headers);
+    process.nextTick(emit, session, 'stream', stream, obj, flags, headers);
   } else {
     let event;
     const status = obj[HTTP2_HEADER_STATUS];
@@ -208,6 +228,12 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
   }
 }
 
+function tryClose(fd) {
+  // Try to close the file descriptor. If closing fails, assert because
+  // an error really should not happen at this point.
+  fs.close(fd, (err) => assert.ifError(err));
+}
+
 // Called to determine if there are trailers to be sent at the end of a
 // Stream. The 'getTrailers' callback is invoked and passed a holder object.
 // The trailers to return are set on that object by the handler. Once the
@@ -216,133 +242,167 @@ function onSessionHeaders(handle, id, cat, flags, headers) {
 // there are trailing headers to send.
 function onStreamTrailers() {
   const stream = this[kOwner];
+  if (stream.destroyed)
+    return [];
   const trailers = Object.create(null);
   stream[kState].getTrailers.call(stream, trailers);
   const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
   if (!Array.isArray(headersList)) {
-    process.nextTick(emit, stream, 'error', headersList);
-    return;
+    stream.destroy(headersList);
+    return [];
   }
   return headersList;
 }
 
-// Called when the stream is closed. The close event is emitted on the
-// Http2Stream instance
-function onStreamClose(code) {
+// Submit an RST-STREAM frame to be sent to the remote peer.
+// This will cause the Http2Stream to be closed.
+function submitRstStream(code) {
+  if (this[kHandle] !== undefined) {
+    this[kHandle].rstStream(code);
+  }
+}
+
+// Called when the stream is closed either by sending or receiving an
+// RST_STREAM frame, or through a natural end-of-stream.
+// If the writable and readable sides of the stream are still open at this
+// point, close them. If there is an open fd for file send, close that also.
+// At this point the underlying node::http2:Http2Stream handle is no
+// longer usable so destroy it also.
+function onStreamClose(code, hasData) {
   const stream = this[kOwner];
-  stream[kUpdateTimer]();
-  abort(stream);
+  if (stream.destroyed)
+    return;
+
   const state = stream[kState];
-  state.rst = true;
-  state.rstCode = code;
-  if (state.fd !== undefined)
-    fs.close(state.fd, afterFDClose.bind(stream));
 
-  setImmediate(stream.destroy.bind(stream));
-}
+  debug(`Http2Stream ${stream[kID]} [Http2Session ` +
+        `${sessionName(stream[kSession][kType])}]: closed with code ${code}` +
+        ` [has data? ${hasData}]`);
 
-function afterFDClose(err) {
-  if (err)
-    process.nextTick(emit, this, 'error', err);
-}
+  if (!stream.closed) {
+    // Unenroll from timeouts
+    unenroll(stream);
+    stream.removeAllListeners('timeout');
+
+    // Set the state flags
+    state.flags |= STREAM_FLAGS_CLOSED;
+    state.rstCode = code;
 
-// Called when an error event needs to be triggered
-function onSessionError(error) {
-  const owner = this[kOwner];
-  owner[kUpdateTimer]();
-  process.nextTick(emit, owner, 'error', error);
+    // Close the writable side of the stream
+    abort(stream);
+    stream.end();
+  }
+
+  if (state.fd !== undefined)
+    tryClose(state.fd);
+  stream[kMaybeDestroy](null, code, hasData);
 }
 
 // Receives a chunk of data for a given stream and forwards it on
 // to the Http2Stream Duplex for processing.
 function onStreamRead(nread, buf, handle) {
   const stream = handle[kOwner];
-  stream[kUpdateTimer]();
   if (nread >= 0 && !stream.destroyed) {
+    debug(`Http2Stream ${stream[kID]} [Http2Session ` +
+          `${sessionName(stream[kSession][kType])}]: receiving data chunk ` +
+          `of size ${nread}`);
+    stream[kUpdateTimer]();
     if (!stream.push(buf)) {
-      handle.readStop();
+      if (!stream.destroyed)  // we have to check a second time
+        handle.readStop();
     }
     return;
   }
   // Last chunk was received. End the readable side.
+  debug(`Http2Stream ${stream[kID]} [Http2Session ` +
+        `${sessionName(stream[kSession][kType])}]: ending readable.`);
   stream.push(null);
+  stream[kMaybeDestroy]();
 }
 
 // Called when the remote peer settings have been updated.
 // Resets the cached settings.
 function onSettings(ack) {
-  const owner = this[kOwner];
-  debug(`Http2Session ${sessionName(owner[kType])}: new settings received`);
-  owner[kUpdateTimer]();
+  const session = this[kOwner];
+  if (session.destroyed)
+    return;
+  session[kUpdateTimer]();
   let event = 'remoteSettings';
   if (ack) {
-    if (owner[kState].pendingAck > 0)
-      owner[kState].pendingAck--;
-    owner[kLocalSettings] = undefined;
+    debug(`Http2Session ${sessionName(session[kType])}: settings acknowledged`);
+    if (session[kState].pendingAck > 0)
+      session[kState].pendingAck--;
+    session[kLocalSettings] = undefined;
     event = 'localSettings';
   } else {
-    owner[kRemoteSettings] = undefined;
+    debug(`Http2Session ${sessionName(session[kType])}: new settings received`);
+    session[kRemoteSettings] = undefined;
   }
-  // Only emit the event if there are listeners registered
-  if (owner.listenerCount(event) > 0)
-    process.nextTick(emit, owner, event, owner[event]);
+  process.nextTick(emit, session, event, session[event]);
 }
 
 // If the stream exists, an attempt will be made to emit an event
 // on the stream object itself. Otherwise, forward it on to the
 // session (which may, in turn, forward it on to the server)
 function onPriority(id, parent, weight, exclusive) {
-  const owner = this[kOwner];
+  const session = this[kOwner];
+  if (session.destroyed)
+    return;
   debug(`Http2Stream ${id} [Http2Session ` +
-        `${sessionName(owner[kType])}]: priority [parent: ${parent}, ` +
+        `${sessionName(session[kType])}]: priority [parent: ${parent}, ` +
         `weight: ${weight}, exclusive: ${exclusive}]`);
-  owner[kUpdateTimer]();
-  const streams = owner[kState].streams;
-  const stream = streams.get(id);
-  const emitter = stream === undefined ? owner : stream;
-  process.nextTick(emit, emitter, 'priority', id, parent, weight, exclusive);
-}
-
-function emitFrameError(self, id, type, code) {
-  if (!self.emit('frameError', type, code, id)) {
-    const err = new errors.Error('ERR_HTTP2_FRAME_ERROR', type, code, id);
-    err.errno = code;
-    self.emit('error', err);
+  const emitter = session[kState].streams.get(id) || session;
+  if (!emitter.destroyed) {
+    emitter[kUpdateTimer]();
+    process.nextTick(emit, emitter, 'priority', id, parent, weight, exclusive);
   }
 }
 
 // Called by the native layer when an error has occurred sending a
 // frame. This should be exceedingly rare.
 function onFrameError(id, type, code) {
-  const owner = this[kOwner];
-  debug(`Http2Session ${sessionName(owner[kType])}: error sending frame type ` +
-        `${type} on stream ${id}, code: ${code}`);
-  owner[kUpdateTimer]();
-  const streams = owner[kState].streams;
-  const stream = streams.get(id);
-  const emitter = stream !== undefined ? stream : owner;
-  process.nextTick(emitFrameError, emitter, id, type, code);
-}
-
-function emitGoaway(self, code, lastStreamID, buf) {
-  self.emit('goaway', code, lastStreamID, buf);
-  // Tear down the session or destroy
-  const state = self[kState];
-  if (state.destroying || state.destroyed)
+  const session = this[kOwner];
+  if (session.destroyed)
     return;
-  if (!state.shuttingDown && !state.shutdown) {
-    self.shutdown({}, self.destroy.bind(self));
+  debug(`Http2Session ${sessionName(session[kType])}: error sending frame ` +
+        `type ${type} on stream ${id}, code: ${code}`);
+  const emitter = session[kState].streams.get(id) || session;
+  emitter[kUpdateTimer]();
+  process.nextTick(emit, emitter, 'frameError', type, code, id);
+}
+
+// Receiving a GOAWAY frame from the connected peer is a signal that no
+// new streams should be created. If the code === NGHTTP2_NO_ERROR, we
+// are going to send our our close, but allow existing frames to close
+// normally. If code !== NGHTTP2_NO_ERROR, we are going to send our own
+// close using the same code then destroy the session with an error.
+// The goaway event will be emitted on next tick.
+function onGoawayData(code, lastStreamID, buf) {
+  const session = this[kOwner];
+  if (session.destroyed)
     return;
+  debug(`Http2Session ${sessionName(session[kType])}: goaway ${code} ` +
+        `received [last stream id: ${lastStreamID}]`);
+
+  const state = session[kState];
+  state.goawayCode = code;
+  state.goawayLastStreamID = lastStreamID;
+
+  session.emit('goaway', code, lastStreamID, buf);
+  if (code === NGHTTP2_NO_ERROR) {
+    // If this is a no error goaway, begin shutting down.
+    // No new streams permitted, but existing streams may
+    // close naturally on their own.
+    session.close();
+  } else {
+    // However, if the code is not NGHTTP_NO_ERROR, destroy the
+    // session immediately. We destroy with an error but send a
+    // goaway using NGHTTP2_NO_ERROR because there was no error
+    // condition on this side of the session that caused the
+    // shutdown.
+    session.destroy(new errors.Error('ERR_HTTP2_SESSION_ERROR', code),
+                    { errorCode: NGHTTP2_NO_ERROR });
   }
-  self.destroy();
-}
-
-// Called by the native layer when a goaway frame has been received
-function onGoawayData(code, lastStreamID, buf) {
-  const owner = this[kOwner];
-  debug(`Http2Session ${sessionName(owner[kType])}: goaway ${code} received ` +
-        `[last stream id: ${lastStreamID}]`);
-  process.nextTick(emitGoaway, owner, code, lastStreamID, buf);
 }
 
 // Returns the padding to use per frame. The selectPadding callback is set
@@ -362,18 +422,23 @@ function onSelectPadding(fn) {
 // will be deferred until the socket is ready to go.
 function requestOnConnect(headers, options) {
   const session = this[kSession];
-  debug(`Http2Session ${sessionName(session[kType])}: connected, ` +
-        'initializing request');
-  const streams = session[kState].streams;
 
-  validatePriorityOptions(options);
+  // At this point, the stream should have already been destroyed during
+  // the session.destroy() method. Do nothing else.
+  if (session.destroyed)
+    return;
 
-  const headersList = mapToHeaders(headers);
-  if (!Array.isArray(headersList)) {
-    process.nextTick(emit, this, 'error', headersList);
+  // If the session was closed while waiting for for the connect, destroy
+  // the stream and do not continue with the request.
+  if (session.closed) {
+    const err = new errors.Error('ERR_HTTP2_GOAWAY_SESSION');
+    this.destroy(err);
     return;
   }
 
+  debug(`Http2Session ${sessionName(session[kType])}: connected, ` +
+        'initializing request');
+
   let streamOptions = 0;
   if (options.endStream)
     streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD;
@@ -385,7 +450,7 @@ function requestOnConnect(headers, options) {
 
   // ret will be either the reserved stream ID (if positive)
   // or an error code (if negative)
-  const ret = session[kHandle].request(headersList,
+  const ret = session[kHandle].request(headers,
                                        streamOptions,
                                        options.parent | 0,
                                        options.weight | 0,
@@ -402,117 +467,82 @@ function requestOnConnect(headers, options) {
   // session if not handled.
   if (typeof ret === 'number') {
     let err;
-    let target = session;
     switch (ret) {
       case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
         err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
-        target = this;
+        this.destroy(err);
         break;
       case NGHTTP2_ERR_INVALID_ARGUMENT:
         err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
-        target = this;
+        this.destroy(err);
         break;
       default:
-        err = new NghttpError(ret);
+        session.destroy(new NghttpError(ret));
     }
-    process.nextTick(emit, target, 'error', err);
     return;
   }
-  const id = ret.id();
-  streams.set(id, this);
-  this[kInit](id, ret);
+  this[kInit](ret.id(), ret);
 }
 
+// Validates that priority options are correct, specifically:
+// 1. options.weight must be a number
+// 2. options.parent must be a positive number
+// 3. options.exclusive must be a boolean
+// 4. if specified, options.silent must be a boolean
+//
+// Also sets the default priority options if they are not set.
 function validatePriorityOptions(options) {
+  let err;
   if (options.weight === undefined) {
     options.weight = NGHTTP2_DEFAULT_WEIGHT;
   } else if (typeof options.weight !== 'number') {
-    const err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                     'weight',
-                                     options.weight);
-    Error.captureStackTrace(err, validatePriorityOptions);
-    throw err;
+    err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
+                               'weight',
+                               options.weight);
   }
 
   if (options.parent === undefined) {
     options.parent = 0;
   } else if (typeof options.parent !== 'number' || options.parent < 0) {
-    const err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                     'parent',
-                                     options.parent);
-    Error.captureStackTrace(err, validatePriorityOptions);
-    throw err;
+    err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
+                               'parent',
+                               options.parent);
   }
 
   if (options.exclusive === undefined) {
     options.exclusive = false;
   } else if (typeof options.exclusive !== 'boolean') {
-    const err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                     'exclusive',
-                                     options.exclusive);
-    Error.captureStackTrace(err, validatePriorityOptions);
-    throw err;
+    err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
+                               'exclusive',
+                               options.exclusive);
   }
 
   if (options.silent === undefined) {
     options.silent = false;
   } else if (typeof options.silent !== 'boolean') {
-    const err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                     'silent',
-                                     options.silent);
+    err = new errors.TypeError('ERR_INVALID_OPT_VALUE',
+                               'silent',
+                               options.silent);
+  }
+
+  if (err) {
     Error.captureStackTrace(err, validatePriorityOptions);
     throw err;
   }
 }
 
+// When an error occurs internally at the binding level, immediately
+// destroy the session.
 function onSessionInternalError(code) {
-  const owner = this[kOwner];
-  const err = new NghttpError(code);
-  process.nextTick(emit, owner, 'error', err);
-}
-
-// Creates the internal binding.Http2Session handle for an Http2Session
-// instance. This occurs only after the socket connection has been
-// established. Note: the binding.Http2Session will take over ownership
-// of the socket. No other code should read from or write to the socket.
-function setupHandle(session, socket, type, options) {
-  return function() {
-    debug(`Http2Session ${sessionName(type)}: setting up session handle`);
-    session[kState].connecting = false;
-
-    updateOptionsBuffer(options);
-    const handle = new binding.Http2Session(type);
-    handle[kOwner] = session;
-    handle.error = onSessionInternalError;
-    handle.onpriority = onPriority;
-    handle.onsettings = onSettings;
-    handle.onheaders = onSessionHeaders;
-    handle.onerror = onSessionError;
-    handle.onframeerror = onFrameError;
-    handle.ongoawaydata = onGoawayData;
-
-    if (typeof options.selectPadding === 'function')
-      handle.ongetpadding = onSelectPadding(options.selectPadding);
-
-    assert(socket._handle !== undefined,
-           'Internal HTTP/2 Failure. The socket is not connected. Please ' +
-           'report this as a bug in Node.js');
-    handle.consume(socket._handle._externalStream);
-
-    session[kHandle] = handle;
-
-    const settings = typeof options.settings === 'object' ?
-      options.settings : Object.create(null);
-
-    session.settings(settings);
-    process.nextTick(emit, session, 'connect', session, socket);
-  };
+  if (this[kOwner] !== undefined)
+    this[kOwner].destroy(new NghttpError(code));
 }
 
 // Submits a SETTINGS frame to be sent to the remote peer.
 function submitSettings(settings) {
-  const type = this[kType];
-  debug(`Http2Session ${sessionName(type)}: submitting settings`);
+  if (this.destroyed)
+    return;
+  debug(`Http2Session ${sessionName(this[kType])}: submitting settings`);
   this[kUpdateTimer]();
   this[kLocalSettings] = undefined;
   updateSettingsBuffer(settings);
@@ -523,6 +553,8 @@ function submitSettings(settings) {
 // Note: If the silent option is true, the change will be made
 // locally with no PRIORITY frame sent.
 function submitPriority(options) {
+  if (this.destroyed)
+    return;
   this[kUpdateTimer]();
 
   // If the parent is the id, do nothing because a
@@ -536,74 +568,16 @@ function submitPriority(options) {
                          !!options.silent);
 }
 
-// Submit an RST-STREAM frame to be sent to the remote peer.
-// This will cause the Http2Stream to be closed.
-function submitRstStream(code) {
-  this[kUpdateTimer]();
-
-  const state = this[kState];
-  if (state.rst) return;
-  state.rst = true;
-  state.rstCode = code;
-
-  const ret = this[kHandle].rstStream(code);
-  if (ret < 0) {
-    const err = new NghttpError(ret);
-    process.nextTick(emit, this, 'error', err);
-    return;
-  }
-  this.destroy();
-}
-
-function doShutdown(options) {
-  const handle = this[kHandle];
-  const state = this[kState];
-  if (handle === undefined || state.shutdown)
-    return; // Nothing to do, possibly because the session shutdown already.
-  const ret = handle.goaway(options.errorCode | 0,
-                            options.lastStreamID | 0,
-                            options.opaqueData);
-  state.shuttingDown = false;
-  state.shutdown = true;
-  if (ret < 0) {
-    debug(`Http2Session ${sessionName(this[kType])}: shutdown failed`);
-    const err = new NghttpError(ret);
-    process.nextTick(emit, this, 'error', err);
+// Submit a GOAWAY frame to be sent to the remote peer.
+// If the lastStreamID is set to <= 0, then the lastProcStreamID will
+// be used. The opaqueData must either be a typed array or undefined
+// (which will be checked elsewhere).
+function submitGoaway(code, lastStreamID, opaqueData) {
+  if (this.destroyed)
     return;
-  }
-  process.nextTick(emit, this, 'shutdown', options);
-}
-
-// Submit a graceful or immediate shutdown request for the Http2Session.
-function submitShutdown(options) {
-  const type = this[kType];
-  debug(`Http2Session ${sessionName(type)}: submitting shutdown request`);
-  const shutdownFn = doShutdown.bind(this, options);
-  if (type === NGHTTP2_SESSION_SERVER && options.graceful === true) {
-    // first send a shutdown notice
-    this[kHandle].shutdownNotice();
-    // then, on flip of the event loop, do the actual shutdown
-    setImmediate(shutdownFn);
-    return;
-  }
-  shutdownFn();
-}
-
-function finishSessionDestroy(socket) {
-  if (!socket.destroyed)
-    socket.destroy();
-
-  const state = this[kState];
-  state.destroying = false;
-  state.destroyed = true;
-
-  // Destroy the handle
-  if (this[kHandle] !== undefined) {
-    this[kHandle].destroy(state.skipUnconsume);
-    this[kHandle] = undefined;
-  }
-
-  process.nextTick(emit, this, 'close');
+  debug(`Http2Session ${sessionName(this[kType])}: submitting goaway`);
+  this[kUpdateTimer]();
+  this[kHandle].goaway(code, lastStreamID, opaqueData);
 }
 
 const proxySocketHandler = {
@@ -648,13 +622,27 @@ const proxySocketHandler = {
   }
 };
 
+// pingCallback() returns a function that is invoked when an HTTP2 PING
+// frame acknowledgement is received. The ack is either true or false to
+// indicate if the ping was successful or not. The duration indicates the
+// number of milliseconds elapsed since the ping was sent and the ack
+// received. The payload is a Buffer containing the 8 bytes of payload
+// data received on the PING acknowlegement.
 function pingCallback(cb) {
-  return function(ack, duration, payload) {
+  return function pingCallback(ack, duration, payload) {
     const err = ack ? null : new errors.Error('ERR_HTTP2_PING_CANCEL');
     cb(err, duration, payload);
   };
 }
 
+// Validates the values in a settings object. Specifically:
+// 1. headerTableSize must be a number in the range 0 <= n <= kMaxInt
+// 2. initialWindowSize must be a number in the range 0 <= n <= kMaxInt
+// 3. maxFrameSize must be a number in the range 16384 <= n <= kMaxFrameSize
+// 4. maxConcurrentStreams must be a number in the range 0 <= n <= kMaxStreams
+// 5. maxHeaderListSize must be a number in the range 0 <= n <= kMaxInt
+// 6. enablePush must be a boolean
+// All settings are optional and may be left undefined
 function validateSettings(settings) {
   settings = Object.assign({}, settings);
   assertWithinRange('headerTableSize',
@@ -677,13 +665,98 @@ function validateSettings(settings) {
     const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE',
                                      'enablePush', settings.enablePush);
     err.actual = settings.enablePush;
+    Error.captureStackTrace(err, 'validateSettings');
     throw err;
   }
   return settings;
 }
 
+// Creates the internal binding.Http2Session handle for an Http2Session
+// instance. This occurs only after the socket connection has been
+// established. Note: the binding.Http2Session will take over ownership
+// of the socket. No other code should read from or write to the socket.
+function setupHandle(socket, type, options) {
+  // If the session has been destroyed, go ahead and emit 'connect',
+  // but do nothing else. The various on('connect') handlers set by
+  // core will check for session.destroyed before progressing, this
+  // ensures that those at l`east get cleared out.
+  if (this.destroyed) {
+    process.nextTick(emit, this, 'connect', this, socket);
+    return;
+  }
+  debug(`Http2Session ${sessionName(type)}: setting up session handle`);
+  this[kState].flags |= SESSION_FLAGS_READY;
+
+  updateOptionsBuffer(options);
+  const handle = new binding.Http2Session(type);
+  handle[kOwner] = this;
+  handle.error = onSessionInternalError;
+  handle.onpriority = onPriority;
+  handle.onsettings = onSettings;
+  handle.onheaders = onSessionHeaders;
+  handle.onframeerror = onFrameError;
+  handle.ongoawaydata = onGoawayData;
+
+  if (typeof options.selectPadding === 'function')
+    handle.ongetpadding = onSelectPadding(options.selectPadding);
+
+  assert(socket._handle !== undefined,
+         'Internal HTTP/2 Failure. The socket is not connected. Please ' +
+         'report this as a bug in Node.js');
+  handle.consume(socket._handle._externalStream);
+
+  this[kHandle] = handle;
+
+  const settings = typeof options.settings === 'object' ?
+    options.settings : {};
+
+  this.settings(settings);
+  process.nextTick(emit, this, 'connect', this, socket);
+}
+
+// Emits a close event followed by an error event if err is truthy. Used
+// by Http2Session.prototype.destroy()
+function emitClose(self, error) {
+  if (error)
+    self.emit('error', error);
+  self.emit('close');
+}
+
 // Upon creation, the Http2Session takes ownership of the socket. The session
 // may not be ready to use immediately if the socket is not yet fully connected.
+// In that case, the Http2Session will wait for the socket to connect. Once
+// the Http2Session is ready, it will emit its own 'connect' event.
+//
+// The Http2Session.goaway() method will send a GOAWAY frame, signalling
+// to the connected peer that a shutdown is in progress. Sending a goaway
+// frame has no other effect, however.
+//
+// Receiving a GOAWAY frame will cause the Http2Session to first emit a 'goaway'
+// event notifying the user that a shutdown is in progress. If the goaway
+// error code equals 0 (NGHTTP2_NO_ERROR), session.close() will be called,
+// causing the Http2Session to send its own GOAWAY frame and switch itself
+// into a graceful closing state. In this state, new inbound or outbound
+// Http2Streams will be rejected. Existing *pending* streams (those created
+// but without an assigned stream ID or handle) will be destroyed with a
+// cancel error. Existing open streams will be permitted to complete on their
+// own. Once all existing streams close, session.destroy() will be called
+// automatically.
+//
+// Calling session.destroy() will tear down the Http2Session immediately,
+// making it no longer usable. Pending and existing streams will be destroyed.
+// The bound socket will be destroyed. Once all resources have been freed up,
+// the 'close' event will be emitted. Note that pending streams will be
+// destroyed using a specific "ERR_HTTP2_STREAM_CANCEL" error. Existing open
+// streams will be destroyed using the same error passed to session.destroy()
+//
+// If destroy is called with an error, an 'error' event will be emitted
+// immediately following the 'close' event.
+//
+// The socket and Http2Session lifecycles are tightly bound. Once one is
+// destroyed, the other should also be destroyed. When the socket is destroyed
+// with an error, session.destroy() will be called with that same error.
+// Likewise, when session.destroy() is called with an error, the same error
+// will be sent to the socket.
 class Http2Session extends EventEmitter {
   constructor(type, options, socket) {
     super();
@@ -704,10 +777,9 @@ class Http2Session extends EventEmitter {
     socket[kSession] = this;
 
     this[kState] = {
+      flags: SESSION_FLAGS_PENDING,
       streams: new Map(),
-      destroyed: false,
-      shutdown: false,
-      shuttingDown: false,
+      pendingStreams: new Set(),
       pendingAck: 0,
       maxPendingAck: Math.max(1, (options.maxPendingAck | 0) || 10),
       writeQueueSize: 0
@@ -725,12 +797,8 @@ class Http2Session extends EventEmitter {
     if (typeof socket.disableRenegotiation === 'function')
       socket.disableRenegotiation();
 
-    socket[kDestroySocket] = socket.destroy;
-    socket.destroy = socketDestroy;
-
-    const setupFn = setupHandle(this, socket, type, options);
+    const setupFn = setupHandle.bind(this, socket, type, options);
     if (socket.connecting) {
-      this[kState].connecting = true;
       const connectEvent =
         socket instanceof tls.TLSSocket ? 'secureConnect' : 'connect';
       socket.once(connectEvent, setupFn);
@@ -738,22 +806,38 @@ class Http2Session extends EventEmitter {
       setupFn();
     }
 
-    // Any individual session can have any number of active open
-    // streams, these may all need to be made aware of changes
-    // in state that occur -- such as when the associated socket
-    // is closed. To do so, we need to set the max listener count
-    // to something more reasonable because we may have any number
-    // of concurrent streams (2^31-1 is the upper limit on the number
-    // of streams)
-    this.setMaxListeners(kMaxStreams);
     debug(`Http2Session ${sessionName(type)}: created`);
   }
 
+  // True if the Http2Session is still waiting for the socket to connect
+  get connecting() {
+    return (this[kState].flags & SESSION_FLAGS_READY) === 0;
+  }
+
+  // True if Http2Session.prototype.close() has been called
+  get closed() {
+    return !!(this[kState].flags & SESSION_FLAGS_CLOSED);
+  }
+
+  // True if Http2Session.prototype.destroy() has been called
+  get destroyed() {
+    return !!(this[kState].flags & SESSION_FLAGS_DESTROYED);
+  }
+
+  // Resets the timeout counter
   [kUpdateTimer]() {
+    if (this.destroyed)
+      return;
     _unrefActive(this);
   }
 
+  // Sets the id of the next stream to be created by this Http2Session.
+  // The value must be a number in the range 0 <= n <= kMaxStreams. The
+  // value also needs to be larger than the current next stream ID.
   setNextStreamID(id) {
+    if (this.destroyed)
+      throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
     if (typeof id !== 'number')
       throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'id', 'number');
     if (id <= 0 || id > kMaxStreams)
@@ -761,10 +845,13 @@ class Http2Session extends EventEmitter {
     this[kHandle].setNextStreamID(id);
   }
 
+  // If ping is called while we are still connecting, or after close() has
+  // been called, the ping callback will be invoked immediately will a ping
+  // cancelled error and a duration of 0.0.
   ping(payload, callback) {
-    const state = this[kState];
-    if (state.destroyed || state.destroying)
+    if (this.destroyed)
       throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
     if (typeof payload === 'function') {
       callback = payload;
       payload = undefined;
@@ -779,17 +866,21 @@ class Http2Session extends EventEmitter {
     }
     if (typeof callback !== 'function')
       throw new errors.TypeError('ERR_INVALID_CALLBACK');
-    return this[kHandle].ping(payload, pingCallback(callback));
+
+    const cb = pingCallback(callback);
+    if (this.connecting || this.closed) {
+      process.nextTick(cb, false, 0.0, payload);
+      return;
+    }
+
+    return this[kHandle].ping(payload, cb);
   }
 
   [kInspect](depth, opts) {
-    const state = this[kState];
     const obj = {
       type: this[kType],
-      destroyed: state.destroyed,
-      destroying: state.destroying,
-      shutdown: state.shutdown,
-      shuttingDown: state.shuttingDown,
+      closed: this.closed,
+      destroyed: this.destroyed,
       state: this.state,
       localSettings: this.localSettings,
       remoteSettings: this.remoteSettings
@@ -810,62 +901,60 @@ class Http2Session extends EventEmitter {
     return this[kType];
   }
 
+  // If a GOAWAY frame has been received, gives the error code specified
+  get goawayCode() {
+    return this[kState].goawayCode || NGHTTP2_NO_ERROR;
+  }
+
+  // If a GOAWAY frame has been received, gives the last stream ID reported
+  get goawayLastStreamID() {
+    return this[kState].goawayLastStreamID || 0;
+  }
+
   // true if the Http2Session is waiting for a settings acknowledgement
   get pendingSettingsAck() {
     return this[kState].pendingAck > 0;
   }
 
-  // true if the Http2Session has been destroyed
-  get destroyed() {
-    return this[kState].destroyed;
-  }
-
   // Retrieves state information for the Http2Session
   get state() {
-    const handle = this[kHandle];
-    return handle === undefined ? {} : getSessionState(handle);
+    return this.connecting || this.destroyed ?
+      {} : getSessionState(this[kHandle]);
   }
 
   // The settings currently in effect for the local peer. These will
   // be updated only when a settings acknowledgement has been received.
   get localSettings() {
-    let settings = this[kLocalSettings];
+    const settings = this[kLocalSettings];
     if (settings !== undefined)
       return settings;
 
-    const handle = this[kHandle];
-    if (handle === undefined)
+    if (this.destroyed || this.connecting)
       return {};
 
-    settings = getSettings(handle, false); // Local
-    this[kLocalSettings] = settings;
-    return settings;
+    return this[kLocalSettings] = getSettings(this[kHandle], false); // Local
   }
 
   // The settings currently in effect for the remote peer.
   get remoteSettings() {
-    let settings = this[kRemoteSettings];
+    const settings = this[kRemoteSettings];
     if (settings !== undefined)
       return settings;
 
-    const handle = this[kHandle];
-    if (handle === undefined)
+    if (this.destroyed || this.connecting)
       return {};
 
-    settings = getSettings(handle, true); // Remote
-    this[kRemoteSettings] = settings;
-    return settings;
+    return this[kRemoteSettings] = getSettings(this[kHandle], true); // Remote
   }
 
   // Submits a SETTINGS frame to be sent to the remote peer.
   settings(settings) {
-    const state = this[kState];
-    if (state.destroyed || state.destroying)
+    if (this.destroyed)
       throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
 
-    // Validate the input first
     assertIsObject(settings, 'settings');
     settings = validateSettings(settings);
+    const state = this[kState];
     if (state.pendingAck === state.maxPendingAck) {
       throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
                              this[kState].pendingAck);
@@ -874,107 +963,144 @@ class Http2Session extends EventEmitter {
 
     state.pendingAck++;
     const settingsFn = submitSettings.bind(this, settings);
-    if (state.connecting) {
+    if (this.connecting) {
       this.once('connect', settingsFn);
       return;
     }
     settingsFn();
   }
 
-  // Destroy the Http2Session
-  destroy() {
-    const state = this[kState];
-    if (state.destroyed || state.destroying)
-      return;
-    debug(`Http2Session ${sessionName(this[kType])}: destroying`);
-    state.destroying = true;
-    state.destroyed = false;
-
-    // Unenroll the timer
-    this.setTimeout(0, sessionOnTimeout);
-
-    // Shut down any still open streams
-    const streams = state.streams;
-    streams.forEach((stream) => stream.destroy());
-
-    // Disassociate from the socket and server
-    const socket = this[kSocket];
-    // socket.pause();
-    delete this[kProxySocket];
-    delete this[kSocket];
-    delete this[kServer];
+  // Sumits a GOAWAY frame to be sent to the remote peer. Note that this
+  // is only a notification, and does not affect the usable state of the
+  // session with the notable exception that new incoming streams will
+  // be rejected automatically.
+  goaway(code = NGHTTP2_NO_ERROR, lastStreamID = 0, opaqueData) {
+    if (this.destroyed)
+      throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
 
-    if (this[kHandle] !== undefined)
-      this[kHandle].destroying();
+    if (opaqueData !== undefined && !isArrayBufferView(opaqueData)) {
+      throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+                                 'opaqueData',
+                                 ['Buffer', 'TypedArray', 'DataView']);
+    }
+    if (typeof code !== 'number') {
+      throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number');
+    }
+    if (typeof lastStreamID !== 'number') {
+      throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
+                                 'lastStreamID', 'number');
+    }
 
-    setImmediate(finishSessionDestroy.bind(this), socket);
+    const goawayFn = submitGoaway.bind(this, code, lastStreamID, opaqueData);
+    if (this.connecting) {
+      this.once('connect', goawayFn);
+      return;
+    }
+    goawayFn();
   }
 
-  // Graceful or immediate shutdown of the Http2Session. Graceful shutdown
-  // is only supported on the server-side
-  shutdown(options, callback) {
-    const state = this[kState];
-    if (state.destroyed || state.destroying)
-      throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
-
-    if (state.shutdown || state.shuttingDown)
+  // Destroy the Http2Session, making it no longer usable and cancelling
+  // any pending activity.
+  destroy(error = NGHTTP2_NO_ERROR, code) {
+    if (this.destroyed)
       return;
+    debug(`Http2Session ${sessionName(this[kType])}: destroying`);
 
-    const type = this[kType];
-
-    if (typeof options === 'function') {
-      callback = options;
-      options = undefined;
+    if (typeof error === 'number') {
+      code = error;
+      error =
+        code !== NGHTTP2_NO_ERROR ?
+          new errors.Error('ERR_HTTP2_SESSION_ERROR', code) : undefined;
     }
+    if (code === undefined && error != null)
+      code = NGHTTP2_INTERNAL_ERROR;
 
-    assertIsObject(options, 'options');
-    options = Object.assign(Object.create(null), options);
+    const state = this[kState];
+    state.flags |= SESSION_FLAGS_DESTROYED;
 
-    if (options.opaqueData !== undefined &&
-        !isArrayBufferView(options.opaqueData)) {
-      throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                 'opaqueData',
-                                 options.opaqueData);
-    }
-    if (type === NGHTTP2_SESSION_SERVER &&
-        options.graceful !== undefined &&
-        typeof options.graceful !== 'boolean') {
-      throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                 'graceful',
-                                 options.graceful);
-    }
-    if (options.errorCode !== undefined &&
-        typeof options.errorCode !== 'number') {
-      throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                 'errorCode',
-                                 options.errorCode);
-    }
-    if (options.lastStreamID !== undefined &&
-        (typeof options.lastStreamID !== 'number' ||
-         options.lastStreamID < 0)) {
-      throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
-                                 'lastStreamID',
-                                 options.lastStreamID);
-    }
+    // Clear timeout and remove timeout listeners
+    unenroll(this);
+    this.removeAllListeners('timeout');
+
+    // Destroy any pending and open streams
+    const cancel = new errors.Error('ERR_HTTP2_STREAM_CANCEL');
+    state.pendingStreams.forEach((stream) => stream.destroy(cancel));
+    state.streams.forEach((stream) => stream.destroy(error));
+
+    // Disassociate from the socket and server
+    const socket = this[kSocket];
+    const handle = this[kHandle];
 
-    debug(`Http2Session ${sessionName(type)}: initiating shutdown`);
-    state.shuttingDown = true;
+    // Destroy the handle if it exists at this point
+    if (handle !== undefined)
+      handle.destroy(code, socket.destroyed);
 
-    if (callback) {
-      this.on('shutdown', callback);
+    // If there is no error, use setImmediate to destroy the socket on the
+    // next iteration of the event loop in order to give data time to transmit.
+    // Otherwise, destroy immediately.
+    if (!socket.destroyed) {
+      if (!error) {
+        setImmediate(socket.end.bind(socket));
+      } else {
+        socket.destroy(error);
+      }
     }
 
-    const shutdownFn = submitShutdown.bind(this, options);
-    if (state.connecting) {
-      this.once('connect', shutdownFn);
+    this[kProxySocket] = undefined;
+    this[kSocket] = undefined;
+    this[kHandle] = undefined;
+    socket[kSession] = undefined;
+    socket[kServer] = undefined;
+
+    // Finally, emit the close and error events (if necessary) on next tick.
+    process.nextTick(emitClose, this, error);
+  }
+
+  // Closing the session will:
+  // 1. Send a goaway frame
+  // 2. Mark the session as closed
+  // 3. Prevent new inbound or outbound streams from being opened
+  // 4. Optionally register a 'close' event handler
+  // 5. Will cause the session to automatically destroy after the
+  //    last currently open Http2Stream closes.
+  //
+  // Close always assumes a good, non-error shutdown (NGHTTP_NO_ERROR)
+  //
+  // If the session has not connected yet, the closed flag will still be
+  // set but the goaway will not be sent until after the connect event
+  // is emitted.
+  close(callback) {
+    if (this.closed || this.destroyed)
       return;
+    debug(`Http2Session ${sessionName(this[kType])}: marking session closed`);
+    this[kState].flags |= SESSION_FLAGS_CLOSED;
+    if (typeof callback === 'function')
+      this.once('close', callback);
+    this.goaway();
+    this[kMaybeDestroy]();
+  }
+
+  // Destroy the session if:
+  // * error is not undefined/null
+  // * session is closed and there are no more pending or open streams
+  [kMaybeDestroy](error) {
+    if (error == null) {
+      const state = this[kState];
+      // Do not destroy if we're not closed and there are pending/open streams
+      if (!this.closed ||
+          state.streams.size > 0 ||
+          state.pendingStreams.size > 0) {
+        return;
+      }
     }
-
-    debug(`Http2Session ${sessionName(type)}: sending shutdown`);
-    shutdownFn();
+    this.destroy(error);
   }
 
   _onTimeout() {
+    // If the session is destroyed, this should never actually be invoked,
+    // but just in case...
+    if (this.destroyed)
+      return;
     // This checks whether a write is currently in progress and also whether
     // that write is actually sending data across the write. The kHandle
     // stored `chunksSentSinceLastWrite` is only updated when a timeout event
@@ -995,6 +1121,9 @@ class Http2Session extends EventEmitter {
   }
 }
 
+// ServerHttp2Session instances should never have to wait for the socket
+// to connect as they are always created after the socket has already been
+// established.
 class ServerHttp2Session extends Http2Session {
   constructor(options, socket, server) {
     super(NGHTTP2_SESSION_SERVER, options, socket);
@@ -1006,6 +1135,10 @@ class ServerHttp2Session extends Http2Session {
   }
 }
 
+// ClientHttp2Session instances have to wait for the socket to connect after
+// they have been created. Various operations such as request() may be used,
+// but the actual protocol communication will only occur after the socket
+// has been connected.
 class ClientHttp2Session extends Http2Session {
   constructor(options, socket) {
     super(NGHTTP2_SESSION_CLIENT, options, socket);
@@ -1014,11 +1147,14 @@ class ClientHttp2Session extends Http2Session {
   // Submits a new HTTP2 request to the connected peer. Returns the
   // associated Http2Stream instance.
   request(headers, options) {
-    const state = this[kState];
-    if (state.destroyed || state.destroying)
-      throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
     debug(`Http2Session ${sessionName(this[kType])}: initiating request`);
 
+    if (this.destroyed)
+      throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
+    if (this.closed)
+      throw new errors.Error('ERR_HTTP2_GOAWAY_SESSION');
+
     this[kUpdateTimer]();
 
     assertIsObject(headers, 'headers');
@@ -1068,15 +1204,19 @@ class ClientHttp2Session extends Http2Session {
                                  options.getTrailers);
     }
 
+    const headersList = mapToHeaders(headers);
+    if (!Array.isArray(headersList))
+      throw headersList;
+
     const stream = new ClientHttp2Stream(this, undefined, undefined, {});
 
     // Close the writable side of the stream if options.endStream is set.
     if (options.endStream)
       stream.end();
 
-    const onConnect = requestOnConnect.bind(stream, headers, options);
-    if (state.connecting) {
-      stream.on('connect', onConnect);
+    const onConnect = requestOnConnect.bind(stream, headersList, options);
+    if (this.connecting) {
+      this.on('connect', onConnect);
     } else {
       onConnect();
     }
@@ -1124,60 +1264,45 @@ function afterDoStreamWrite(status, handle, req) {
 
   if (session !== undefined)
     session[kState].writeQueueSize -= bytes;
-
   if (typeof req.callback === 'function')
-    req.callback();
+    req.callback(null);
   req.handle = undefined;
 }
 
 function onHandleFinish() {
+  const handle = this[kHandle];
   if (this[kID] === undefined) {
     this.once('ready', onHandleFinish);
-  } else {
-    const handle = this[kHandle];
-    if (handle !== undefined) {
-      const req = new ShutdownWrap();
-      req.oncomplete = () => {};
-      req.handle = handle;
-      handle.shutdown(req);
-    }
+  } else if (handle !== undefined) {
+    const req = new ShutdownWrap();
+    req.oncomplete = () => {};
+    req.handle = handle;
+    handle.shutdown(req);
   }
 }
 
-function onSessionClose(hadError, code) {
-  abort(this);
-  this.push(null);  // Close the readable side
-  this.end();       // Close the writable side
-}
-
 function streamOnResume() {
-  if (this[kID] === undefined) {
-    this.once('ready', streamOnResume);
-    return;
-  }
-  this[kHandle].readStart();
+  if (!this.destroyed && !this.pending)
+    this[kHandle].readStart();
 }
 
 function streamOnPause() {
-  this[kHandle].readStop();
-}
-
-function handleFlushData(handle) {
-  handle.flushData();
+  // if (!this.destroyed && !this.pending)
+  //   this[kHandle].readStop();
 }
 
-function streamOnSessionConnect() {
-  const session = this[kSession];
-  debug(`Http2Session ${sessionName(session[kType])}: session connected`);
-  this[kState].connecting = false;
-  process.nextTick(emit, this, 'connect');
+function handleFlushData(self) {
+  if (!this.destroyed && !this.pending)
+    this[kHandle].flushData();
 }
 
+// If the writable side of the Http2Stream is still open, emit the
+// 'aborted' event and set the aborted flag.
 function abort(stream) {
-  if (!stream[kState].aborted &&
+  if (!stream.aborted &&
       !(stream._writableState.ended || stream._writableState.ending)) {
-    stream.emit('aborted');
-    stream[kState].aborted = true;
+    process.nextTick(emit, stream, 'aborted');
+    stream[kState].flags |= STREAM_FLAGS_ABORTED;
   }
 }
 
@@ -1189,37 +1314,41 @@ class Http2Stream extends Duplex {
     options.decodeStrings = false;
     super(options);
     this[async_id_symbol] = -1;
+
+    // Corking the stream automatically allows writes to happen
+    // but ensures that those are buffered until the handle has
+    // been assigned.
     this.cork();
     this[kSession] = session;
+    session[kState].pendingStreams.add(this);
 
-    const state = this[kState] = {
-      rst: false,
+    this[kState] = {
+      flags: STREAM_FLAGS_PENDING,
       rstCode: NGHTTP2_NO_ERROR,
-      headersSent: false,
-      headRequest: false,
-      aborted: false,
-      closeHandler: onSessionClose.bind(this),
       writeQueueSize: 0
     };
 
     this.once('finish', onHandleFinish);
     this.on('resume', streamOnResume);
     this.on('pause', streamOnPause);
-    session.once('close', state.closeHandler);
-
-    if (session[kState].connecting) {
-      state.connecting = true;
-      session.once('connect', streamOnSessionConnect.bind(this));
-    }
   }
 
   [kUpdateTimer]() {
+    if (this.destroyed)
+      return;
     _unrefActive(this);
     if (this[kSession])
       _unrefActive(this[kSession]);
   }
 
   [kInit](id, handle) {
+    const state = this[kState];
+    state.flags |= STREAM_FLAGS_READY;
+
+    const session = this[kSession];
+    session[kState].pendingStreams.delete(this);
+    session[kState].streams.set(id, this);
+
     this[kID] = id;
     this[async_id_symbol] = handle.getAsyncId();
     handle[kOwner] = this;
@@ -1233,7 +1362,9 @@ class Http2Stream extends Duplex {
 
   [kInspect](depth, opts) {
     const obj = {
-      id: this[kID],
+      id: this[kID] || '<pending>',
+      closed: this.closed,
+      destroyed: this.destroyed,
       state: this.state,
       readableState: this._readableState,
       writableState: this._writableState
@@ -1241,6 +1372,10 @@ class Http2Stream extends Duplex {
     return `Http2Stream ${util.format(obj)}`;
   }
 
+  get pending() {
+    return this[kID] === undefined;
+  }
+
   // The id of the Http2Stream, will be undefined if the socket is not
   // yet connected.
   get id() {
@@ -1253,6 +1388,8 @@ class Http2Stream extends Duplex {
   }
 
   _onTimeout() {
+    if (this.destroyed)
+      return;
     // This checks whether a write is currently in progress and also whether
     // that write is actually sending data across the write. The kHandle
     // stored `chunksSentSinceLastWrite` is only updated when a timeout event
@@ -1272,19 +1409,24 @@ class Http2Stream extends Duplex {
     process.nextTick(emit, this, 'timeout');
   }
 
+  // true if the HEADERS frame has been sent
+  get headersSent() {
+    return !!(this[kState].flags & STREAM_FLAGS_HEADERS_SENT);
+  }
+
   // true if the Http2Stream was aborted abnormally.
   get aborted() {
-    return this[kState].aborted;
+    return !!(this[kState].flags & STREAM_FLAGS_ABORTED);
   }
 
   // true if dealing with a HEAD request
   get headRequest() {
-    return this[kState].headRequest;
+    return !!(this[kState].flags & STREAM_FLAGS_HEAD_REQUEST);
   }
 
   // The error code reported when this Http2Stream was closed.
   get rstCode() {
-    return this[kState].rst ? this[kState].rstCode : undefined;
+    return this[kState].rstCode;
   }
 
   // State information for the Http2Stream
@@ -1302,14 +1444,26 @@ class Http2Stream extends Duplex {
   }
 
   _write(data, encoding, cb) {
-    if (this[kID] === undefined) {
+    // When the Http2Stream is first created, it is corked until the
+    // handle and the stream ID is assigned. However, if the user calls
+    // uncork() before that happens, the Duplex will attempt to pass
+    // writes through. Those need to be queued up here.
+    if (this.pending) {
       this.once('ready', this._write.bind(this, data, encoding, cb));
       return;
     }
 
-    this[kUpdateTimer]();
+    // If the stream has been destroyed, there's nothing else we can do
+    // because the handle has been destroyed. This should only be an
+    // issue if a write occurs before the 'ready' event in the case where
+    // the duplex is uncorked before the stream is ready to go. In that
+    // case, drop the data on the floor. An error should have already been
+    // emitted.
+    if (this.destroyed)
+      return;
 
-    if (!this[kState].headersSent)
+    this[kUpdateTimer]();
+    if (!this.headersSent)
       this[kProceed]();
 
     const handle = this[kHandle];
@@ -1326,14 +1480,27 @@ class Http2Stream extends Duplex {
   }
 
   _writev(data, cb) {
-    if (this[kID] === undefined) {
+    // When the Http2Stream is first created, it is corked until the
+    // handle and the stream ID is assigned. However, if the user calls
+    // uncork() before that happens, the Duplex will attempt to pass
+    // writes through. Those need to be queued up here.
+    if (this.pending) {
       this.once('ready', this._writev.bind(this, data, cb));
       return;
     }
 
+    // If the stream has been destroyed, there's nothing else we can do
+    // because the handle has been destroyed. This should only be an
+    // issue if a write occurs before the 'ready' event in the case where
+    // the duplex is uncorked before the stream is ready to go. In that
+    // case, drop the data on the floor. An error should have already been
+    // emitted.
+    if (this.destroyed)
+      return;
+
     this[kUpdateTimer]();
 
-    if (!this[kState].headersSent)
+    if (!this.headersSent)
       this[kProceed]();
 
     const handle = this[kHandle];
@@ -1360,47 +1527,12 @@ class Http2Stream extends Duplex {
       this.push(null);
       return;
     }
-    if (this[kHandle] !== undefined)
-      process.nextTick(handleFlushData, this[kHandle]);
-  }
-
-  // Submits an RST-STREAM frame to shutdown this stream.
-  // If the stream ID has not yet been allocated, the action will
-  // defer until the ready event is emitted.
-  // After sending the rstStream, this.destroy() will be called making
-  // the stream object no longer usable.
-  rstStream(code = NGHTTP2_NO_ERROR) {
-    if (typeof code !== 'number')
-      throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number');
-    if (code < 0 || code > kMaxInt)
-      throw new errors.RangeError('ERR_OUT_OF_RANGE', 'code');
-
-    const rstStreamFn = submitRstStream.bind(this, code);
-    if (this[kID] === undefined) {
-      this.once('ready', rstStreamFn);
-      return;
+    const flushfn = handleFlushData.bind(this);
+    if (!this.pending) {
+      flushfn();
+    } else {
+      this.once('ready', flushfn);
     }
-    rstStreamFn();
-  }
-
-  rstWithNoError() {
-    this.rstStream(NGHTTP2_NO_ERROR);
-  }
-
-  rstWithProtocolError() {
-    this.rstStream(NGHTTP2_PROTOCOL_ERROR);
-  }
-
-  rstWithCancel() {
-    this.rstStream(NGHTTP2_CANCEL);
-  }
-
-  rstWithRefuse() {
-    this.rstStream(NGHTTP2_REFUSED_STREAM);
-  }
-
-  rstWithInternalError() {
-    this.rstStream(NGHTTP2_INTERNAL_ERROR);
   }
 
   priority(options) {
@@ -1412,86 +1544,144 @@ class Http2Stream extends Duplex {
     validatePriorityOptions(options);
 
     const priorityFn = submitPriority.bind(this, options);
-    if (this[kID] === undefined) {
+
+    // If the handle has not yet been assigned, queue up the priority
+    // frame to be sent as soon as the ready event is emitted.
+    if (this.pending) {
       this.once('ready', priorityFn);
       return;
     }
     priorityFn();
   }
 
-  // Called by this.destroy().
-  // * If called before the stream is allocated, will defer until the
-  //   ready event is emitted.
-  // * Will submit an RST stream to shutdown the stream if necessary.
-  //   This will cause the internal resources to be released.
-  // * Then cleans up the resources on the js side
-  _destroy(err, callback) {
-    const session = this[kSession];
-    if (this[kID] === undefined) {
-      this.once('ready', this._destroy.bind(this, err, callback));
-      return;
-    }
+  get closed() {
+    return !!(this[kState].flags & STREAM_FLAGS_CLOSED);
+  }
+
+  // Close initiates closing the Http2Stream instance by sending an RST_STREAM
+  // frame to the connected peer. The readable and writable sides of the
+  // Http2Stream duplex are closed and the timeout timer is unenrolled. If
+  // a callback is passed, it is registered to listen for the 'close' event.
+  //
+  // If the handle and stream ID have not been assigned yet, the close
+  // will be queued up to wait for the ready event. As soon as the stream ID
+  // is determined, the close will proceed.
+  //
+  // Submitting the RST_STREAM frame to the underlying handle will cause
+  // the Http2Stream to be closed and ultimately destroyed. After calling
+  // close, it is still possible to queue up PRIORITY and RST_STREAM frames,
+  // but no DATA and HEADERS frames may be sent.
+  close(code = NGHTTP2_NO_ERROR, callback) {
+    if (typeof code !== 'number')
+      throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number');
+    if (code < 0 || code > kMaxInt)
+      throw new errors.RangeError('ERR_OUT_OF_RANGE', 'code');
 
-    debug(`Http2Stream ${this[kID]} [Http2Session ` +
-          `${sessionName(session[kType])}]: destroying stream`);
+    // Unenroll the timeout.
+    unenroll(this);
+    this.removeAllListeners('timeout');
+
+    // Close the writable
+    abort(this);
+    this.end();
+
+    if (this.closed)
+      return;
 
     const state = this[kState];
-    session[kState].writeQueueSize -= state.writeQueueSize;
-    state.writeQueueSize = 0;
+    state.flags |= STREAM_FLAGS_CLOSED;
+    state.rstCode = code;
 
-    const server = session[kServer];
-    if (server !== undefined && err) {
-      server.emit('streamError', err, this);
+    if (callback !== undefined) {
+      if (typeof callback !== 'function')
+        throw new errors.TypeError('ERR_INVALID_CALLBACK');
+      this.once('close', callback);
     }
 
-    process.nextTick(continueStreamDestroy.bind(this), err, callback);
-  }
-}
-
-function continueStreamDestroy(err, callback) {
-  const session = this[kSession];
-  const state = this[kState];
+    if (this[kHandle] === undefined)
+      return;
 
-  // Submit RST-STREAM frame if one hasn't been sent already and the
-  // stream hasn't closed normally...
-  const rst = state.rst;
-  let code = state.rstCode;
-  if (!rst && !session.destroyed) {
-    code = err instanceof Error ? NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
-    this.rstStream(code);
+    const rstStreamFn = submitRstStream.bind(this, code);
+    // If the handle has not yet been assigned, queue up the request to
+    // ensure that the RST_STREAM frame is sent after the stream ID has
+    // been determined.
+    if (this.pending) {
+      this.push(null);
+      this.once('ready', rstStreamFn);
+      return;
+    }
+    rstStreamFn();
   }
 
-  // Remove the close handler on the session
-  session.removeListener('close', state.closeHandler);
+  // Called by this.destroy().
+  // * Will submit an RST stream to shutdown the stream if necessary.
+  //   This will cause the internal resources to be released.
+  // * Then cleans up the resources on the js side
+  _destroy(err, callback) {
+    const session = this[kSession];
+    const handle = this[kHandle];
+    const id = this[kID];
 
-  // Unenroll the timer
-  this.setTimeout(0);
+    debug(`Http2Stream ${this[kID] || '<pending>'} [Http2Session ` +
+          `${sessionName(session[kType])}]: destroying stream`);
+    const state = this[kState];
+    const code = state.rstCode =
+      err != null ?
+        NGHTTP2_INTERNAL_ERROR :
+        state.rstCode || NGHTTP2_NO_ERROR;
+    if (handle !== undefined) {
+      // If the handle exists, we need to close, then destroy the handle
+      this.close(code);
+      if (!this._readableState.ended && !this._readableState.ending)
+        this.push(null);
+      handle.destroy();
+      session[kState].streams.delete(id);
+    } else {
+      unenroll(this);
+      this.removeAllListeners('timeout');
+      state.flags |= STREAM_FLAGS_CLOSED;
+      abort(this);
+      this.end();
+      this.push(null);
+      session[kState].pendingStreams.delete(this);
+    }
 
-  setImmediate(finishStreamDestroy.bind(this));
+    // Adjust the write queue size for accounting
+    session[kState].writeQueueSize -= state.writeQueueSize;
+    state.writeQueueSize = 0;
 
-  // RST code 8 not emitted as an error as its used by clients to signify
-  // abort and is already covered by aborted event, also allows more
-  // seamless compatibility with http1
-  if (code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL && !err) {
-    err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
-  }
-  callback(err);
-  abort(this);
-  this.push(null);  // Close the readable side
-  this.end();       // Close the writable side
-  process.nextTick(emit, this, 'close', code);
-}
+    // RST code 8 not emitted as an error as its used by clients to signify
+    // abort and is already covered by aborted event, also allows more
+    // seamless compatibility with http1
+    if (err == null && code !== NGHTTP2_NO_ERROR && code !== NGHTTP2_CANCEL)
+      err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
 
-function finishStreamDestroy() {
-  const id = this[kID];
-  this[kSession][kState].streams.delete(id);
-  this[kSession] = undefined;
-  const handle = this[kHandle];
-  if (handle !== undefined) {
+    this[kSession] = undefined;
     this[kHandle] = undefined;
-    handle.destroy();
+
+    // This notifies the session that this stream has been destroyed and
+    // gives the session the opportunity to clean itself up. The session
+    // will destroy if it has been closed and there are no other open or
+    // pending streams.
+    session[kMaybeDestroy]();
+    process.nextTick(emit, this, 'close', code);
+    callback(err);
+  }
+
+  // The Http2Stream can be destroyed if it has closed and if the readable
+  // side has received the final chunk.
+  [kMaybeDestroy](error, code = NGHTTP2_NO_ERROR, hasData = true) {
+    if (error == null) {
+      if (code === NGHTTP2_NO_ERROR &&
+          ((!this._readableState.ended && hasData) ||
+          !this._writableState.ended ||
+          this._writableState.pendingcb > 0 ||
+          !this.closed)) {
+        return;
+      }
+    }
+    this.destroy(error);
   }
-  this.emit('destroy');
 }
 
 function processHeaders(headers) {
@@ -1505,7 +1695,7 @@ function processHeaders(headers) {
   // This is intentionally stricter than the HTTP/1 implementation, which
   // allows values between 100 and 999 (inclusive) in order to allow for
   // backwards compatibility with non-spec compliant code. With HTTP/2,
-  // we have the opportunity to start fresh with stricter spec copmliance.
+  // we have the opportunity to start fresh with stricter spec compliance.
   // This will have an impact on the compatibility layer for anyone using
   // non-standard, non-compliant status codes.
   if (statusCode < 200 || statusCode > 599)
@@ -1515,36 +1705,47 @@ function processHeaders(headers) {
   return headers;
 }
 
-function processRespondWithFD(fd, headers, offset = 0, length = -1,
+function processRespondWithFD(self, fd, headers, offset = 0, length = -1,
                               streamOptions = 0) {
-  const state = this[kState];
-  state.headersSent = true;
+  const state = self[kState];
+  state.flags |= STREAM_FLAGS_HEADERS_SENT;
+
+  const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
+  if (!Array.isArray(headersList)) {
+    self.destroy(headersList);
+    return;
+  }
+
 
   // Close the writable side of the stream
-  this.end();
+  self.end();
 
-  const ret = this[kHandle].respondFD(fd, headers,
+  const ret = self[kHandle].respondFD(fd, headersList,
                                       offset, length,
                                       streamOptions);
 
   if (ret < 0) {
-    const err = new NghttpError(ret);
-    process.nextTick(emit, this, 'error', err);
+    self.destroy(new NghttpError(ret));
     return;
   }
   // exact length of the file doesn't matter here, since the
   // stream is closing anyway - just use 1 to signify that
   // a write does exist
-  trackWriteState(this, 1);
+  trackWriteState(self, 1);
 }
 
 function doSendFD(session, options, fd, headers, streamOptions, err, stat) {
-  if (this.destroyed || session.destroyed) {
-    abort(this);
+  if (err) {
+    this.destroy(err);
     return;
   }
-  if (err) {
-    process.nextTick(emit, this, 'error', err);
+
+  // This can happen if the stream is destroyed or closed while we are waiting
+  // for the file descriptor to be opened or the stat call to be completed.
+  // In either case, we do not want to continue because the we are shutting
+  // down and should not attempt to send any data.
+  if (this.destroyed || this.closed) {
+    this.destroy(new errors.Error('ERR_HTTP2_INVALID_STREAM'));
     return;
   }
 
@@ -1553,47 +1754,47 @@ function doSendFD(session, options, fd, headers, streamOptions, err, stat) {
     length: options.length !== undefined ? options.length : -1
   };
 
-  if (typeof options.statCheck === 'function' &&
-      options.statCheck.call(this, stat, headers, statOptions) === false) {
-    return;
-  }
-
-  const headersList = mapToHeaders(headers,
-                                   assertValidPseudoHeaderResponse);
-  if (!Array.isArray(headersList)) {
-    process.nextTick(emit, this, 'error', headersList);
+  // options.statCheck is a user-provided function that can be used to
+  // verify stat values, override or set headers, or even cancel the
+  // response operation. If statCheck explicitly returns false, the
+  // response is canceled. The user code may also send a separate type
+  // of response so check again for the HEADERS_SENT flag
+  if ((typeof options.statCheck === 'function' &&
+       options.statCheck.call(this, stat, headers, statOptions) === false) ||
+       (this[kState].flags & STREAM_FLAGS_HEADERS_SENT)) {
     return;
   }
 
-  processRespondWithFD.call(this, fd, headersList,
-                            statOptions.offset,
-                            statOptions.length,
-                            streamOptions);
+  processRespondWithFD(this, fd, headers,
+                       statOptions.offset | 0,
+                       statOptions.length | 0,
+                       streamOptions);
 }
 
 function doSendFileFD(session, options, fd, headers, streamOptions, err, stat) {
-  if (this.destroyed || session.destroyed) {
-    abort(this);
-    return;
-  }
   const onError = options.onError;
 
   if (err) {
-    if (onError) {
+    tryClose(fd);
+    if (onError)
       onError(err);
-    } else {
+    else
       this.destroy(err);
-    }
     return;
   }
 
   if (!stat.isFile()) {
-    err = new errors.Error('ERR_HTTP2_SEND_FILE');
-    if (onError) {
+    const err = new errors.Error('ERR_HTTP2_SEND_FILE');
+    if (onError)
       onError(err);
-    } else {
+    else
       this.destroy(err);
-    }
+    return;
+  }
+
+  if (this.destroyed || this.closed) {
+    tryClose(fd);
+    this.destroy(new errors.Error('ERR_HTTP2_INVALID_STREAM'));
     return;
   }
 
@@ -1602,9 +1803,14 @@ function doSendFileFD(session, options, fd, headers, streamOptions, err, stat) {
     length: options.length !== undefined ? options.length : -1
   };
 
-  // Set the content-length by default
-  if (typeof options.statCheck === 'function' &&
-      options.statCheck.call(this, stat, headers) === false) {
+  // options.statCheck is a user-provided function that can be used to
+  // verify stat values, override or set headers, or even cancel the
+  // response operation. If statCheck explicitly returns false, the
+  // response is canceled. The user code may also send a separate type
+  // of response so check again for the HEADERS_SENT flag
+  if ((typeof options.statCheck === 'function' &&
+       options.statCheck.call(this, stat, headers) === false) ||
+       (this[kState].flags & STREAM_FLAGS_HEADERS_SENT)) {
     return;
   }
 
@@ -1613,35 +1819,27 @@ function doSendFileFD(session, options, fd, headers, streamOptions, err, stat) {
       Math.min(stat.size - (+statOptions.offset),
                statOptions.length);
 
-  if (headers[HTTP2_HEADER_CONTENT_LENGTH] === undefined)
-    headers[HTTP2_HEADER_CONTENT_LENGTH] = statOptions.length;
+  headers[HTTP2_HEADER_CONTENT_LENGTH] = statOptions.length;
 
-  const headersList = mapToHeaders(headers,
-                                   assertValidPseudoHeaderResponse);
-  if (!Array.isArray(headersList)) {
-    process.nextTick(emit, this, 'error', headersList);
-    return;
-  }
-
-  processRespondWithFD.call(this, fd, headersList,
-                            options.offset,
-                            options.length,
-                            streamOptions);
+  processRespondWithFD(this, fd, headers,
+                       options.offset | 0,
+                       statOptions.length | 0,
+                       streamOptions);
 }
 
 function afterOpen(session, options, headers, streamOptions, err, fd) {
   const state = this[kState];
   const onError = options.onError;
-  if (this.destroyed || session.destroyed) {
-    abort(this);
-    return;
-  }
   if (err) {
-    if (onError) {
+    if (onError)
       onError(err);
-    } else {
+    else
       this.destroy(err);
-    }
+    return;
+  }
+  if (this.destroyed || this.closed) {
+    tryClose(fd);
+    abort(this);
     return;
   }
   state.fd = fd;
@@ -1654,8 +1852,6 @@ function afterOpen(session, options, headers, streamOptions, err, fd) {
 function streamOnError(err) {
   // we swallow the error for parity with HTTP1
   // all the errors that ends here are not critical for the project
-  debug(`Http2Stream ${this[kID]} [Http2Session ` +
-        `${this[kSession][kType]}: error`, err);
 }
 
 
@@ -1668,25 +1864,22 @@ class ServerHttp2Stream extends Http2Stream {
     this.on('error', streamOnError);
   }
 
-  // true if the HEADERS frame has been sent
-  get headersSent() {
-    return this[kState].headersSent;
-  }
-
   // true if the remote peer accepts push streams
   get pushAllowed() {
-    return this[kSession].remoteSettings.enablePush;
+    return !this.destroyed &&
+           !this.closed &&
+           !this.session.closed &&
+           !this.session.destroyed &&
+           this[kSession].remoteSettings.enablePush;
   }
 
   // create a push stream, call the given callback with the created
   // Http2Stream for the push stream.
   pushStream(headers, options, callback) {
-    if (this.destroyed)
-      throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
+    if (!this.pushAllowed)
+      throw new errors.Error('ERR_HTTP2_PUSH_DISABLED');
 
     const session = this[kSession];
-    if (!session.remoteSettings.enablePush)
-      throw new errors.Error('ERR_HTTP2_PUSH_DISABLED');
 
     debug(`Http2Stream ${this[kID]} [Http2Session ` +
           `${sessionName(session[kType])}]: initiating push stream`);
@@ -1736,44 +1929,45 @@ class ServerHttp2Stream extends Http2Stream {
           err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
           break;
         case NGHTTP2_ERR_STREAM_CLOSED:
-          err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
+          err = new errors.Error('ERR_HTTP2_INVALID_STREAM');
           break;
         default:
           err = new NghttpError(ret);
           break;
       }
-      process.nextTick(emit, this, 'error', err);
+      process.nextTick(callback, err);
       return;
     }
 
     const id = ret.id();
     const stream = new ServerHttp2Stream(session, ret, id, options, headers);
-    session[kState].streams.set(id, stream);
 
     if (options.endStream)
       stream.end();
 
     if (headRequest)
-      stream[kState].headRequest = true;
+      stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST;
 
-    process.nextTick(callback, stream, headers, 0);
+    process.nextTick(callback, null, stream, headers, 0);
   }
 
   // Initiate a response on this Http2Stream
   respond(headers, options) {
-    const session = this[kSession];
-    if (this.destroyed)
+    if (this.destroyed || this.closed)
       throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
-    debug(`Http2Stream ${this[kID]} [Http2Session ` +
-          `${sessionName(session[kType])}]: initiating response`);
-    this[kUpdateTimer]();
-    const state = this[kState];
-
-    if (state.headersSent)
+    if (this.headersSent)
       throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
 
+    const state = this[kState];
+
     assertIsObject(options, 'options');
     options = Object.assign({}, options);
+
+    const session = this[kSession];
+    debug(`Http2Stream ${this[kID]} [Http2Session ` +
+          `${sessionName(session[kType])}]: initiating response`);
+    this[kUpdateTimer]();
+
     options.endStream = !!options.endStream;
 
     let streamOptions = 0;
@@ -1799,7 +1993,7 @@ class ServerHttp2Stream extends Http2Stream {
     if (statusCode === HTTP_STATUS_NO_CONTENT ||
         statusCode === HTTP_STATUS_RESET_CONTENT ||
         statusCode === HTTP_STATUS_NOT_MODIFIED ||
-        state.headRequest === true) {
+        this.headRequest === true) {
       options.endStream = true;
     }
 
@@ -1807,17 +2001,15 @@ class ServerHttp2Stream extends Http2Stream {
     if (!Array.isArray(headersList))
       throw headersList;
 
-    state.headersSent = true;
+    state.flags |= STREAM_FLAGS_HEADERS_SENT;
 
     // Close the writable side if the endStream option is set
     if (options.endStream)
       this.end();
 
     const ret = this[kHandle].respond(headersList, streamOptions);
-    if (ret < 0) {
-      const err = new NghttpError(ret);
-      process.nextTick(emit, this, 'error', err);
-    }
+    if (ret < 0)
+      this.destroy(new NghttpError(ret));
   }
 
   // Initiate a response using an open FD. Note that there are fewer
@@ -1827,19 +2019,15 @@ class ServerHttp2Stream extends Http2Stream {
   // mechanism is not able to read from the fd, then the stream will be
   // reset with an error code.
   respondWithFD(fd, headers, options) {
-    const session = this[kSession];
-    if (this.destroyed)
+    if (this.destroyed || this.closed)
       throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
-    debug(`Http2Stream ${this[kID]} [Http2Session ` +
-          `${sessionName(session[kType])}]: initiating response`);
-    this[kUpdateTimer]();
-    const state = this[kState];
-
-    if (state.headersSent)
+    if (this.headersSent)
       throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
 
+    const session = this[kSession];
+
     assertIsObject(options, 'options');
-    options = Object.assign(Object.create(null), options);
+    options = Object.assign({}, options);
 
     if (options.offset !== undefined && typeof options.offset !== 'number')
       throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
@@ -1866,13 +2054,17 @@ class ServerHttp2Stream extends Http2Stream {
                                    options.getTrailers);
       }
       streamOptions |= STREAM_OPTION_GET_TRAILERS;
-      state.getTrailers = options.getTrailers;
+      this[kState].getTrailers = options.getTrailers;
     }
 
     if (typeof fd !== 'number')
       throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
                                  'fd', 'number');
 
+    debug(`Http2Stream ${this[kID]} [Http2Session ` +
+          `${sessionName(session[kType])}]: initiating response`);
+    this[kUpdateTimer]();
+
     headers = processHeaders(headers);
     const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
     // Payload/DATA frames are not permitted in these cases
@@ -1889,17 +2081,10 @@ class ServerHttp2Stream extends Http2Stream {
       return;
     }
 
-    const headersList = mapToHeaders(headers,
-                                     assertValidPseudoHeaderResponse);
-    if (!Array.isArray(headersList)) {
-      process.nextTick(emit, this, 'error', headersList);
-      return;
-    }
-
-    processRespondWithFD.call(this, fd, headersList,
-                              options.offset,
-                              options.length,
-                              streamOptions);
+    processRespondWithFD(this, fd, headers,
+                         options.offset,
+                         options.length,
+                         streamOptions);
   }
 
   // Initiate a file response on this Http2Stream. The path is passed to
@@ -1910,19 +2095,13 @@ class ServerHttp2Stream extends Http2Stream {
   // headers. If statCheck returns false, the operation is aborted and no
   // file details are sent.
   respondWithFile(path, headers, options) {
-    const session = this[kSession];
-    if (this.destroyed)
+    if (this.destroyed || this.closed)
       throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
-    debug(`Http2Stream ${this[kID]} [Http2Session ` +
-          `${sessionName(session[kType])}]: initiating response`);
-    this[kUpdateTimer]();
-    const state = this[kState];
-
-    if (state.headersSent)
+    if (this.headersSent)
       throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
 
     assertIsObject(options, 'options');
-    options = Object.assign(Object.create(null), options);
+    options = Object.assign({}, options);
 
     if (options.offset !== undefined && typeof options.offset !== 'number')
       throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
@@ -1949,9 +2128,15 @@ class ServerHttp2Stream extends Http2Stream {
                                    options.getTrailers);
       }
       streamOptions |= STREAM_OPTION_GET_TRAILERS;
-      state.getTrailers = options.getTrailers;
+      this[kState].getTrailers = options.getTrailers;
     }
 
+    const session = this[kSession];
+    debug(`Http2Stream ${this[kID]} [Http2Session ` +
+          `${sessionName(session[kType])}]: initiating response`);
+    this[kUpdateTimer]();
+
+
     headers = processHeaders(headers);
     const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
     // Payload/DATA frames are not permitted in these cases
@@ -1973,18 +2158,18 @@ class ServerHttp2Stream extends Http2Stream {
   // a 1xx informational code and it MUST be sent before the request/response
   // headers are sent, or an error will be thrown.
   additionalHeaders(headers) {
-    if (this.destroyed)
+    if (this.destroyed || this.closed)
       throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
-
-    if (this[kState].headersSent)
+    if (this.headersSent)
       throw new errors.Error('ERR_HTTP2_HEADERS_AFTER_RESPOND');
 
+    assertIsObject(headers, 'headers');
+    headers = Object.assign(Object.create(null), headers);
+
     const session = this[kSession];
     debug(`Http2Stream ${this[kID]} [Http2Session ` +
-          `${sessionName(session[kType])}]: sending additional headers`);
+    `${sessionName(session[kType])}]: sending additional headers`);
 
-    assertIsObject(headers, 'headers');
-    headers = Object.assign(Object.create(null), headers);
     if (headers[HTTP2_HEADER_STATUS] != null) {
       const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
       if (statusCode === HTTP_STATUS_SWITCHING_PROTOCOLS)
@@ -1997,17 +2182,13 @@ class ServerHttp2Stream extends Http2Stream {
 
     this[kUpdateTimer]();
 
-    const headersList = mapToHeaders(headers,
-                                     assertValidPseudoHeaderResponse);
-    if (!Array.isArray(headersList)) {
+    const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
+    if (!Array.isArray(headersList))
       throw headersList;
-    }
 
     const ret = this[kHandle].info(headersList);
-    if (ret < 0) {
-      const err = new NghttpError(ret);
-      process.nextTick(emit, this, 'error', err);
-    }
+    if (ret < 0)
+      this.destroy(new NghttpError(ret));
   }
 }
 
@@ -2016,7 +2197,7 @@ ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond;
 class ClientHttp2Stream extends Http2Stream {
   constructor(session, handle, id, options) {
     super(session, options);
-    this[kState].headersSent = true;
+    this[kState].flags |= STREAM_FLAGS_HEADERS_SENT;
     if (id !== undefined)
       this[kInit](id, handle);
     this.on('headers', handleHeaderContinue);
@@ -2024,9 +2205,8 @@ class ClientHttp2Stream extends Http2Stream {
 }
 
 function handleHeaderContinue(headers) {
-  if (headers[HTTP2_HEADER_STATUS] === HTTP_STATUS_CONTINUE) {
+  if (headers[HTTP2_HEADER_STATUS] === HTTP_STATUS_CONTINUE)
     this.emit('continue');
-  }
 }
 
 const setTimeout = {
@@ -2034,6 +2214,8 @@ const setTimeout = {
   enumerable: true,
   writable: true,
   value: function(msecs, callback) {
+    if (this.destroyed)
+      return;
     if (typeof msecs !== 'number') {
       throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
                                  'msecs',
@@ -2061,91 +2243,44 @@ const setTimeout = {
 Object.defineProperty(Http2Stream.prototype, 'setTimeout', setTimeout);
 Object.defineProperty(Http2Session.prototype, 'setTimeout', setTimeout);
 
-// --------------------------------------------------------------------
 
-// Set as a replacement for socket.prototype.destroy upon the
-// establishment of a new connection.
-function socketDestroy(error) {
-  const session = this[kSession];
-  const type = session[kType];
-  debug(`Http2Session ${sessionName(type)}: socket destroy called`);
-  delete this[kServer];
-  // destroy the session first so that it will stop trying to
-  // send data while we close the socket.
-  session.destroy();
-  this.destroy = this[kDestroySocket];
-  this.destroy(error);
-}
-
-// When an Http2Session emits an error, first try to forward it to the
-// server as a sessionError; failing that, forward it to the socket as
-// a sessionError; failing that, destroy, remove the error listener, and
-// re-emit the error event
-function sessionOnError(error) {
-  debug(`Http2Session ${sessionName(this[kType])}: session error: ` +
-        `${error.message}`);
-  if (this[kServer] !== undefined && this[kServer].emit('sessionError', error))
-    return;
-  if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
-    return;
-  this.destroy();
-  this.removeListener('error', sessionOnError);
-  this.emit('error', error);
-}
-
-// When a Socket emits an error, forward it to the session as a
-// socketError; failing that, remove the listener and call destroy
+// When the socket emits an error, destroy the associated Http2Session and
+// foward it the same error.
 function socketOnError(error) {
   const session = this[kSession];
-  const type = session && session[kType];
-  debug(`Http2Session ${sessionName(type)}: socket error: ${error.message}`);
-  if (kRenegTest.test(error.message))
-    return this.destroy();
-  if (session !== undefined &&
-      session.emit('socketError', error, this))
-    return;
-  this.removeListener('error', socketOnError);
-  this.destroy(error);
+  if (session !== undefined) {
+    debug(`Http2Session ${sessionName(session[kType])}: socket error [` +
+          `${error.message}]`);
+    session.destroy(error);
+  }
 }
 
 // Handles the on('stream') event for a session and forwards
 // it on to the server object.
 function sessionOnStream(stream, headers, flags, rawHeaders) {
-  this[kServer].emit('stream', stream, headers, flags, rawHeaders);
+  if (this[kServer] !== undefined)
+    this[kServer].emit('stream', stream, headers, flags, rawHeaders);
 }
 
 function sessionOnPriority(stream, parent, weight, exclusive) {
-  debug(`Http2Session ${sessionName(this[kType])}: priority change received`);
-  this[kServer].emit('priority', stream, parent, weight, exclusive);
+  if (this[kServer] !== undefined)
+    this[kServer].emit('priority', stream, parent, weight, exclusive);
 }
 
-function sessionOnSocketError(error, socket) {
-  if (this.listenerCount('socketError') <= 1 && this[kServer] !== undefined)
-    this[kServer].emit('socketError', error, socket, this);
+function sessionOnError(error) {
+  if (this[kServer])
+    this[kServer].emit('sessionError', error, this);
 }
 
-// When the session times out on the server, attempt a graceful shutdown
+// When the session times out on the server, try emitting a timeout event.
+// If no handler is registered, destroy the session.
 function sessionOnTimeout() {
-  process.nextTick(() => {
-    const state = this[kState];
-    // if destroyed or destryoing, do nothing
-    if (state.destroyed || state.destroying)
-      return;
-    const server = this[kServer];
-    const socket = this[kSocket];
-    // If server or socket are undefined, then we're already in the process of
-    // shutting down, do nothing.
-    if (server === undefined || socket === undefined)
-      return;
-    if (!server.emit('timeout', this)) {
-      this.shutdown(
-        {
-          graceful: true,
-          errorCode: NGHTTP2_NO_ERROR
-        },
-        socket.destroy.bind(socket));
-    }
-  });
+  // if destroyed or closed already, do nothing
+  if (this.destroyed || this.closed)
+    return;
+  const server = this[kServer];
+  if (!server.emit('timeout', this))
+    this.destroy();  // No error code, just things down.
 }
 
 function connectionListener(socket) {
@@ -2157,10 +2292,9 @@ function connectionListener(socket) {
     if (options.allowHTTP1 === true)
       return httpConnectionListener.call(this, socket);
     // Let event handler deal with the socket
-    if (this.emit('unknownProtocol', socket))
-      return;
-    // Reject non-HTTP/2 client
-    return socket.destroy();
+    if (!this.emit('unknownProtocol', socket))
+      socket.destroy();
+    return;
   }
 
   socket.on('error', socketOnError);
@@ -2169,27 +2303,24 @@ function connectionListener(socket) {
   // Set up the Session
   const session = new ServerHttp2Session(options, socket, this);
 
-  session.on('error', sessionOnError);
   session.on('stream', sessionOnStream);
   session.on('priority', sessionOnPriority);
-  session.on('socketError', sessionOnSocketError);
+  session.on('error', sessionOnError);
 
-  if (this.timeout) {
-    session.setTimeout(this.timeout);
-    session.on('timeout', sessionOnTimeout);
-  }
+  if (this.timeout)
+    session.setTimeout(this.timeout, sessionOnTimeout);
 
   socket[kServer] = this;
 
-  process.nextTick(emit, this, 'session', session);
+  this.emit('session', session);
 }
 
 function initializeOptions(options) {
   assertIsObject(options, 'options');
-  options = Object.assign(Object.create(null), options);
+  options = Object.assign({}, options);
   options.allowHalfOpen = true;
   assertIsObject(options.settings, 'options.settings');
-  options.settings = Object.assign(Object.create(null), options.settings);
+  options.settings = Object.assign({}, options.settings);
   return options;
 }
 
@@ -2203,9 +2334,9 @@ function initializeTLSOptions(options, servername) {
   return options;
 }
 
-function onErrorSecureServerSession(err, conn) {
-  if (!this.emit('clientError', err, conn))
-    conn.destroy(err);
+function onErrorSecureServerSession(err, socket) {
+  if (!this.emit('clientError', err, socket))
+    socket.destroy(err);
 }
 
 class Http2SecureServer extends TLSServer {
@@ -2261,25 +2392,18 @@ function setupCompat(ev) {
 
 function socketOnClose() {
   const session = this[kSession];
-  if (session !== undefined && !session.destroyed) {
-    // Skip unconsume because the socket is destroyed.
-    session[kState].skipUnconsume = true;
-    session.destroy();
+  if (session !== undefined) {
+    debug(`Http2Session ${sessionName(session[kType])}: socket closed`);
+    const err = session.connecting ?
+      new errors.Error('ERR_SOCKET_CLOSED') : null;
+    const state = session[kState];
+    state.streams.forEach((stream) => stream.close(NGHTTP2_CANCEL));
+    state.pendingStreams.forEach((stream) => stream.close(NGHTTP2_CANCEL));
+    session.close();
+    session[kMaybeDestroy](err);
   }
 }
 
-// If the session emits an error, forward it to the socket as a sessionError;
-// failing that, destroy the session, remove the listener and re-emit the error
-function clientSessionOnError(error) {
-  debug(`Http2Session ${sessionName(this[kType])}]: session error: ` +
-        `${error.message}`);
-  if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
-    return;
-  this.destroy();
-  this.removeListener('error', socketOnError);
-  this.removeListener('error', clientSessionOnError);
-}
-
 function connect(authority, options, listener) {
   if (typeof options === 'function') {
     listener = options;
@@ -2287,7 +2411,7 @@ function connect(authority, options, listener) {
   }
 
   assertIsObject(options, 'options');
-  options = Object.assign(Object.create(null), options);
+  options = Object.assign({}, options);
 
   if (typeof authority === 'string')
     authority = new URL(authority);
@@ -2320,8 +2444,6 @@ function connect(authority, options, listener) {
 
   const session = new ClientHttp2Session(options, socket);
 
-  session.on('error', clientSessionOnError);
-
   session[kAuthority] = `${options.servername || host}:${port}`;
   session[kProtocol] = protocol;
 
@@ -2342,19 +2464,16 @@ Object.defineProperty(connect, promisify.custom, {
 });
 
 function createSecureServer(options, handler) {
-  if (options == null || typeof options !== 'object') {
-    throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
-                               'options',
-                               'object');
-  }
+  assertIsObject(options, 'options');
   return new Http2SecureServer(options, handler);
 }
 
 function createServer(options, handler) {
   if (typeof options === 'function') {
     handler = options;
-    options = Object.create(null);
+    options = {};
   }
+  assertIsObject(options, 'options');
   return new Http2Server(options, handler);
 }
 
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 1ce3c5cb46e231..ac785de4cd3196 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -23,13 +23,30 @@ using v8::Undefined;
 
 namespace http2 {
 
+// These configure the callbacks required by nghttp2 itself. There are
+// two sets of callback functions, one that is used if a padding callback
+// is set, and other that does not include the padding callback.
 const Http2Session::Callbacks Http2Session::callback_struct_saved[2] = {
     Callbacks(false),
     Callbacks(true)};
 
+// The Http2Scope object is used to queue a write to the i/o stream. It is
+// used whenever any action is take on the underlying nghttp2 API that may
+// push data into nghttp2 outbound data queue.
+//
+// For example:
+//
+// Http2Scope h2scope(session);
+// nghttp2_submit_ping(**session, ... );
+//
+// When the Http2Scope passes out of scope and is deconstructed, it will
+// call Http2Session::MaybeScheduleWrite().
 Http2Scope::Http2Scope(Http2Stream* stream) : Http2Scope(stream->session()) {}
 
 Http2Scope::Http2Scope(Http2Session* session) {
+  if (session == nullptr)
+    return;
+
   if (session->flags_ & (SESSION_STATE_HAS_SCOPE |
                          SESSION_STATE_WRITE_SCHEDULED)) {
     // There is another scope further below on the stack, or it is already
@@ -48,9 +65,19 @@ Http2Scope::~Http2Scope() {
   session_->MaybeScheduleWrite();
 }
 
+// The Http2Options object is used during the construction of Http2Session
+// instances to configure an appropriate nghttp2_options struct. The class
+// uses a single TypedArray instance that is shared with the JavaScript side
+// to more efficiently pass values back and forth.
 Http2Options::Http2Options(Environment* env) {
   nghttp2_option_new(&options_);
 
+  // We manually handle flow control within a session in order to
+  // implement backpressure -- that is, we only send WINDOW_UPDATE
+  // frames to the remote peer as data is actually consumed by user
+  // code. This ensures that the flow of data over the connection
+  // does not move too quickly and limits the amount of data we
+  // are required to buffer.
   nghttp2_option_set_no_auto_window_update(options_, 1);
 
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
@@ -83,6 +110,10 @@ Http2Options::Http2Options(Environment* env) {
         buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
   }
 
+  // The padding strategy sets the mechanism by which we determine how much
+  // additional frame padding to apply to DATA and HEADERS frames. Currently
+  // this is set on a per-session basis, but eventually we may switch to
+  // a per-stream setting, giving users greater control
   if (flags & (1 << IDX_OPTIONS_PADDING_STRATEGY)) {
     padding_strategy_type strategy =
         static_cast<padding_strategy_type>(
@@ -90,16 +121,27 @@ Http2Options::Http2Options(Environment* env) {
     SetPaddingStrategy(strategy);
   }
 
+  // The max header list pairs option controls the maximum number of
+  // header pairs the session may accept. This is a hard limit.. that is,
+  // if the remote peer sends more than this amount, the stream will be
+  // automatically closed with an RST_STREAM.
   if (flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS)) {
     SetMaxHeaderPairs(buffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS]);
   }
 
+  // The HTTP2 specification places no limits on the number of HTTP2
+  // PING frames that can be sent. In order to prevent PINGS from being
+  // abused as an attack vector, however, we place a strict upper limit
+  // on the number of unacknowledged PINGS that can be sent at any given
+  // time.
   if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)) {
     SetMaxOutstandingPings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS]);
   }
 }
 
-
+// The Http2Settings class is used to configure a SETTINGS frame that is
+// to be sent to the connected peer. The settings are set using a TypedArray
+// that is shared with the JavaScript side.
 Http2Settings::Http2Settings(Environment* env) : env_(env) {
   entries_.AllocateSufficientStorage(IDX_SETTINGS_COUNT);
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
@@ -160,6 +202,9 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
 }
 
 
+// Generates a Buffer that contains the serialized payload of a SETTINGS
+// frame. This can be used, for instance, to create the Base64-encoded
+// content of an Http2-Settings header field.
 inline Local<Value> Http2Settings::Pack() {
   const size_t len = count_ * 6;
   Local<Value> buf = Buffer::New(env_, len).ToLocalChecked();
@@ -173,27 +218,28 @@ inline Local<Value> Http2Settings::Pack() {
     return Undefined(env_->isolate());
 }
 
-
+// Updates the shared TypedArray with the current remote or local settings for
+// the session.
 inline void Http2Settings::Update(Environment* env,
                                   Http2Session* session,
                                   get_setting fn) {
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
       env->http2_state()->settings_buffer;
   buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =
-      fn(session->session(), NGHTTP2_SETTINGS_HEADER_TABLE_SIZE);
+      fn(**session, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE);
   buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] =
-      fn(session->session(), NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
+      fn(**session, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
   buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] =
-      fn(session->session(), NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
+      fn(**session, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE);
   buffer[IDX_SETTINGS_MAX_FRAME_SIZE] =
-      fn(session->session(), NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
+      fn(**session, NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
   buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] =
-      fn(session->session(), NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
+      fn(**session, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
   buffer[IDX_SETTINGS_ENABLE_PUSH] =
-      fn(session->session(), NGHTTP2_SETTINGS_ENABLE_PUSH);
+      fn(**session, NGHTTP2_SETTINGS_ENABLE_PUSH);
 }
 
-
+// Initializes the shared TypedArray with the default settings values.
 inline void Http2Settings::RefreshDefaults(Environment* env) {
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
       env->http2_state()->settings_buffer;
@@ -217,6 +263,9 @@ inline void Http2Settings::RefreshDefaults(Environment* env) {
 }
 
 
+// The Http2Priority class initializes an appropriate nghttp2_priority_spec
+// struct used when either creating a stream or updating its priority
+// settings.
 Http2Priority::Http2Priority(Environment* env,
                              Local<Value> parent,
                              Local<Value> weight,
@@ -241,7 +290,8 @@ inline const char* Http2Session::TypeName() {
   }
 }
 
-
+// The Headers class initializes a proper array of nghttp2_nv structs
+// containing the header name value pairs.
 Headers::Headers(Isolate* isolate,
                  Local<Context> context,
                  Local<Array> headers) {
@@ -299,8 +349,11 @@ Headers::Headers(Isolate* isolate,
 }
 
 
+// Sets the various callback functions that nghttp2 will use to notify us
+// about significant events while processing http2 stuff.
 Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
   CHECK_EQ(nghttp2_session_callbacks_new(&callbacks), 0);
+
   nghttp2_session_callbacks_set_on_begin_headers_callback(
     callbacks, OnBeginHeadersCallback);
   nghttp2_session_callbacks_set_on_header_callback2(
@@ -329,7 +382,6 @@ Http2Session::Callbacks::~Callbacks() {
   nghttp2_session_callbacks_del(callbacks);
 }
 
-
 Http2Session::Http2Session(Environment* env,
                            Local<Object> wrap,
                            nghttp2_session_type type)
@@ -337,6 +389,7 @@ Http2Session::Http2Session(Environment* env,
       session_type_(type) {
   MakeWeak<Http2Session>(this);
 
+  // Capture the configuration options for this session
   Http2Options opts(env);
 
   int32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
@@ -368,37 +421,64 @@ Http2Session::Http2Session(Environment* env,
   CHECK_EQ(fn(&session_, callbacks, this, *opts), 0);
 }
 
+void Http2Session::Unconsume() {
+  if (stream_ != nullptr) {
+    DEBUG_HTTP2SESSION(this, "unconsuming the i/o stream");
+    stream_->set_destruct_cb({ nullptr, nullptr });
+    stream_->set_alloc_cb({ nullptr, nullptr });
+    stream_->set_read_cb({ nullptr, nullptr });
+    stream_->Unconsume();
+    stream_ = nullptr;
+  }
+}
 
 Http2Session::~Http2Session() {
+  if (!object().IsEmpty())
+    ClearWrap(object());
+  persistent().Reset();
   CHECK(persistent().IsEmpty());
-  Close();
+  Unconsume();
+  DEBUG_HTTP2SESSION(this, "freeing nghttp2 session");
+  nghttp2_session_del(session_);
 }
 
-
-void Http2Session::Close() {
+// Closes the session and frees the associated resources
+void Http2Session::Close(uint32_t code, bool socket_closed) {
   DEBUG_HTTP2SESSION(this, "closing session");
-  if (!object().IsEmpty())
-    ClearWrap(object());
-  persistent().Reset();
 
-  if (session_ == nullptr)
+  if (flags_ & SESSION_STATE_CLOSED)
     return;
+  flags_ |= SESSION_STATE_CLOSED;
+
+  // Stop reading on the i/o stream
+  if (stream_ != nullptr)
+    stream_->ReadStop();
+
+  // If the socket is not closed, then attempt to send a closing GOAWAY
+  // frame. There is no guarantee that this GOAWAY will be received by
+  // the peer but the HTTP/2 spec recommends sendinng it anyway. We'll
+  // make a best effort.
+  if (!socket_closed) {
+    Http2Scope h2scope(this);
+    DEBUG_HTTP2SESSION2(this, "terminating session with code %d", code);
+    CHECK_EQ(nghttp2_session_terminate_session(session_, code), 0);
+  } else {
+    Unconsume();
+  }
 
-  CHECK_EQ(nghttp2_session_terminate_session(session_, NGHTTP2_NO_ERROR), 0);
-  nghttp2_session_del(session_);
-  session_ = nullptr;
-
+  // If there are outstanding pings, those will need to be canceled, do
+  // so on the next iteration of the event loop to avoid calling out into
+  // javascript since this may be called during garbage collection.
   while (!outstanding_pings_.empty()) {
     Http2Session::Http2Ping* ping = PopPing();
-    // Since this method may be called from GC, calling into JS directly
-    // is not allowed.
     env()->SetImmediate([](Environment* env, void* data) {
       static_cast<Http2Session::Http2Ping*>(data)->Done(false);
     }, static_cast<void*>(ping));
   }
 }
 
-
+// Locates an existing known stream by ID. nghttp2 has a similar method
+// but this is faster and does not fail if the stream is not found.
 inline Http2Stream* Http2Session::FindStream(int32_t id) {
   auto s = streams_.find(id);
   return s != streams_.end() ? s->second : nullptr;
@@ -414,14 +494,18 @@ inline void Http2Session::RemoveStream(int32_t id) {
   streams_.erase(id);
 }
 
-
+// Used as one of the Padding Strategy functions. Uses the maximum amount
+// of padding allowed for the current frame.
 inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
                                                    size_t maxPayloadLen) {
   DEBUG_HTTP2SESSION2(this, "using max frame size padding: %d", maxPayloadLen);
   return maxPayloadLen;
 }
 
-
+// Used as one of the Padding Strategy functions. Uses a callback to JS land
+// to determine the amount of padding for the current frame. This option is
+// rather more expensive because of the JS boundary cross. It generally should
+// not be the preferred option.
 inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
                                                size_t maxPayloadLen) {
   DEBUG_HTTP2SESSION(this, "using callback to determine padding");
@@ -448,20 +532,10 @@ inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
 }
 
 
-// Submits a graceful shutdown notice to nghttp
-// See: https://nghttp2.org/documentation/nghttp2_submit_shutdown_notice.html
-inline void Http2Session::SubmitShutdownNotice() {
-  // Only an HTTP2 Server is permitted to send a shutdown notice
-  if (session_type_ == NGHTTP2_SESSION_CLIENT)
-    return;
-  DEBUG_HTTP2SESSION(this, "sending shutdown notice");
-  // The only situation where this should fail is if the system is
-  // out of memory, which will cause other problems. Go ahead and crash
-  // in that case.
-  CHECK_EQ(nghttp2_submit_shutdown_notice(session_), 0);
-}
-
-
+// Sends a SETTINGS frame to the connected peer. This has the side effect of
+// changing the settings state within the nghttp2_session, but those will
+// only be considered active once the connected peer acknowledges the SETTINGS
+// frame.
 // Note: This *must* send a SETTINGS frame even if niv == 0
 inline void Http2Session::Settings(const nghttp2_settings_entry iv[],
                                    size_t niv) {
@@ -475,25 +549,40 @@ inline void Http2Session::Settings(const nghttp2_settings_entry iv[],
 
 
 // Write data received from the i/o stream to the underlying nghttp2_session.
+// On each call to nghttp2_session_mem_recv, nghttp2 will begin calling the
+// various callback functions. Each of these will typically result in a call
+// out to JavaScript so this particular function is rather hot and can be
+// quite expensive. This is a potential performance optimization target later.
 inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) {
   size_t total = 0;
   // Note that nghttp2_session_mem_recv is a synchronous operation that
   // will trigger a number of other callbacks. Those will, in turn have
   // multiple side effects.
   for (size_t n = 0; n < nbufs; n++) {
+    DEBUG_HTTP2SESSION2(this, "receiving %d bytes [wants data? %d]",
+                        bufs[n].len,
+                        nghttp2_session_want_read(session_));
     ssize_t ret =
       nghttp2_session_mem_recv(session_,
                                reinterpret_cast<uint8_t*>(bufs[n].base),
                                bufs[n].len);
     CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
 
-    if (ret < 0)
+    // If there is an error calling any of the callbacks, ret will be a
+    // negative number identifying the error code. This can happen, for
+    // instance, if the session is destroyed during any of the JS callbacks
+    // Note: if ssize_t is not defined (e.g. on Win32), nghttp2 will typedef
+    // ssize_t to int. Cast here so that the < 0 check actually works on
+    // Windows.
+    if (static_cast<int>(ret) < 0)
       return ret;
 
     total += ret;
   }
   // Send any data that was queued up while processing the received data.
-  SendPendingData();
+  if (!IsDestroyed()) {
+    SendPendingData();
+  }
   return total;
 }
 
@@ -506,6 +595,10 @@ inline int32_t GetFrameID(const nghttp2_frame* frame) {
 }
 
 
+// Called by nghttp2 at the start of receiving a HEADERS frame. We use this
+// callback to determine if a new stream is being created or if we are simply
+// adding a new block of headers to an existing stream. The header pairs
+// themselves are set in the OnHeaderCallback
 inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
                                                 const nghttp2_frame* frame,
                                                 void* user_data) {
@@ -517,12 +610,17 @@ inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
   if (stream == nullptr) {
     new Http2Stream(session, id, frame->headers.cat);
   } else {
+    // If the stream has already been destroyed, ignore.
+    if (stream->IsDestroyed())
+      return 0;
     stream->StartHeaders(frame->headers.cat);
   }
   return 0;
 }
 
-
+// Called by nghttp2 for each header name/value pair in a HEADERS block.
+// This had to have been preceeded by a call to OnBeginHeadersCallback so
+// the Http2Stream is guaranteed to already exist.
 inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
                                           const nghttp2_frame* frame,
                                           nghttp2_rcbuf* name,
@@ -532,6 +630,10 @@ inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
   Http2Session* session = static_cast<Http2Session*>(user_data);
   int32_t id = GetFrameID(frame);
   Http2Stream* stream = session->FindStream(id);
+  CHECK_NE(stream, nullptr);
+  // If the stream has already been destroyed, ignore.
+  if (stream->IsDestroyed())
+    return 0;
   if (!stream->AddHeader(name, value, flags)) {
     // This will only happen if the connected peer sends us more
     // than the allowed number of header items at any given time
@@ -542,6 +644,8 @@ inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
 }
 
 
+// Called by nghttp2 when a complete HTTP2 frame has been received. There are
+// only a handful of frame types tha we care about handling here.
 inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
                                         const nghttp2_frame* frame,
                                         void* user_data) {
@@ -575,6 +679,12 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
 }
 
 
+// If nghttp2 is unable to send a queued up frame, it will call this callback
+// to let us know. If the failure occurred because we are in the process of
+// closing down the session or stream, we go ahead and ignore it. We don't
+// really care about those and there's nothing we can reasonably do about it
+// anyway. Other types of failures are reported up to JavaScript. This should
+// be exceedingly rare.
 inline int Http2Session::OnFrameNotSent(nghttp2_session* handle,
                                         const nghttp2_frame* frame,
                                         int error_code,
@@ -602,7 +712,7 @@ inline int Http2Session::OnFrameNotSent(nghttp2_session* handle,
   return 0;
 }
 
-
+// Called by nghttp2 when a stream closes.
 inline int Http2Session::OnStreamClose(nghttp2_session* handle,
                                        int32_t id,
                                        uint32_t code,
@@ -615,17 +725,24 @@ inline int Http2Session::OnStreamClose(nghttp2_session* handle,
   Context::Scope context_scope(context);
   DEBUG_HTTP2SESSION2(session, "stream %d closed with code: %d", id, code);
   Http2Stream* stream = session->FindStream(id);
-  // Intentionally ignore the callback if the stream does not exist
-  if (stream != nullptr) {
+  // Intentionally ignore the callback if the stream does not exist or has
+  // already been destroyed
+  if (stream != nullptr && !stream->IsDestroyed()) {
+    stream->AddChunk(nullptr, 0);
     stream->Close(code);
     // It is possible for the stream close to occur before the stream is
     // ever passed on to the javascript side. If that happens, skip straight
-    // to destroying the stream
+    // to destroying the stream. We can check this by looking for the
+    // onstreamclose function. If it exists, then the stream has already
+    // been passed on to javascript.
     Local<Value> fn =
         stream->object()->Get(context, env->onstreamclose_string())
             .ToLocalChecked();
     if (fn->IsFunction()) {
-      Local<Value> argv[1] = { Integer::NewFromUnsigned(isolate, code) };
+      Local<Value> argv[2] = {
+        Integer::NewFromUnsigned(isolate, code),
+        Boolean::New(isolate, stream->HasDataChunks(true))
+      };
       stream->MakeCallback(fn.As<Function>(), arraysize(argv), argv);
     } else {
       stream->Destroy();
@@ -634,7 +751,10 @@ inline int Http2Session::OnStreamClose(nghttp2_session* handle,
   return 0;
 }
 
-
+// Called by nghttp2 when an invalid header has been received. For now, we
+// ignore these. If this callback was not provided, nghttp2 would handle
+// invalid headers strictly and would shut down the stream. We are intentionally
+// being more lenient here although we may want to revisit this choice later.
 inline int Http2Session::OnInvalidHeader(nghttp2_session* session,
                                          const nghttp2_frame* frame,
                                          nghttp2_rcbuf* name,
@@ -645,7 +765,10 @@ inline int Http2Session::OnInvalidHeader(nghttp2_session* session,
   return 0;
 }
 
-
+// When nghttp2 receives a DATA frame, it will deliver the data payload to
+// us in discrete chunks. We push these into a linked list stored in the
+// Http2Sttream which is flushed out to JavaScript as quickly as possible.
+// This can be a particularly hot path.
 inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
                                              uint8_t flags,
                                              int32_t id,
@@ -658,30 +781,39 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
   // We should never actually get a 0-length chunk so this check is
   // only a precaution at this point.
   if (len > 0) {
+    // Notify nghttp2 that we've consumed a chunk of data on the connection
+    // so that it can send a WINDOW_UPDATE frame. This is a critical part of
+    // the flow control process in http2
     CHECK_EQ(nghttp2_session_consume_connection(handle, len), 0);
     Http2Stream* stream = session->FindStream(id);
+    // If the stream has been destroyed, ignore this chunk
+    if (stream->IsDestroyed())
+      return 0;
     stream->AddChunk(data, len);
   }
   return 0;
 }
 
-
-inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* session,
+// Called by nghttp2 when it needs to determine how much padding to use in
+// a DATA or HEADERS frame.
+inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* handle,
                                              const nghttp2_frame* frame,
                                              size_t maxPayloadLen,
                                              void* user_data) {
-  Http2Session* handle = static_cast<Http2Session*>(user_data);
+  Http2Session* session = static_cast<Http2Session*>(user_data);
   ssize_t padding = frame->hd.length;
 
-  return handle->padding_strategy_ == PADDING_STRATEGY_MAX
-    ? handle->OnMaxFrameSizePadding(padding, maxPayloadLen)
-    : handle->OnCallbackPadding(padding, maxPayloadLen);
+  return session->padding_strategy_ == PADDING_STRATEGY_MAX
+    ? session->OnMaxFrameSizePadding(padding, maxPayloadLen)
+    : session->OnCallbackPadding(padding, maxPayloadLen);
 }
 
 #define BAD_PEER_MESSAGE "Remote peer returned unexpected data while we "     \
                          "expected SETTINGS frame.  Perhaps, peer does not "  \
                          "support HTTP/2 properly."
 
+// We use this currently to determine when an attempt is made to use the http2
+// protocol with a non-http2 peer.
 inline int Http2Session::OnNghttpError(nghttp2_session* handle,
                                        const char* message,
                                        size_t len,
@@ -705,9 +837,11 @@ inline int Http2Session::OnNghttpError(nghttp2_session* handle,
   return 0;
 }
 
-
+// Once all of the DATA frames for a Stream have been sent, the GetTrailers
+// method calls out to JavaScript to fetch the trailing headers that need
+// to be sent.
 inline void Http2Session::GetTrailers(Http2Stream* stream, uint32_t* flags) {
-  if (stream->HasTrailers()) {
+  if (!stream->IsDestroyed() && stream->HasTrailers()) {
     Http2Stream::SubmitTrailers submit_trailers{this, stream, flags};
     stream->OnTrailers(submit_trailers);
   }
@@ -734,6 +868,9 @@ inline void Http2Stream::SubmitTrailers::Submit(nghttp2_nv* trailers,
 }
 
 
+// Called by OnFrameReceived to notify JavaScript land that a complete
+// HEADERS frame has been received and processed. This method converts the
+// received headers into a JavaScript array and pushes those out to JS.
 inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
   Isolate* isolate = env()->isolate();
   HandleScope scope(isolate);
@@ -744,6 +881,10 @@ inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
   DEBUG_HTTP2SESSION2(this, "handle headers frame for stream %d", id);
   Http2Stream* stream = FindStream(id);
 
+  // If the stream has already been destroyed, ignore.
+  if (stream->IsDestroyed())
+    return;
+
   nghttp2_header* headers = stream->headers();
   size_t count = stream->headers_count();
 
@@ -795,6 +936,10 @@ inline void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
 }
 
 
+// Called by OnFrameReceived when a complete PRIORITY frame has been
+// received. Notifies JS land about the priority change. Note that priorities
+// are considered advisory only, so this has no real effect other than to
+// simply let user code know that the priority has changed.
 inline void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
   Isolate* isolate = env()->isolate();
   HandleScope scope(isolate);
@@ -817,11 +962,19 @@ inline void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
 }
 
 
+// Called by OnFrameReceived when a complete DATA frame has been received.
+// If we know that this is the last DATA frame (because the END_STREAM flag
+// is set), then we'll terminate the readable side of the StreamBase. If
+// the StreamBase is flowing, we'll push the chunks of data out to JS land.
 inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
   int32_t id = GetFrameID(frame);
   DEBUG_HTTP2SESSION2(this, "handling data frame for stream %d", id);
   Http2Stream* stream = FindStream(id);
 
+  // If the stream has already been destroyed, do nothing
+  if (stream->IsDestroyed())
+    return;
+
   if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
     stream->AddChunk(nullptr, 0);
   }
@@ -831,6 +984,7 @@ inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
 }
 
 
+// Called by OnFrameReceived when a complete GOAWAY frame has been received.
 inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
   Isolate* isolate = env()->isolate();
   HandleScope scope(isolate);
@@ -856,6 +1010,7 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
   MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
 }
 
+// Called by OnFrameReceived when a complete PING frame has been received.
 inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
   bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
   if (ack) {
@@ -865,7 +1020,7 @@ inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
   }
 }
 
-
+// Called by OnFrameReceived when a complete SETTINGS frame has been received.
 inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
   Isolate* isolate = env()->isolate();
   HandleScope scope(isolate);
@@ -878,6 +1033,10 @@ inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
   MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
 }
 
+// If the underlying nghttp2_session struct has data pending in its outbound
+// queue, MaybeScheduleWrite will schedule a SendPendingData() call to occcur
+// on the next iteration of the Node.js event loop (using the SetImmediate
+// queue), but only if a write has not already been scheduled.
 void Http2Session::MaybeScheduleWrite() {
   CHECK_EQ(flags_ & SESSION_STATE_WRITE_SCHEDULED, 0);
   if (session_ != nullptr && nghttp2_session_want_write(session_)) {
@@ -900,16 +1059,24 @@ void Http2Session::MaybeScheduleWrite() {
   }
 }
 
-
+// Prompts nghttp2 to begin serializing it's pending data and pushes each
+// chunk out to the i/o socket to be sent. This is a particularly hot method
+// that will generally be called at least twice be event loop iteration.
+// This is a potential performance optimization target later.
 void Http2Session::SendPendingData() {
   DEBUG_HTTP2SESSION(this, "sending pending data");
   // Do not attempt to send data on the socket if the destroying flag has
   // been set. That means everything is shutting down and the socket
   // will not be usable.
-  if (IsDestroying())
+  if (IsDestroyed())
     return;
   flags_ &= ~SESSION_STATE_WRITE_SCHEDULED;
 
+  // SendPendingData should not be called recursively.
+  if (flags_ & SESSION_STATE_SENDING)
+    return;
+  flags_ |= SESSION_STATE_SENDING;
+
   WriteWrap* req = nullptr;
   char* dest = nullptr;
   size_t destRemaining = 0;
@@ -958,14 +1125,17 @@ void Http2Session::SendPendingData() {
     }
   }
   CHECK_NE(srcLength, NGHTTP2_ERR_NOMEM);
-
-  if (destLength > 0) {
+  if (destLength > 0 && srcLength >= 0) {
     DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", destLength);
     Send(req, dest, destLength);
   }
-}
+  DEBUG_HTTP2SESSION2(this, "wants data in return? %d",
+                      nghttp2_session_want_read(session_));
 
+  flags_ &= ~SESSION_STATE_SENDING;
+}
 
+// Creates a new Http2Stream and submits a new http2 request.
 inline Http2Stream* Http2Session::SubmitRequest(
     nghttp2_priority_spec* prispec,
     nghttp2_nv* nva,
@@ -987,7 +1157,7 @@ inline void Http2Session::SetChunksSinceLastWrite(size_t n) {
   chunks_sent_since_last_write_ = n;
 }
 
-
+// Allocates the data buffer used to pass outbound data to the i/o stream.
 WriteWrap* Http2Session::AllocateSend() {
   HandleScope scope(env()->isolate());
   Local<Object> obj =
@@ -1002,12 +1172,11 @@ WriteWrap* Http2Session::AllocateSend() {
   return WriteWrap::New(env(), obj, stream_, size + 9);
 }
 
+// Pushes chunks of data to the i/o stream.
 void Http2Session::Send(WriteWrap* req, char* buf, size_t length) {
-  DEBUG_HTTP2SESSION(this, "attempting to send data");
-  if (stream_ == nullptr || !stream_->IsAlive() || stream_->IsClosing()) {
+  DEBUG_HTTP2SESSION2(this, "attempting to send %d bytes", length);
+  if (stream_ == nullptr)
     return;
-  }
-
   chunks_sent_since_last_write_++;
   uv_buf_t actual = uv_buf_init(buf, length);
   if (stream_->DoWrite(req, &actual, 1, nullptr)) {
@@ -1015,7 +1184,7 @@ void Http2Session::Send(WriteWrap* req, char* buf, size_t length) {
   }
 }
 
-
+// Allocates the data buffer used to receive inbound data from the i/o stream
 void Http2Session::OnStreamAllocImpl(size_t suggested_size,
                                      uv_buf_t* buf,
                                      void* ctx) {
@@ -1024,13 +1193,14 @@ void Http2Session::OnStreamAllocImpl(size_t suggested_size,
   buf->len = kAllocBufferSize;
 }
 
-
+// Callback used to receive inbound data from the i/o stream
 void Http2Session::OnStreamReadImpl(ssize_t nread,
                                     const uv_buf_t* bufs,
                                     uv_handle_type pending,
                                     void* ctx) {
   Http2Session* session = static_cast<Http2Session*>(ctx);
   Http2Scope h2scope(session);
+  DEBUG_HTTP2SESSION2(session, "receiving %d bytes", nread);
   if (nread < 0) {
     uv_buf_t tmp_buf;
     tmp_buf.base = nullptr;
@@ -1041,19 +1211,42 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
                               session->prev_read_cb_.ctx);
     return;
   }
-  if (nread > 0) {
+  if (bufs->len > 0) {
     // Only pass data on if nread > 0
     uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) };
     ssize_t ret = session->Write(buf, 1);
-    if (ret < 0) {
+
+    // Note: if ssize_t is not defined (e.g. on Win32), nghttp2 will typedef
+    // ssize_t to int. Cast here so that the < 0 check actually works on
+    // Windows.
+    if (static_cast<int>(ret) < 0) {
       DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret);
-      CHECK_EQ(nghttp2_session_terminate_session(session->session(),
-                                                 NGHTTP2_PROTOCOL_ERROR), 0);
+      Environment* env = session->env();
+      Isolate* isolate = env->isolate();
+      HandleScope scope(isolate);
+      Local<Context> context = env->context();
+      Context::Scope context_scope(context);
+
+      Local<Value> argv[1] = {
+        Integer::New(isolate, ret),
+      };
+      session->MakeCallback(env->error_string(), arraysize(argv), argv);
+    } else {
+      DEBUG_HTTP2SESSION2(session, "processed %d bytes. wants more? %d", ret,
+                          nghttp2_session_want_read(**session));
     }
   }
 }
 
+void Http2Session::OnStreamDestructImpl(void* ctx) {
+  Http2Session* session = static_cast<Http2Session*>(ctx);
+  session->stream_ = nullptr;
+}
 
+// Every Http2Session session is tightly bound to a single i/o StreamBase
+// (typically a net.Socket or tls.TLSSocket). The lifecycle of the two is
+// tightly coupled with all data transfer between the two happening at the
+// C++ layer via the StreamBase API.
 void Http2Session::Consume(Local<External> external) {
   StreamBase* stream = static_cast<StreamBase*>(external->Value());
   stream->Consume();
@@ -1062,24 +1255,11 @@ void Http2Session::Consume(Local<External> external) {
   prev_read_cb_ = stream->read_cb();
   stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this });
   stream->set_read_cb({ Http2Session::OnStreamReadImpl, this });
+  stream->set_destruct_cb({ Http2Session::OnStreamDestructImpl, this });
   DEBUG_HTTP2SESSION(this, "i/o stream consumed");
 }
 
 
-void Http2Session::Unconsume() {
-  if (prev_alloc_cb_.is_empty())
-    return;
-  stream_->set_alloc_cb(prev_alloc_cb_);
-  stream_->set_read_cb(prev_read_cb_);
-  prev_alloc_cb_.clear();
-  prev_read_cb_.clear();
-  stream_ = nullptr;
-  DEBUG_HTTP2SESSION(this, "i/o stream unconsumed");
-}
-
-
-
-
 Http2Stream::Http2Stream(
     Http2Session* session,
     int32_t id,
@@ -1119,26 +1299,37 @@ Http2Stream::Http2Stream(
 
 
 Http2Stream::~Http2Stream() {
-  CHECK(persistent().IsEmpty());
+  if (session_ != nullptr) {
+    session_->RemoveStream(id_);
+    session_ = nullptr;
+  }
+
   if (!object().IsEmpty())
     ClearWrap(object());
   persistent().Reset();
+  CHECK(persistent().IsEmpty());
 }
 
+// Notify the Http2Stream that a new block of HEADERS is being processed.
 void Http2Stream::StartHeaders(nghttp2_headers_category category) {
   DEBUG_HTTP2STREAM2(this, "starting headers, category: %d", id_, category);
+  CHECK(!this->IsDestroyed());
   current_headers_length_ = 0;
   current_headers_.clear();
   current_headers_category_ = category;
 }
 
+
 nghttp2_stream* Http2Stream::operator*() {
   return nghttp2_session_find_stream(**session_, id_);
 }
 
 
+// Calls out to JavaScript land to fetch the actual trailer headers to send
+// for this stream.
 void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
   DEBUG_HTTP2STREAM(this, "prompting for trailers");
+  CHECK(!this->IsDestroyed());
   Isolate* isolate = env()->isolate();
   HandleScope scope(isolate);
   Local<Context> context = env()->context();
@@ -1146,7 +1337,7 @@ void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
 
   Local<Value> ret =
       MakeCallback(env()->ontrailers_string(), 0, nullptr).ToLocalChecked();
-  if (!ret.IsEmpty()) {
+  if (!ret.IsEmpty() && !IsDestroyed()) {
     if (ret->IsArray()) {
       Local<Array> headers = ret.As<Array>();
       if (headers->Length() > 0) {
@@ -1157,21 +1348,36 @@ void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
   }
 }
 
+inline bool Http2Stream::HasDataChunks(bool ignore_eos) {
+  return data_chunks_.size() > (ignore_eos ? 1 : 0);
+}
 
+// Appends a chunk of received DATA frame data to this Http2Streams internal
+// queue. Note that we must memcpy each chunk because of the way that nghttp2
+// handles it's internal memory`.
 inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) {
+  CHECK(!this->IsDestroyed());
+  if (flags_ & NGHTTP2_STREAM_FLAG_EOS)
+    return;
   char* buf = nullptr;
-  if (len > 0) {
+  if (len > 0 && data != nullptr) {
     buf = Malloc<char>(len);
     memcpy(buf, data, len);
+  } else if (data == nullptr) {
+    flags_ |= NGHTTP2_STREAM_FLAG_EOS;
   }
   data_chunks_.emplace(uv_buf_init(buf, len));
 }
 
-
+// The Http2Stream class is a subclass of StreamBase. The DoWrite method
+// receives outbound chunks of data to send as outbound DATA frames. These
+// are queued in an internal linked list of uv_buf_t structs that are sent
+// when nghttp2 is ready to serialize the data frame.
 int Http2Stream::DoWrite(WriteWrap* req_wrap,
                          uv_buf_t* bufs,
                          size_t count,
                          uv_stream_t* send_handle) {
+  CHECK(!this->IsDestroyed());
   session_->SetChunksSinceLastWrite();
 
   nghttp2_stream_write_t* req = new nghttp2_stream_write_t;
@@ -1189,6 +1395,7 @@ int Http2Stream::DoWrite(WriteWrap* req_wrap,
 
 
 inline void Http2Stream::Close(int32_t code) {
+  CHECK(!this->IsDestroyed());
   flags_ |= NGHTTP2_STREAM_FLAG_CLOSED;
   code_ = code;
   DEBUG_HTTP2STREAM2(this, "closed with code %d", code);
@@ -1196,6 +1403,7 @@ inline void Http2Stream::Close(int32_t code) {
 
 
 inline void Http2Stream::Shutdown() {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   flags_ |= NGHTTP2_STREAM_FLAG_SHUT;
   CHECK_NE(nghttp2_session_resume_data(session_->session(), id_),
@@ -1204,26 +1412,23 @@ inline void Http2Stream::Shutdown() {
 }
 
 int Http2Stream::DoShutdown(ShutdownWrap* req_wrap) {
+  CHECK(!this->IsDestroyed());
   req_wrap->Dispatched();
   Shutdown();
   req_wrap->Done(0);
   return 0;
 }
 
+// Destroy the Http2Stream and render it unusable. Actual resources for the
+// Stream will not be freed until the next tick of the Node.js event loop
+// using the SetImmediate queue.
 inline void Http2Stream::Destroy() {
-  Http2Scope h2scope(this);
-  DEBUG_HTTP2STREAM(this, "destroying stream");
   // Do nothing if this stream instance is already destroyed
   if (IsDestroyed())
     return;
-
   flags_ |= NGHTTP2_STREAM_FLAG_DESTROYED;
-  Http2Session* session = this->session_;
 
-  if (session != nullptr) {
-    session_->RemoveStream(id_);
-    session_ = nullptr;
-  }
+  DEBUG_HTTP2STREAM(this, "destroying stream");
 
   // Free any remaining incoming data chunks.
   while (!data_chunks_.empty()) {
@@ -1232,24 +1437,30 @@ inline void Http2Stream::Destroy() {
     data_chunks_.pop();
   }
 
-  // Free any remaining outgoing data chunks.
-  while (!queue_.empty()) {
-    nghttp2_stream_write* head = queue_.front();
-    head->cb(head->req, UV_ECANCELED);
-    delete head;
-    queue_.pop();
-  }
-
-  if (!object().IsEmpty())
-    ClearWrap(object());
-  persistent().Reset();
+  // Wait until the start of the next loop to delete because there
+  // may still be some pending operations queued for this stream.
+  env()->SetImmediate([](Environment* env, void* data) {
+    Http2Stream* stream = static_cast<Http2Stream*>(data);
+
+    // Free any remaining outgoing data chunks here. This should be done
+    // here because it's possible for destroy to have been called while
+    // we still have qeueued outbound writes.
+    while (!stream->queue_.empty()) {
+      nghttp2_stream_write* head = stream->queue_.front();
+      head->cb(head->req, UV_ECANCELED);
+      delete head;
+      stream->queue_.pop();
+    }
 
-  delete this;
+    delete stream;
+  }, this, this->object());
 }
 
 
-void Http2Stream::OnDataChunk(
-    uv_buf_t* chunk) {
+// Uses the StreamBase API to push a single chunk of queued inbound DATA
+// to JS land.
+void Http2Stream::OnDataChunk(uv_buf_t* chunk) {
+  CHECK(!this->IsDestroyed());
   Isolate* isolate = env()->isolate();
   HandleScope scope(isolate);
   ssize_t len = -1;
@@ -1263,6 +1474,7 @@ void Http2Stream::OnDataChunk(
 
 
 inline void Http2Stream::FlushDataChunks() {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   if (!data_chunks_.empty()) {
     uv_buf_t buf = data_chunks_.front();
@@ -1278,9 +1490,12 @@ inline void Http2Stream::FlushDataChunks() {
 }
 
 
+// Initiates a response on the Http2Stream using data provided via the
+// StreamBase Streams API.
 inline int Http2Stream::SubmitResponse(nghttp2_nv* nva,
                                        size_t len,
                                        int options) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "submitting response");
   if (options & STREAM_OPTION_GET_TRAILERS)
@@ -1302,6 +1517,7 @@ inline int Http2Stream::SubmitFile(int fd,
                                    int64_t offset,
                                    int64_t length,
                                    int options) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "submitting file");
   if (options & STREAM_OPTION_GET_TRAILERS)
@@ -1319,6 +1535,7 @@ inline int Http2Stream::SubmitFile(int fd,
 
 // Submit informational headers for a stream.
 inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM2(this, "sending %d informational headers", len);
   int ret = nghttp2_submit_headers(session_->session(),
@@ -1329,9 +1546,10 @@ inline int Http2Stream::SubmitInfo(nghttp2_nv* nva, size_t len) {
   return ret;
 }
 
-
+// Submit a PRIORITY frame to the connected peer.
 inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
                                        bool silent) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "sending priority spec");
   int ret = silent ?
@@ -1344,27 +1562,28 @@ inline int Http2Stream::SubmitPriority(nghttp2_priority_spec* prispec,
   return ret;
 }
 
-
-inline int Http2Stream::SubmitRstStream(const uint32_t code) {
+// Closes the Http2Stream by submitting an RST_STREAM frame to the connected
+// peer.
+inline void Http2Stream::SubmitRstStream(const uint32_t code) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
-  DEBUG_HTTP2STREAM2(this, "sending rst-stream with code %d", code);
+  // Force a purge of any currently pending data here to make sure
+  // it is sent before closing the stream.
   session_->SendPendingData();
-  CHECK_EQ(nghttp2_submit_rst_stream(session_->session(),
-                                     NGHTTP2_FLAG_NONE,
-                                     id_,
-                                     code), 0);
-  return 0;
+  CHECK_EQ(nghttp2_submit_rst_stream(**session_, NGHTTP2_FLAG_NONE,
+                                     id_, code), 0);
 }
 
 
-// Submit a push promise.
+// Submit a push promise and create the associated Http2Stream if successful.
 inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva,
                                                    size_t len,
                                                    int32_t* ret,
                                                    int options) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   DEBUG_HTTP2STREAM(this, "sending push promise");
-  *ret = nghttp2_submit_push_promise(session_->session(), NGHTTP2_FLAG_NONE,
+  *ret = nghttp2_submit_push_promise(**session_, NGHTTP2_FLAG_NONE,
                                      id_, nva, len, nullptr);
   CHECK_NE(*ret, NGHTTP2_ERR_NOMEM);
   Http2Stream* stream = nullptr;
@@ -1374,7 +1593,10 @@ inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva,
   return stream;
 }
 
+// Switch the StreamBase into flowing mode to begin pushing chunks of data
+// out to JS land.
 inline int Http2Stream::ReadStart() {
+  CHECK(!this->IsDestroyed());
   flags_ |= NGHTTP2_STREAM_FLAG_READ_START;
   flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED;
 
@@ -1384,8 +1606,9 @@ inline int Http2Stream::ReadStart() {
   return 0;
 }
 
-
+// Switch the StreamBase into paused mode.
 inline int Http2Stream::ReadStop() {
+  CHECK(!this->IsDestroyed());
   if (!IsReading())
     return 0;
   flags_ |= NGHTTP2_STREAM_FLAG_READ_PAUSED;
@@ -1402,6 +1625,7 @@ inline int Http2Stream::Write(nghttp2_stream_write_t* req,
                               const uv_buf_t bufs[],
                               unsigned int nbufs,
                               nghttp2_stream_write_cb cb) {
+  CHECK(!this->IsDestroyed());
   Http2Scope h2scope(this);
   if (!IsWritable()) {
     if (cb != nullptr)
@@ -1424,9 +1648,15 @@ inline size_t GetBufferLength(nghttp2_rcbuf* buf) {
   return nghttp2_rcbuf_get_buf(buf).len;
 }
 
+// Ads a header to the Http2Stream. Note that the header name and value are
+// provided using a buffer structure provided by nghttp2 that allows us to
+// avoid unnecessary memcpy's. Those buffers are ref counted. The ref count
+// is incremented here and are decremented when the header name and values
+// are garbage collected later.
 inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
                                    nghttp2_rcbuf* value,
                                    uint8_t flags) {
+  CHECK(!this->IsDestroyed());
   size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
   if (current_headers_.size() == max_header_pairs_ ||
       current_headers_length_ + length > max_header_length_) {
@@ -1455,7 +1685,9 @@ Http2Stream* GetStream(Http2Session* session,
   return stream;
 }
 
+// A Provider is the thing that provides outbound DATA frame data.
 Http2Stream::Provider::Provider(Http2Stream* stream, int options) {
+  CHECK(!stream->IsDestroyed());
   provider_.source.ptr = stream;
   empty_ = options & STREAM_OPTION_EMPTY_PAYLOAD;
 }
@@ -1469,8 +1701,12 @@ Http2Stream::Provider::~Provider() {
   provider_.source.ptr = nullptr;
 }
 
+// The FD Provider pulls data from a file descriptor using libuv. All of the
+// data transfer occurs in C++, without any chunks being passed through JS
+// land.
 Http2Stream::Provider::FD::FD(Http2Stream* stream, int options, int fd)
     : Http2Stream::Provider(stream, options) {
+  CHECK(!stream->IsDestroyed());
   provider_.source.fd = fd;
   provider_.read_callback = Http2Stream::Provider::FD::OnRead;
 }
@@ -1490,6 +1726,7 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
                                           void* user_data) {
   Http2Session* session = static_cast<Http2Session*>(user_data);
   Http2Stream* stream = session->FindStream(id);
+
   DEBUG_HTTP2SESSION2(session, "reading outbound file data for stream %d", id);
   CHECK_EQ(id, stream->id());
 
@@ -1524,16 +1761,26 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
   stream->fd_offset_ += numchars;
   stream->fd_length_ -= numchars;
 
+  DEBUG_HTTP2SESSION2(session, "sending %d bytes", numchars);
+
   // if numchars < length, assume that we are done.
   if (static_cast<size_t>(numchars) < length || length <= 0) {
     DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id);
     *flags |= NGHTTP2_DATA_FLAG_EOF;
     session->GetTrailers(stream, flags);
+    // If the stream or session gets destroyed during the GetTrailers
+    // callback, check that here and close down the stream
+    if (stream->IsDestroyed())
+      return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+    if (session->IsDestroyed())
+      return NGHTTP2_ERR_CALLBACK_FAILURE;
   }
 
   return numchars;
 }
 
+// The Stream Provider pulls data from a linked list of uv_buf_t structs
+// built via the StreamBase API and the Streams js API.
 Http2Stream::Provider::Stream::Stream(int options)
     : Http2Stream::Provider(options) {
   provider_.read_callback = Http2Stream::Provider::Stream::OnRead;
@@ -1593,10 +1840,14 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
   if (stream->queue_.empty() && !stream->IsWritable()) {
     DEBUG_HTTP2SESSION2(session, "no more data for stream %d", id);
     *flags |= NGHTTP2_DATA_FLAG_EOF;
-
     session->GetTrailers(stream, flags);
+    // If the stream or session gets destroyed during the GetTrailers
+    // callback, check that here and close down the stream
+    if (stream->IsDestroyed())
+      return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+    if (session->IsDestroyed())
+      return NGHTTP2_ERR_CALLBACK_FAILURE;
   }
-
   return amount;
 }
 
@@ -1604,6 +1855,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
 
 // Implementation of the JavaScript API
 
+// Fetches the string description of a nghttp2 error code and passes that
+// back to JS land
 void HttpErrorString(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   uint32_t val = args[0]->Uint32Value(env->context()).ToChecked();
@@ -1624,13 +1877,15 @@ void PackSettings(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(settings.Pack());
 }
 
-
+// A TypedArray instance is shared between C++ and JS land to contain the
+// default SETTINGS. RefreshDefaultSettings updates that TypedArray with the
+// default values.
 void RefreshDefaultSettings(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Http2Settings::RefreshDefaults(env);
 }
 
-
+// Sets the next stream ID the Http2Session. If successful, returns true.
 void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Http2Session* session;
@@ -1644,7 +1899,9 @@ void Http2Session::SetNextStreamID(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2SESSION2(session, "set next stream id to %d", id);
 }
 
-
+// A TypedArray instance is shared between C++ and JS land to contain the
+// SETTINGS (either remote or local). RefreshSettings updates the current
+// values established for each of the settings so those can be read in JS land.
 template <get_setting fn>
 void Http2Session::RefreshSettings(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
@@ -1654,7 +1911,9 @@ void Http2Session::RefreshSettings(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2SESSION(session, "settings refreshed for session");
 }
 
-
+// A TypedArray instance is shared between C++ and JS land to contain state
+// information of the current Http2Session. This updates the values in the
+// TypedRray so those can be read in JS land.
 void Http2Session::RefreshState(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Http2Session* session;
@@ -1687,6 +1946,7 @@ void Http2Session::RefreshState(const FunctionCallbackInfo<Value>& args) {
 }
 
 
+// Constructor for new Http2Session instances.
 void Http2Session::New(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   CHECK(args.IsConstructCall());
@@ -1698,6 +1958,7 @@ void Http2Session::New(const FunctionCallbackInfo<Value>& args) {
 }
 
 
+// Binds the Http2Session with a StreamBase used for i/o
 void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) {
   Http2Session* session;
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
@@ -1705,31 +1966,21 @@ void Http2Session::Consume(const FunctionCallbackInfo<Value>& args) {
   session->Consume(args[0].As<External>());
 }
 
-
+// Destroys the Http2Session instance and renders it unusable
 void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
   Http2Session* session;
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
   DEBUG_HTTP2SESSION(session, "destroying session");
-
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
 
-  bool skipUnconsume = args[0]->BooleanValue(context).ToChecked();
-
-  if (!skipUnconsume)
-    session->Unconsume();
-  session->Close();
-}
-
+  uint32_t code = args[0]->Uint32Value(context).ToChecked();
+  bool socketDestroyed = args[1]->BooleanValue(context).ToChecked();
 
-void Http2Session::Destroying(const FunctionCallbackInfo<Value>& args) {
-  Http2Session* session;
-  ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-  session->MarkDestroying();
-  DEBUG_HTTP2SESSION(session, "preparing to destroy session");
+  session->Close(code, socketDestroyed);
 }
 
-
+// Submits a SETTINGS frame for the Http2Session
 void Http2Session::Settings(const FunctionCallbackInfo<Value>& args) {
   Http2Session* session;
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
@@ -1740,7 +1991,8 @@ void Http2Session::Settings(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2SESSION(session, "settings submitted");
 }
 
-
+// Submits a new request on the Http2Session and returns either an error code
+// or the Http2Stream object.
 void Http2Session::Request(const FunctionCallbackInfo<Value>& args) {
   Http2Session* session;
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
@@ -1772,49 +2024,50 @@ void Http2Session::Request(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(stream->object());
 }
 
+// Submits a GOAWAY frame to signal that the Http2Session is in the process
+// of shutting down. Note that this function does not actually alter the
+// state of the Http2Session, it's simply a notification.
+void Http2Session::Goaway(uint32_t code,
+                          int32_t lastStreamID,
+                          uint8_t* data,
+                          size_t len) {
+  if (IsDestroyed())
+    return;
 
-void Http2Session::ShutdownNotice(const FunctionCallbackInfo<Value>& args) {
-  Http2Session* session;
-  ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-  session->SubmitShutdownNotice();
-  DEBUG_HTTP2SESSION(session, "shutdown notice sent");
+  Http2Scope h2scope(this);
+  // the last proc stream id is the most recently created Http2Stream.
+  if (lastStreamID <= 0)
+    lastStreamID = nghttp2_session_get_last_proc_stream_id(session_);
+  DEBUG_HTTP2SESSION(this, "submitting goaway");
+  nghttp2_submit_goaway(session_, NGHTTP2_FLAG_NONE,
+                        lastStreamID, code, data, len);
 }
 
-
+// Submits a GOAWAY frame to signal that the Http2Session is in the process
+// of shutting down. The opaque data argument is an optional TypedArray that
+// can be used to send debugging data to the connected peer.
 void Http2Session::Goaway(const FunctionCallbackInfo<Value>& args) {
-  Http2Session* session;
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
+  Http2Session* session;
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-  Http2Scope h2scope(session);
 
-  uint32_t errorCode = args[0]->Uint32Value(context).ToChecked();
+  uint32_t code = args[0]->Uint32Value(context).ToChecked();
   int32_t lastStreamID = args[1]->Int32Value(context).ToChecked();
   Local<Value> opaqueData = args[2];
-
   uint8_t* data = nullptr;
   size_t length = 0;
 
-  if (opaqueData->BooleanValue(context).ToChecked()) {
-    THROW_AND_RETURN_UNLESS_BUFFER(env, opaqueData);
-    SPREAD_BUFFER_ARG(opaqueData, buf);
-    data = reinterpret_cast<uint8_t*>(buf_data);
-    length = buf_length;
+  if (Buffer::HasInstance(opaqueData)) {
+    data = reinterpret_cast<uint8_t*>(Buffer::Data(opaqueData));
+    length = Buffer::Length(opaqueData);
   }
 
-  int status = nghttp2_submit_goaway(session->session(),
-                                     NGHTTP2_FLAG_NONE,
-                                     lastStreamID,
-                                     errorCode,
-                                     data, length);
-  CHECK_NE(status, NGHTTP2_ERR_NOMEM);
-  args.GetReturnValue().Set(status);
-  DEBUG_HTTP2SESSION2(session, "immediate shutdown initiated with "
-                      "last stream id %d, code %d, and opaque-data length %d",
-                      lastStreamID, errorCode, length);
+  session->Goaway(code, lastStreamID, data, length);
 }
 
-
+// Update accounting of data chunks. This is used primarily to manage timeout
+// logic when using the FD Provider.
 void Http2Session::UpdateChunksSent(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Isolate* isolate = env->isolate();
@@ -1831,18 +2084,21 @@ void Http2Session::UpdateChunksSent(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(length);
 }
 
-
+// Submits an RST_STREAM frame effectively closing the Http2Stream. Note that
+// this *WILL* alter the state of the stream, causing the OnStreamClose
+// callback to the triggered.
 void Http2Stream::RstStream(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
   Http2Stream* stream;
   ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
   uint32_t code = args[0]->Uint32Value(context).ToChecked();
-  args.GetReturnValue().Set(stream->SubmitRstStream(code));
-  DEBUG_HTTP2STREAM2(stream, "rst_stream code %d sent", code);
+  DEBUG_HTTP2STREAM2(stream, "sending rst_stream with code %d", code);
+  stream->SubmitRstStream(code);
 }
 
-
+// Initiates a response on the Http2Stream using the StreamBase API to provide
+// outbound DATA frames.
 void Http2Stream::Respond(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -1860,7 +2116,8 @@ void Http2Stream::Respond(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2STREAM(stream, "response submitted");
 }
 
-
+// Initiates a response on the Http2Stream using a file descriptor to provide
+// outbound DATA frames.
 void Http2Stream::RespondFD(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -1883,7 +2140,7 @@ void Http2Stream::RespondFD(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2STREAM2(stream, "file response submitted for fd %d", fd);
 }
 
-
+// Submits informational headers on the Http2Stream
 void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -1899,14 +2156,14 @@ void Http2Stream::Info(const FunctionCallbackInfo<Value>& args) {
                      headers->Length());
 }
 
-
+// Grab the numeric id of the Http2Stream
 void Http2Stream::GetID(const FunctionCallbackInfo<Value>& args) {
   Http2Stream* stream;
   ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
   args.GetReturnValue().Set(stream->id());
 }
 
-
+// Destroy the Http2Stream, rendering it no longer usable
 void Http2Stream::Destroy(const FunctionCallbackInfo<Value>& args) {
   Http2Stream* stream;
   ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
@@ -1914,7 +2171,7 @@ void Http2Stream::Destroy(const FunctionCallbackInfo<Value>& args) {
   stream->Destroy();
 }
 
-
+// Prompt the Http2Stream to begin sending data to the JS land.
 void Http2Stream::FlushData(const FunctionCallbackInfo<Value>& args) {
   Http2Stream* stream;
   ASSIGN_OR_RETURN_UNWRAP(&stream, args.Holder());
@@ -1922,7 +2179,7 @@ void Http2Stream::FlushData(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2STREAM(stream, "data flushed to js");
 }
 
-
+// Initiate a Push Promise and create the associated Http2Stream
 void Http2Stream::PushPromise(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -1948,7 +2205,7 @@ void Http2Stream::PushPromise(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(stream->object());
 }
 
-
+// Send a PRIORITY frame
 void Http2Stream::Priority(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Local<Context> context = env->context();
@@ -1962,7 +2219,9 @@ void Http2Stream::Priority(const FunctionCallbackInfo<Value>& args) {
   DEBUG_HTTP2STREAM(stream, "priority submitted");
 }
 
-
+// A TypedArray shared by C++ and JS land is used to communicate state
+// information about the Http2Stream. This updates the values in that
+// TypedArray so that the state can be read by JS.
 void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Http2Stream* stream;
@@ -1999,11 +2258,14 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
   }
 }
 
+// Submits a PING frame to be sent to the connected peer.
 void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
   Http2Session* session;
   ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
 
+  // A PING frame may have exactly 8 bytes of payload data. If not provided,
+  // then the current hrtime will be used as the payload.
   uint8_t* payload = nullptr;
   if (Buffer::HasInstance(args[0])) {
     payload = reinterpret_cast<uint8_t*>(Buffer::Data(args[0]));
@@ -2014,11 +2276,18 @@ void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
   Local<Object> obj = ping->object();
   obj->Set(env->context(), env->ondone_string(), args[1]).FromJust();
 
+  // To prevent abuse, we strictly limit the number of unacknowledged PING
+  // frames that may be sent at any given time. This is configurable in the
+  // Options when creating a Http2Session.
   if (!session->AddPing(ping)) {
     ping->Done(false);
     return args.GetReturnValue().Set(false);
   }
 
+  // The Ping itself is an Async resource. When the acknowledgement is recieved,
+  // the callback will be invoked and a notification sent out to JS land. The
+  // notification will include the duration of the ping, allowing the round
+  // trip to be measured.
   ping->Send(payload);
   args.GetReturnValue().Set(true);
 }
@@ -2039,6 +2308,7 @@ bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
   return true;
 }
 
+
 Http2Session::Http2Ping::Http2Ping(
     Http2Session* session)
         : AsyncWrap(session->env(),
@@ -2086,6 +2356,8 @@ void Http2Session::Http2Ping::Done(bool ack, const uint8_t* payload) {
   delete this;
 }
 
+
+// Set up the process.binding('http2') binding.
 void Initialize(Local<Object> target,
                 Local<Value> unused,
                 Local<Context> context,
@@ -2164,8 +2436,6 @@ void Initialize(Local<Object> target,
   env->SetProtoMethod(session, "ping", Http2Session::Ping);
   env->SetProtoMethod(session, "consume", Http2Session::Consume);
   env->SetProtoMethod(session, "destroy", Http2Session::Destroy);
-  env->SetProtoMethod(session, "destroying", Http2Session::Destroying);
-  env->SetProtoMethod(session, "shutdownNotice", Http2Session::ShutdownNotice);
   env->SetProtoMethod(session, "goaway", Http2Session::Goaway);
   env->SetProtoMethod(session, "settings", Http2Session::Settings);
   env->SetProtoMethod(session, "request", Http2Session::Request);
diff --git a/src/node_http2.h b/src/node_http2.h
index 9960361f79c3ff..12eacb07659549 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -70,7 +70,13 @@ void inline debug_vfprintf(const char* format, ...) {
 #define DEBUG_HTTP2STREAM2(...) do {} while (0)
 #endif
 
+// We strictly limit the number of outstanding unacknowledged PINGS a user
+// may send in order to prevent abuse. The current default cap is 10. The
+// user may set a different limit using a per Http2Session configuration
+// option.
 #define DEFAULT_MAX_PINGS 10
+
+// These are the standard HTTP/2 defaults as specified by the RFC
 #define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
 #define DEFAULT_SETTINGS_ENABLE_PUSH 1
 #define DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE 65535
@@ -83,10 +89,10 @@ void inline debug_vfprintf(const char* format, ...) {
 #define MAX_MAX_HEADER_LIST_SIZE 16777215u
 #define DEFAULT_MAX_HEADER_LIST_PAIRS 128u
 
-struct nghttp2_stream_write_t;
-
 #define MAX_BUFFER_COUNT 16
 
+struct nghttp2_stream_write_t;
+
 enum nghttp2_session_type {
   NGHTTP2_SESSION_SERVER,
   NGHTTP2_SESSION_CLIENT
@@ -109,11 +115,15 @@ enum nghttp2_stream_flags {
   // Stream is destroyed
   NGHTTP2_STREAM_FLAG_DESTROYED = 0x10,
   // Stream has trailers
-  NGHTTP2_STREAM_FLAG_TRAILERS = 0x20
+  NGHTTP2_STREAM_FLAG_TRAILERS = 0x20,
+  // Stream has received all the data it can
+  NGHTTP2_STREAM_FLAG_EOS = 0x40
 };
 
 enum nghttp2_stream_options {
+  // Stream is not going to have any DATA frames
   STREAM_OPTION_EMPTY_PAYLOAD = 0x1,
+  // Stream might have trailing headers
   STREAM_OPTION_GET_TRAILERS = 0x2,
 };
 
@@ -136,7 +146,6 @@ struct nghttp2_header {
 };
 
 
-
 struct nghttp2_stream_write_t {
   void* data;
   int status;
@@ -416,9 +425,10 @@ const char* nghttp2_errname(int rv) {
 
 enum session_state_flags {
   SESSION_STATE_NONE = 0x0,
-  SESSION_STATE_DESTROYING = 0x1,
-  SESSION_STATE_HAS_SCOPE = 0x2,
-  SESSION_STATE_WRITE_SCHEDULED = 0x4
+  SESSION_STATE_HAS_SCOPE = 0x1,
+  SESSION_STATE_WRITE_SCHEDULED = 0x2,
+  SESSION_STATE_CLOSED = 0x4,
+  SESSION_STATE_SENDING = 0x8,
 };
 
 // This allows for 4 default-sized frames with their frame headers
@@ -554,6 +564,8 @@ class Http2Stream : public AsyncWrap,
       unsigned int nbufs,
       nghttp2_stream_write_cb cb);
 
+  inline bool HasDataChunks(bool ignore_eos = false);
+
   inline void AddChunk(const uint8_t* data, size_t len);
 
   inline void FlushDataChunks();
@@ -591,7 +603,7 @@ class Http2Stream : public AsyncWrap,
                             bool silent = false);
 
   // Submits an RST_STREAM frame using the given code
-  inline int SubmitRstStream(const uint32_t code);
+  inline void SubmitRstStream(const uint32_t code);
 
   // Submits a PUSH_PROMISE frame with this stream as the parent.
   inline Http2Stream* SubmitPushPromise(
@@ -799,9 +811,11 @@ class Http2Session : public AsyncWrap {
 
   void Start();
   void Stop();
-  void Close();
+  void Close(uint32_t code = NGHTTP2_NO_ERROR,
+             bool socket_closed = false);
   void Consume(Local<External> external);
   void Unconsume();
+  void Goaway(uint32_t code, int32_t lastStreamID, uint8_t* data, size_t len);
 
   bool Ping(v8::Local<v8::Function> function);
 
@@ -827,8 +841,9 @@ class Http2Session : public AsyncWrap {
 
   inline const char* TypeName();
 
-  inline void MarkDestroying() { flags_ |= SESSION_STATE_DESTROYING; }
-  inline bool IsDestroying() { return flags_ & SESSION_STATE_DESTROYING; }
+  inline bool IsDestroyed() {
+    return (flags_ & SESSION_STATE_CLOSED) || session_ == nullptr;
+  }
 
   // Schedule a write if nghttp2 indicates it wants to write to the socket.
   void MaybeScheduleWrite();
@@ -842,9 +857,6 @@ class Http2Session : public AsyncWrap {
   // Removes a stream instance from this session
   inline void RemoveStream(int32_t id);
 
-  // Sends a notice to the connected peer that the session is shutting down.
-  inline void SubmitShutdownNotice();
-
   // Submits a SETTINGS frame to the connected peer.
   inline void Settings(const nghttp2_settings_entry iv[], size_t niv);
 
@@ -868,6 +880,7 @@ class Http2Session : public AsyncWrap {
                                const uv_buf_t* bufs,
                                uv_handle_type pending,
                                void* ctx);
+  static void OnStreamDestructImpl(void* ctx);
 
   // The JavaScript API
   static void New(const FunctionCallbackInfo<Value>& args);
@@ -878,7 +891,6 @@ class Http2Session : public AsyncWrap {
   static void Settings(const FunctionCallbackInfo<Value>& args);
   static void Request(const FunctionCallbackInfo<Value>& args);
   static void SetNextStreamID(const FunctionCallbackInfo<Value>& args);
-  static void ShutdownNotice(const FunctionCallbackInfo<Value>& args);
   static void Goaway(const FunctionCallbackInfo<Value>& args);
   static void UpdateChunksSent(const FunctionCallbackInfo<Value>& args);
   static void RefreshState(const FunctionCallbackInfo<Value>& args);
diff --git a/test/known_issues/test-http2-client-http1-server.js b/test/known_issues/test-http2-client-http1-server.js
index 53f7bf42c465e1..616427b3904e16 100644
--- a/test/known_issues/test-http2-client-http1-server.js
+++ b/test/known_issues/test-http2-client-http1-server.js
@@ -7,6 +7,7 @@ if (!common.hasCrypto)
 const http = require('http');
 const http2 = require('http2');
 
+// Creating an http1 server here...
 const server = http.createServer(common.mustNotCall());
 
 server.listen(0, common.mustCall(() => {
@@ -15,13 +16,17 @@ server.listen(0, common.mustCall(() => {
   const req = client.request();
   req.on('close', common.mustCall());
 
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_ERROR',
+    type: Error,
+    message: 'Protocol error'
+  }));
+
   client.on('error', common.expectsError({
     code: 'ERR_HTTP2_ERROR',
     type: Error,
     message: 'Protocol error'
   }));
 
-  client.on('close', (...args) => {
-    server.close();
-  });
+  client.on('close', common.mustCall(() => server.close()));
 }));
diff --git a/test/parallel/test-http2-client-data-end.js b/test/parallel/test-http2-client-data-end.js
index 43665029630c12..2f251692d5c412 100644
--- a/test/parallel/test-http2-client-data-end.js
+++ b/test/parallel/test-http2-client-data-end.js
@@ -5,53 +5,37 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const http2 = require('http2');
+const Countdown = require('../common/countdown');
 
 const server = http2.createServer();
 server.on('stream', common.mustCall((stream, headers, flags) => {
-  const port = server.address().port;
   if (headers[':path'] === '/') {
-    stream.pushStream({
-      ':scheme': 'http',
-      ':path': '/foobar',
-      ':authority': `localhost:${port}`,
-    }, (push, headers) => {
+    stream.pushStream({ ':path': '/foobar' }, (err, push, headers) => {
+      assert.ifError(err);
       push.respond({
         'content-type': 'text/html',
-        ':status': 200,
         'x-push-data': 'pushed by server',
       });
       push.write('pushed by server ');
-      // Sending in next immediate ensures that a second data frame
-      // will be sent to the client, which will cause the 'data' event
-      // to fire multiple times.
-      setImmediate(() => {
-        push.end('data');
-      });
+      setImmediate(() => push.end('data'));
       stream.end('st');
     });
   }
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  });
+  stream.respond({ 'content-type': 'text/html' });
   stream.write('te');
 }));
 
 
 server.listen(0, common.mustCall(() => {
   const port = server.address().port;
-  const headers = { ':path': '/' };
   const client = http2.connect(`http://localhost:${port}`);
 
-  const req = client.request(headers);
+  const req = client.request();
 
-  let expected = 2;
-  function maybeClose() {
-    if (--expected === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
+  const countdown = new Countdown(2, () => {
+    server.close();
+    client.close();
+  });
 
   req.on('response', common.mustCall((headers) => {
     assert.strictEqual(headers[':status'], 200);
@@ -70,13 +54,11 @@ server.listen(0, common.mustCall(() => {
 
     stream.setEncoding('utf8');
     let pushData = '';
-    stream.on('data', common.mustCall((d) => {
-      pushData += d;
-    }, 2));
+    stream.on('data', (d) => pushData += d);
     stream.on('end', common.mustCall(() => {
       assert.strictEqual(pushData, 'pushed by server data');
-      maybeClose();
     }));
+    stream.on('close', () => countdown.dec());
   }));
 
   let data = '';
@@ -85,7 +67,6 @@ server.listen(0, common.mustCall(() => {
   req.on('data', common.mustCallAtLeast((d) => data += d));
   req.on('end', common.mustCall(() => {
     assert.strictEqual(data, 'test');
-    maybeClose();
   }));
-  req.end();
+  req.on('close', () => countdown.dec());
 }));
diff --git a/test/parallel/test-http2-client-destroy.js b/test/parallel/test-http2-client-destroy.js
index bb93366247aef7..fab8a4fc24d652 100644
--- a/test/parallel/test-http2-client-destroy.js
+++ b/test/parallel/test-http2-client-destroy.js
@@ -8,139 +8,115 @@ if (!common.hasCrypto)
 const assert = require('assert');
 const h2 = require('http2');
 const { kSocket } = require('internal/http2/util');
+const Countdown = require('../common/countdown');
 
 {
   const server = h2.createServer();
-  server.listen(
-    0,
-    common.mustCall(() => {
-      const destroyCallbacks = [
-        (client) => client.destroy(),
-        (client) => client[kSocket].destroy()
-      ];
-
-      let remaining = destroyCallbacks.length;
-
-      destroyCallbacks.forEach((destroyCallback) => {
-        const client = h2.connect(`http://localhost:${server.address().port}`);
-        client.on(
-          'connect',
-          common.mustCall(() => {
-            const socket = client[kSocket];
-
-            assert(socket, 'client session has associated socket');
-            assert(
-              !client.destroyed,
-              'client has not been destroyed before destroy is called'
-            );
-            assert(
-              !socket.destroyed,
-              'socket has not been destroyed before destroy is called'
-            );
-
-            // Ensure that 'close' event is emitted
-            client.on('close', common.mustCall());
-
-            destroyCallback(client);
-
-            assert(
-              !client[kSocket],
-              'client.socket undefined after destroy is called'
-            );
-
-            // Must must be closed
-            client.on(
-              'close',
-              common.mustCall(() => {
-                assert(client.destroyed);
-              })
-            );
-
-            // socket will close on process.nextTick
-            socket.on(
-              'close',
-              common.mustCall(() => {
-                assert(socket.destroyed);
-              })
-            );
-
-            if (--remaining === 0) {
-              server.close();
-            }
-          })
+  server.listen(0, common.mustCall(() => {
+    const destroyCallbacks = [
+      (client) => client.destroy(),
+      (client) => client[kSocket].destroy()
+    ];
+
+    const countdown = new Countdown(destroyCallbacks.length, () => {
+      server.close();
+    });
+
+    destroyCallbacks.forEach((destroyCallback) => {
+      const client = h2.connect(`http://localhost:${server.address().port}`);
+      client.on('connect', common.mustCall(() => {
+        const socket = client[kSocket];
+
+        assert(socket, 'client session has associated socket');
+        assert(
+          !client.destroyed,
+          'client has not been destroyed before destroy is called'
         );
-      });
-    })
-  );
+        assert(
+          !socket.destroyed,
+          'socket has not been destroyed before destroy is called'
+        );
+
+        destroyCallback(client);
+
+        client.on('close', common.mustCall(() => {
+          assert(client.destroyed);
+        }));
+
+        countdown.dec();
+      }));
+    });
+  }));
 }
 
 // test destroy before client operations
 {
   const server = h2.createServer();
-  server.listen(
-    0,
-    common.mustCall(() => {
-      const client = h2.connect(`http://localhost:${server.address().port}`);
-      const req = client.request();
-      client.destroy();
-
-      req.on('response', common.mustNotCall());
-      req.resume();
-
-      const sessionError = {
-        type: Error,
-        code: 'ERR_HTTP2_INVALID_SESSION',
-        message: 'The session has been destroyed'
-      };
-
+  server.listen(0, common.mustCall(() => {
+    const client = h2.connect(`http://localhost:${server.address().port}`);
+    const socket = client[kSocket];
+    socket.on('close', common.mustCall(() => {
+      assert(socket.destroyed);
+    }));
+
+
+    const req = client.request();
+    req.on('error', common.expectsError({
+      code: 'ERR_HTTP2_STREAM_CANCEL',
+      type: Error,
+      message: 'The pending stream has been canceled'
+    }));
+
+    client.destroy();
+
+    req.on('response', common.mustNotCall());
+
+    const sessionError = {
+      type: Error,
+      code: 'ERR_HTTP2_INVALID_SESSION',
+      message: 'The session has been destroyed'
+    };
+
+    common.expectsError(() => client.request(), sessionError);
+    common.expectsError(() => client.settings({}), sessionError);
+    client.close();  // should be a non-op at this point
+
+    // Wait for setImmediate call from destroy() to complete
+    // so that state.destroyed is set to true
+    setImmediate(() => {
       common.expectsError(() => client.request(), sessionError);
       common.expectsError(() => client.settings({}), sessionError);
-      common.expectsError(() => client.shutdown(), sessionError);
-
-      // Wait for setImmediate call from destroy() to complete
-      // so that state.destroyed is set to true
-      setImmediate(() => {
-        common.expectsError(() => client.request(), sessionError);
-        common.expectsError(() => client.settings({}), sessionError);
-        common.expectsError(() => client.shutdown(), sessionError);
-      });
-
-      req.on(
-        'end',
-        common.mustCall(() => {
-          server.close();
-        })
-      );
-      req.end();
-    })
-  );
+      client.close();  // should be a non-op at this point
+    });
+
+    req.resume();
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => server.close()));
+  }));
 }
 
 // test destroy before goaway
 {
   const server = h2.createServer();
-  server.on(
-    'stream',
-    common.mustCall((stream) => {
-      stream.on('error', common.mustCall());
-      stream.session.shutdown();
-    })
-  );
-  server.listen(
-    0,
-    common.mustCall(() => {
-      const client = h2.connect(`http://localhost:${server.address().port}`);
+  server.on('stream', common.mustCall((stream) => {
+    stream.session.destroy();
+  }));
+
+  server.listen(0, common.mustCall(() => {
+    const client = h2.connect(`http://localhost:${server.address().port}`);
+    // On some platforms (e.g. windows), an ECONNRESET may occur at this
+    // point -- or it may not. Do not make this a mustCall
+    client.on('error', () => {});
+
+    client.on('close', () => {
+      server.close();
+      // calling destroy in here should not matter
+      client.destroy();
+    });
 
-      client.on(
-        'goaway',
-        common.mustCall(() => {
-          // We ought to be able to destroy the client in here without an error
-          server.close();
-          client.destroy();
-        })
-      );
-
-      client.request();
-    })
-  );
+    const req = client.request();
+    // On some platforms (e.g. windows), an ECONNRESET may occur at this
+    // point -- or it may not. Do not make this a mustCall
+    req.on('error', () => {});
+  }));
 }
diff --git a/test/parallel/test-http2-client-onconnect-errors.js b/test/parallel/test-http2-client-onconnect-errors.js
index 08007753654878..44fe6875602187 100644
--- a/test/parallel/test-http2-client-onconnect-errors.js
+++ b/test/parallel/test-http2-client-onconnect-errors.js
@@ -1,13 +1,14 @@
 'use strict';
 
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
 const {
   constants,
   Http2Session,
   nghttp2ErrorString
 } = process.binding('http2');
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
 const http2 = require('http2');
 
 // tests error handling within requestOnConnect
@@ -69,6 +70,8 @@ server.listen(0, common.mustCall(() => runTest(tests.shift())));
 
 function runTest(test) {
   const client = http2.connect(`http://localhost:${server.address().port}`);
+  client.on('close', common.mustCall());
+
   const req = client.request({ ':method': 'POST' });
 
   currentError = test.ngError;
@@ -83,15 +86,15 @@ function runTest(test) {
   if (test.type === 'stream') {
     client.on('error', errorMustNotCall);
     req.on('error', errorMustCall);
-    req.on('error', common.mustCall(() => {
-      client.destroy();
-    }));
   } else {
     client.on('error', errorMustCall);
-    req.on('error', errorMustNotCall);
+    req.on('error', common.expectsError({
+      code: 'ERR_HTTP2_STREAM_CANCEL'
+    }));
   }
 
-  req.on('end', common.mustCall(() => {
+  req.on('end', common.mustCall());
+  req.on('close', common.mustCall(() => {
     client.destroy();
 
     if (!tests.length) {
diff --git a/test/parallel/test-http2-client-port-80.js b/test/parallel/test-http2-client-port-80.js
index a9d19eb5b9f008..fc82e231f6af8f 100644
--- a/test/parallel/test-http2-client-port-80.js
+++ b/test/parallel/test-http2-client-port-80.js
@@ -16,4 +16,10 @@ net.connect = common.mustCall((...args) => {
 });
 
 const client = http2.connect('http://localhost:80');
-client.destroy();
+
+// A socket error may or may not occur depending on whether there is something
+// currently listening on port 80. Keep this as a non-op and not a mustCall or
+// mustNotCall.
+client.on('error', () => {});
+
+client.close();
diff --git a/test/parallel/test-http2-client-priority-before-connect.js b/test/parallel/test-http2-client-priority-before-connect.js
index b062107e4ab7f7..a9615d2cd69f12 100644
--- a/test/parallel/test-http2-client-priority-before-connect.js
+++ b/test/parallel/test-http2-client-priority-before-connect.js
@@ -8,31 +8,21 @@ const h2 = require('http2');
 const server = h2.createServer();
 
 // we use the lower-level API here
-server.on('stream', common.mustCall(onStream));
-
-function onStream(stream, headers, flags) {
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  });
-  stream.end('hello world');
-}
-
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
+server.on('stream', common.mustCall((stream) => {
+  stream.respond();
+  stream.end('ok');
+}));
 
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
-
-  const req = client.request({ ':path': '/' });
+  const req = client.request();
   req.priority({});
 
   req.on('response', common.mustCall());
   req.resume();
-  req.on('end', common.mustCall(() => {
+  req.on('end', common.mustCall());
+  req.on('close', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
-  req.end();
-
 }));
diff --git a/test/parallel/test-http2-client-promisify-connect.js b/test/parallel/test-http2-client-promisify-connect.js
index b66827c1507302..2eb7da3b9cfd85 100644
--- a/test/parallel/test-http2-client-promisify-connect.js
+++ b/test/parallel/test-http2-client-promisify-connect.js
@@ -15,7 +15,6 @@ server.on('stream', common.mustCall((stream) => {
   stream.end('ok');
 }));
 server.listen(0, common.mustCall(() => {
-
   const connect = util.promisify(http2.connect);
 
   connect(`http://localhost:${server.address().port}`)
@@ -28,7 +27,7 @@ server.listen(0, common.mustCall(() => {
       req.on('data', (chunk) => data += chunk);
       req.on('end', common.mustCall(() => {
         assert.strictEqual(data, 'ok');
-        client.destroy();
+        client.close();
         server.close();
       }));
     }));
diff --git a/test/parallel/test-http2-client-request-options-errors.js b/test/parallel/test-http2-client-request-options-errors.js
index 5d3fc0ab5a1fd8..3ad808cb1fbe23 100644
--- a/test/parallel/test-http2-client-request-options-errors.js
+++ b/test/parallel/test-http2-client-request-options-errors.js
@@ -3,7 +3,6 @@
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
-const assert = require('assert');
 const http2 = require('http2');
 
 // Check if correct errors are emitted when wrong type of data is passed
@@ -34,29 +33,27 @@ server.listen(0, common.mustCall(() => {
   const port = server.address().port;
   const client = http2.connect(`http://localhost:${port}`);
 
-  Object.keys(optionsToTest).forEach((option) => {
-    Object.keys(types).forEach((type) => {
-      if (type === optionsToTest[option]) {
-        return;
-      }
-
-      assert.throws(
-        () => client.request({
-          ':method': 'CONNECT',
-          ':authority': `localhost:${port}`
-        }, {
-          [option]: types[type]
-        }),
-        common.expectsError({
-          type: TypeError,
-          code: 'ERR_INVALID_OPT_VALUE',
-          message: `The value "${String(types[type])}" is invalid ` +
-                   `for option "${option}"`
-        })
-      );
+  client.on('connect', () => {
+    Object.keys(optionsToTest).forEach((option) => {
+      Object.keys(types).forEach((type) => {
+        if (type === optionsToTest[option])
+          return;
+
+        common.expectsError(
+          () => client.request({
+            ':method': 'CONNECT',
+            ':authority': `localhost:${port}`
+          }, {
+            [option]: types[type]
+          }), {
+            type: TypeError,
+            code: 'ERR_INVALID_OPT_VALUE',
+            message: `The value "${String(types[type])}" is invalid ` +
+                    `for option "${option}"`
+          });
+      });
     });
+    server.close();
+    client.close();
   });
-
-  server.close();
-  client.destroy();
 }));
diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js
index eb3a0087d7893c..b0faaa5de2a398 100644
--- a/test/parallel/test-http2-client-rststream-before-connect.js
+++ b/test/parallel/test-http2-client-rststream-before-connect.js
@@ -8,33 +8,37 @@ const h2 = require('http2');
 
 const server = h2.createServer();
 server.on('stream', (stream) => {
+  stream.on('close', common.mustCall());
   stream.respond();
   stream.end('ok');
 });
 
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
-
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
-
-  const req = client.request({ ':path': '/' });
-  req.rstStream(0);
+  const req = client.request();
+  req.close(1);
+  assert.strictEqual(req.closed, true);
 
   // make sure that destroy is called
   req._destroy = common.mustCall(req._destroy.bind(req));
 
   // second call doesn't do anything
-  assert.doesNotThrow(() => req.rstStream(8));
+  assert.doesNotThrow(() => req.close(8));
 
   req.on('close', common.mustCall((code) => {
     assert.strictEqual(req.destroyed, true);
-    assert.strictEqual(code, 0);
+    assert.strictEqual(code, 1);
     server.close();
-    client.destroy();
+    client.close();
+  }));
+
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 1'
   }));
 
-  req.on('response', common.mustNotCall());
+  req.on('response', common.mustCall());
   req.resume();
   req.on('end', common.mustCall());
   req.end();
diff --git a/test/parallel/test-http2-client-set-priority.js b/test/parallel/test-http2-client-set-priority.js
index f3e1d7afa50d7e..64b7b56dfa2543 100644
--- a/test/parallel/test-http2-client-set-priority.js
+++ b/test/parallel/test-http2-client-set-priority.js
@@ -10,26 +10,20 @@ const checkWeight = (actual, expect) => {
   const server = http2.createServer();
   server.on('stream', common.mustCall((stream, headers, flags) => {
     assert.strictEqual(stream.state.weight, expect);
-    stream.respond({
-      'content-type': 'text/html',
-      ':status': 200
-    });
+    stream.respond();
     stream.end('test');
   }));
 
   server.listen(0, common.mustCall(() => {
-    const port = server.address().port;
-    const client = http2.connect(`http://localhost:${port}`);
+    const client = http2.connect(`http://localhost:${server.address().port}`);
+    const req = client.request({}, { weight: actual });
 
-    const headers = { ':path': '/' };
-    const req = client.request(headers, { weight: actual });
-
-    req.on('data', common.mustCall(() => {}));
-    req.on('end', common.mustCall(() => {
+    req.on('data', common.mustCall());
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => {
       server.close();
-      client.destroy();
+      client.close();
     }));
-    req.end();
   }));
 };
 
diff --git a/test/parallel/test-http2-client-settings-before-connect.js b/test/parallel/test-http2-client-settings-before-connect.js
index 27caa9e601897b..4642bf5220f554 100644
--- a/test/parallel/test-http2-client-settings-before-connect.js
+++ b/test/parallel/test-http2-client-settings-before-connect.js
@@ -3,62 +3,53 @@
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
-const assert = require('assert');
 const h2 = require('http2');
 
 const server = h2.createServer();
 
 // we use the lower-level API here
-server.on('stream', common.mustCall(onStream));
-
-function onStream(stream, headers, flags) {
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  });
-  stream.end('hello world');
-}
-
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
+server.on('stream', common.mustCall((stream, headers, flags) => {
+  stream.respond();
+  stream.end('ok');
+}));
+server.on('session', common.mustCall((session) => {
+  session.on('remoteSettings', common.mustCall(2));
+}));
 
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
 
-  assert.throws(() => client.settings({ headerTableSize: -1 }),
-                RangeError);
-  assert.throws(() => client.settings({ headerTableSize: 2 ** 32 }),
-                RangeError);
-  assert.throws(() => client.settings({ initialWindowSize: -1 }),
-                RangeError);
-  assert.throws(() => client.settings({ initialWindowSize: 2 ** 32 }),
-                RangeError);
-  assert.throws(() => client.settings({ maxFrameSize: 1 }),
-                RangeError);
-  assert.throws(() => client.settings({ maxFrameSize: 2 ** 24 }),
-                RangeError);
-  assert.throws(() => client.settings({ maxConcurrentStreams: -1 }),
-                RangeError);
-  assert.throws(() => client.settings({ maxConcurrentStreams: 2 ** 31 }),
-                RangeError);
-  assert.throws(() => client.settings({ maxHeaderListSize: -1 }),
-                RangeError);
-  assert.throws(() => client.settings({ maxHeaderListSize: 2 ** 32 }),
-                RangeError);
-  ['a', 1, 0, null, {}].forEach((i) => {
-    assert.throws(() => client.settings({ enablePush: i }), TypeError);
+  [
+    ['headerTableSize', -1, RangeError],
+    ['headerTableSize', 2 ** 32, RangeError],
+    ['initialWindowSize', -1, RangeError],
+    ['initialWindowSize', 2 ** 32, RangeError],
+    ['maxFrameSize', 1, RangeError],
+    ['maxFrameSize', 2 ** 24, RangeError],
+    ['maxConcurrentStreams', -1, RangeError],
+    ['maxConcurrentStreams', 2 ** 31, RangeError],
+    ['maxHeaderListSize', -1, RangeError],
+    ['maxHeaderListSize', 2 ** 32, RangeError],
+    ['enablePush', 'a', TypeError],
+    ['enablePush', 1, TypeError],
+    ['enablePush', 0, TypeError],
+    ['enablePush', null, TypeError],
+    ['enablePush', {}, TypeError]
+  ].forEach((i) => {
+    common.expectsError(
+      () => client.settings({ [i[0]]: i[1] }),
+      {
+        code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+        type: i[2] });
   });
 
   client.settings({ maxFrameSize: 1234567 });
 
-  const req = client.request({ ':path': '/' });
-
+  const req = client.request();
   req.on('response', common.mustCall());
   req.resume();
-  req.on('end', common.mustCall(() => {
+  req.on('close', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
-  req.end();
-
 }));
diff --git a/test/parallel/test-http2-client-shutdown-before-connect.js b/test/parallel/test-http2-client-shutdown-before-connect.js
index 4fed0ee3ad0703..bd971ebf7de69c 100644
--- a/test/parallel/test-http2-client-shutdown-before-connect.js
+++ b/test/parallel/test-http2-client-shutdown-before-connect.js
@@ -10,15 +10,7 @@ const server = h2.createServer();
 // we use the lower-level API here
 server.on('stream', common.mustNotCall());
 
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
-
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
-
-  client.shutdown({ graceful: true }, common.mustCall(() => {
-    server.close();
-    client.destroy();
-  }));
-
+  client.close(common.mustCall(() => server.close()));
 }));
diff --git a/test/parallel/test-http2-client-socket-destroy.js b/test/parallel/test-http2-client-socket-destroy.js
index faf4643b0304e3..3eb7e898edcfc1 100644
--- a/test/parallel/test-http2-client-socket-destroy.js
+++ b/test/parallel/test-http2-client-socket-destroy.js
@@ -14,38 +14,27 @@ const body =
 const server = h2.createServer();
 
 // we use the lower-level API here
-server.on('stream', common.mustCall(onStream));
-
-function onStream(stream) {
-  // The stream aborted event must have been triggered
+server.on('stream', common.mustCall((stream) => {
   stream.on('aborted', common.mustCall());
-
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  });
+  stream.on('close', common.mustCall());
+  stream.respond();
   stream.write(body);
-}
-
-server.listen(0);
+  // purposefully do not end()
+}));
 
-server.on('listening', common.mustCall(function() {
+server.listen(0, common.mustCall(function() {
   const client = h2.connect(`http://localhost:${this.address().port}`);
-
-  const req = client.request({ ':path': '/' });
+  const req = client.request();
 
   req.on('response', common.mustCall(() => {
     // send a premature socket close
     client[kSocket].destroy();
   }));
-  req.on('data', common.mustNotCall());
 
-  req.on('end', common.mustCall(() => {
-    server.close();
-  }));
+  req.resume();
+  req.on('end', common.mustCall());
+  req.on('close', common.mustCall(() => server.close()));
 
   // On the client, the close event must call
   client.on('close', common.mustCall());
-  req.end();
-
 }));
diff --git a/test/parallel/test-http2-client-stream-destroy-before-connect.js b/test/parallel/test-http2-client-stream-destroy-before-connect.js
index 06afbf3ce8ceb2..a2412b9f1d646a 100644
--- a/test/parallel/test-http2-client-stream-destroy-before-connect.js
+++ b/test/parallel/test-http2-client-stream-destroy-before-connect.js
@@ -20,36 +20,29 @@ server.on('stream', (stream) => {
     assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR');
     assert.strictEqual(err.message, 'Stream closed with error code 2');
   });
-  stream.respond({});
+  stream.respond();
   stream.end();
 });
 
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
-
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
 
-  const req = client.request({ ':path': '/' });
-  const err = new Error('test');
-  req.destroy(err);
+  const req = client.request();
+  req.destroy(new Error('test'));
 
-  req.on('error', common.mustCall((err) => {
-    common.expectsError({
-      type: Error,
-      message: 'test'
-    })(err);
+  req.on('error', common.expectsError({
+    type: Error,
+    message: 'test'
   }));
 
   req.on('close', common.mustCall((code) => {
     assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR);
     assert.strictEqual(code, NGHTTP2_INTERNAL_ERROR);
     server.close();
-    client.destroy();
+    client.close();
   }));
 
   req.on('response', common.mustNotCall());
   req.resume();
   req.on('end', common.mustCall());
-
 }));
diff --git a/test/parallel/test-http2-client-unescaped-path.js b/test/parallel/test-http2-client-unescaped-path.js
index adfbd61fe762b4..190f8ce75e8917 100644
--- a/test/parallel/test-http2-client-unescaped-path.js
+++ b/test/parallel/test-http2-client-unescaped-path.js
@@ -4,6 +4,7 @@ const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
 const http2 = require('http2');
+const Countdown = require('../common/countdown');
 
 const server = http2.createServer();
 
@@ -13,14 +14,12 @@ const count = 32;
 
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
+  client.setMaxListeners(33);
 
-  let remaining = count + 1;
-  function maybeClose() {
-    if (--remaining === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
+  const countdown = new Countdown(count + 1, () => {
+    server.close();
+    client.close();
+  });
 
   // nghttp2 will catch the bad header value for us.
   function doTest(i) {
@@ -30,7 +29,7 @@ server.listen(0, common.mustCall(() => {
       type: Error,
       message: 'Stream closed with error code 1'
     }));
-    req.on('close', common.mustCall(maybeClose));
+    req.on('close', common.mustCall(() => countdown.dec()));
   }
 
   for (let i = 0; i <= count; i += 1)
diff --git a/test/parallel/test-http2-client-upload.js b/test/parallel/test-http2-client-upload.js
index 8fb5f369ca4cb7..70a8ff3ced01c6 100644
--- a/test/parallel/test-http2-client-upload.js
+++ b/test/parallel/test-http2-client-upload.js
@@ -9,6 +9,7 @@ const assert = require('assert');
 const http2 = require('http2');
 const fs = require('fs');
 const fixtures = require('../common/fixtures');
+const Countdown = require('../common/countdown');
 
 const loc = fixtures.path('person.jpg');
 let fileData;
@@ -34,20 +35,21 @@ fs.readFile(loc, common.mustCall((err, data) => {
   server.listen(0, common.mustCall(() => {
     const client = http2.connect(`http://localhost:${server.address().port}`);
 
-    let remaining = 2;
-    function maybeClose() {
-      if (--remaining === 0) {
-        server.close();
-        client.shutdown();
-      }
-    }
+    const countdown = new Countdown(2, () => {
+      server.close();
+      client.close();
+    });
 
     const req = client.request({ ':method': 'POST' });
     req.on('response', common.mustCall());
+
     req.resume();
-    req.on('end', common.mustCall(maybeClose));
+    req.on('end', common.mustCall());
+
+    req.on('finish', () => countdown.dec());
     const str = fs.createReadStream(loc);
-    req.on('finish', common.mustCall(maybeClose));
+    str.on('end', common.mustCall());
+    str.on('close', () => countdown.dec());
     str.pipe(req);
   }));
 }));
diff --git a/test/parallel/test-http2-client-write-before-connect.js b/test/parallel/test-http2-client-write-before-connect.js
index 26674dcad369e3..6588d7dccd139d 100644
--- a/test/parallel/test-http2-client-write-before-connect.js
+++ b/test/parallel/test-http2-client-write-before-connect.js
@@ -8,47 +8,30 @@ const h2 = require('http2');
 
 const server = h2.createServer();
 
-const {
-  HTTP2_HEADER_PATH,
-  HTTP2_HEADER_METHOD,
-  HTTP2_METHOD_POST
-} = h2.constants;
-
 // we use the lower-level API here
-server.on('stream', common.mustCall(onStream));
-
-function onStream(stream, headers, flags) {
+server.on('stream', common.mustCall((stream, headers, flags) => {
   let data = '';
   stream.setEncoding('utf8');
   stream.on('data', (chunk) => data += chunk);
   stream.on('end', common.mustCall(() => {
     assert.strictEqual(data, 'some data more data');
   }));
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  });
-  stream.end('hello world');
-}
-
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
+  stream.respond();
+  stream.end('ok');
+}));
 
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
 
-  const req = client.request({
-    [HTTP2_HEADER_PATH]: '/',
-    [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST });
+  const req = client.request({ ':method': 'POST' });
   req.write('some data ');
-  req.write('more data');
+  req.end('more data');
 
   req.on('response', common.mustCall());
   req.resume();
-  req.on('end', common.mustCall(() => {
+  req.on('end', common.mustCall());
+  req.on('close', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
-  req.end();
-
 }));
diff --git a/test/parallel/test-http2-compat-errors.js b/test/parallel/test-http2-compat-errors.js
index 5774d1a922bd52..c84318bad68e35 100644
--- a/test/parallel/test-http2-compat-errors.js
+++ b/test/parallel/test-http2-compat-errors.js
@@ -4,9 +4,7 @@
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
-const assert = require('assert');
 const h2 = require('http2');
-const { Http2Stream } = require('internal/http2/core');
 
 // Errors should not be reported both in Http2ServerRequest
 // and Http2ServerResponse
@@ -14,6 +12,7 @@ const { Http2Stream } = require('internal/http2/core');
 let expected = null;
 
 const server = h2.createServer(common.mustCall(function(req, res) {
+  res.stream.on('error', common.mustCall());
   req.on('error', common.mustNotCall());
   res.on('error', common.mustNotCall());
   req.on('aborted', common.mustCall());
@@ -26,27 +25,12 @@ const server = h2.createServer(common.mustCall(function(req, res) {
   server.close();
 }));
 
-server.on('streamError', common.mustCall(function(err, stream) {
-  assert.strictEqual(err, expected);
-  assert.strictEqual(stream instanceof Http2Stream, true);
-}));
-
 server.listen(0, common.mustCall(function() {
-  const port = server.address().port;
-
-  const url = `http://localhost:${port}`;
-  const client = h2.connect(url, common.mustCall(function() {
-    const headers = {
-      ':path': '/foobar',
-      ':method': 'GET',
-      ':scheme': 'http',
-      ':authority': `localhost:${port}`,
-    };
-    const request = client.request(headers);
-    request.on('data', common.mustCall(function(chunk) {
-      // cause an error on the server side
+  const url = `http://localhost:${server.address().port}`;
+  const client = h2.connect(url, common.mustCall(() => {
+    const request = client.request();
+    request.on('data', common.mustCall((chunk) => {
       client.destroy();
     }));
-    request.end();
   }));
 }));
diff --git a/test/parallel/test-http2-compat-expect-continue-check.js b/test/parallel/test-http2-compat-expect-continue-check.js
index 800df1c432944f..6aded8b52935c1 100644
--- a/test/parallel/test-http2-compat-expect-continue-check.js
+++ b/test/parallel/test-http2-compat-expect-continue-check.js
@@ -12,74 +12,47 @@ const testResBody = 'other stuff!\n';
 // through server receiving it, triggering 'checkContinue' custom handler,
 // writing the rest of the request to finally the client receiving to.
 
-function handler(req, res) {
-  console.error('Server sent full response');
+const server = http2.createServer(
+  common.mustNotCall('Full request received before 100 Continue')
+);
 
-  res.writeHead(200, {
-    'content-type': 'text/plain',
-    'abcd': '1'
-  });
+server.on('checkContinue', common.mustCall((req, res) => {
+  res.writeContinue();
+  res.writeHead(200, {});
   res.end(testResBody);
   // should simply return false if already too late to write
   assert.strictEqual(res.writeContinue(), false);
   res.on('finish', common.mustCall(
     () => process.nextTick(() => assert.strictEqual(res.writeContinue(), false))
   ));
-}
-
-const server = http2.createServer(
-  common.mustNotCall('Full request received before 100 Continue')
-);
-
-server.on('checkContinue', common.mustCall((req, res) => {
-  console.error('Server received Expect: 100-continue');
-
-  res.writeContinue();
-
-  // timeout so that we allow the client to receive continue first
-  setTimeout(
-    common.mustCall(() => handler(req, res)),
-    common.platformTimeout(100)
-  );
 }));
 
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
+server.listen(0, common.mustCall(() => {
   let body = '';
 
-  const port = server.address().port;
-  const client = http2.connect(`http://localhost:${port}`);
+  const client = http2.connect(`http://localhost:${server.address().port}`);
   const req = client.request({
     ':method': 'POST',
-    ':path': '/world',
     expect: '100-continue'
   });
-  console.error('Client sent request');
 
   let gotContinue = false;
   req.on('continue', common.mustCall(() => {
-    console.error('Client received 100-continue');
     gotContinue = true;
   }));
 
   req.on('response', common.mustCall((headers) => {
-    console.error('Client received response headers');
-
     assert.strictEqual(gotContinue, true);
     assert.strictEqual(headers[':status'], 200);
-    assert.strictEqual(headers['abcd'], '1');
+    req.end();
   }));
 
   req.setEncoding('utf-8');
   req.on('data', common.mustCall((chunk) => { body += chunk; }));
 
   req.on('end', common.mustCall(() => {
-    console.error('Client received full response');
-
     assert.strictEqual(body, testResBody);
-
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/parallel/test-http2-compat-expect-continue.js b/test/parallel/test-http2-compat-expect-continue.js
index 6f08e813ef385a..42fa80ae4e8620 100644
--- a/test/parallel/test-http2-compat-expect-continue.js
+++ b/test/parallel/test-http2-compat-expect-continue.js
@@ -17,8 +17,6 @@ const server = http2.createServer();
 let sentResponse = false;
 
 server.on('request', common.mustCall((req, res) => {
-  console.error('Server sent full response');
-
   res.end(testResBody);
   sentResponse = true;
 }));
@@ -28,38 +26,29 @@ server.listen(0);
 server.on('listening', common.mustCall(() => {
   let body = '';
 
-  const port = server.address().port;
-  const client = http2.connect(`http://localhost:${port}`);
+  const client = http2.connect(`http://localhost:${server.address().port}`);
   const req = client.request({
     ':method': 'POST',
-    ':path': '/world',
     expect: '100-continue'
   });
-  console.error('Client sent request');
 
   let gotContinue = false;
   req.on('continue', common.mustCall(() => {
-    console.error('Client received 100-continue');
     gotContinue = true;
   }));
 
   req.on('response', common.mustCall((headers) => {
-    console.error('Client received response headers');
-
     assert.strictEqual(gotContinue, true);
     assert.strictEqual(sentResponse, true);
     assert.strictEqual(headers[':status'], 200);
+    req.end();
   }));
 
   req.setEncoding('utf8');
   req.on('data', common.mustCall((chunk) => { body += chunk; }));
-
   req.on('end', common.mustCall(() => {
-    console.error('Client received full response');
-
     assert.strictEqual(body, testResBody);
-
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/parallel/test-http2-compat-expect-handling.js b/test/parallel/test-http2-compat-expect-handling.js
index 0a5de368c6cfff..f36032c972fc45 100644
--- a/test/parallel/test-http2-compat-expect-handling.js
+++ b/test/parallel/test-http2-compat-expect-handling.js
@@ -39,7 +39,7 @@ function nextTest(testsToRun) {
   }));
 
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     nextTest(testsToRun - 1);
   }));
 }
diff --git a/test/parallel/test-http2-compat-method-connect.js b/test/parallel/test-http2-compat-method-connect.js
index 1f43b3891b24ed..21ad23e92ba65b 100644
--- a/test/parallel/test-http2-compat-method-connect.js
+++ b/test/parallel/test-http2-compat-method-connect.js
@@ -33,7 +33,7 @@ function testMethodConnect(testsToRun) {
   }));
   req.resume();
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     testMethodConnect(testsToRun - 1);
   }));
   req.end();
diff --git a/test/parallel/test-http2-compat-serverrequest-end.js b/test/parallel/test-http2-compat-serverrequest-end.js
index b6bfd04089a103..d34372118582db 100644
--- a/test/parallel/test-http2-compat-serverrequest-end.js
+++ b/test/parallel/test-http2-compat-serverrequest-end.js
@@ -31,18 +31,11 @@ server.listen(0, common.mustCall(function() {
   }));
 
   const url = `http://localhost:${port}`;
-  const client = h2.connect(url, common.mustCall(function() {
-    const headers = {
-      ':path': '/foobar',
-      ':method': 'GET',
-      ':scheme': 'http',
-      ':authority': `localhost:${port}`
-    };
-    const request = client.request(headers);
+  const client = h2.connect(url, common.mustCall(() => {
+    const request = client.request();
     request.resume();
-    request.on('end', common.mustCall(function() {
-      client.destroy();
+    request.on('end', common.mustCall(() => {
+      client.close();
     }));
-    request.end();
   }));
 }));
diff --git a/test/parallel/test-http2-compat-serverrequest-headers.js b/test/parallel/test-http2-compat-serverrequest-headers.js
index 58cc52c64f6c91..5843104c019189 100644
--- a/test/parallel/test-http2-compat-serverrequest-headers.js
+++ b/test/parallel/test-http2-compat-serverrequest-headers.js
@@ -79,7 +79,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverrequest-pause.js b/test/parallel/test-http2-compat-serverrequest-pause.js
index f8494bb0ddee39..62a23997c75bd8 100644
--- a/test/parallel/test-http2-compat-serverrequest-pause.js
+++ b/test/parallel/test-http2-compat-serverrequest-pause.js
@@ -46,7 +46,7 @@ server.listen(0, common.mustCall(() => {
   request.resume();
   request.end(testStr);
   request.on('end', common.mustCall(function() {
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/parallel/test-http2-compat-serverrequest-pipe.js b/test/parallel/test-http2-compat-serverrequest-pipe.js
index 787078888c709e..53e54cdf913b0e 100644
--- a/test/parallel/test-http2-compat-serverrequest-pipe.js
+++ b/test/parallel/test-http2-compat-serverrequest-pipe.js
@@ -36,7 +36,7 @@ server.listen(0, common.mustCall(() => {
   function maybeClose() {
     if (--remaining === 0) {
       server.close();
-      client.destroy();
+      client.close();
     }
   }
 
diff --git a/test/parallel/test-http2-compat-serverrequest-settimeout.js b/test/parallel/test-http2-compat-serverrequest-settimeout.js
index 460eb576bfd4f6..f7189161802301 100644
--- a/test/parallel/test-http2-compat-serverrequest-settimeout.js
+++ b/test/parallel/test-http2-compat-serverrequest-settimeout.js
@@ -12,7 +12,6 @@ const server = http2.createServer();
 server.on('request', (req, res) => {
   req.setTimeout(msecs, common.mustCall(() => {
     res.end();
-    req.setTimeout(msecs, common.mustNotCall());
   }));
   res.on('finish', common.mustCall(() => {
     req.setTimeout(msecs, common.mustNotCall());
@@ -35,7 +34,7 @@ server.listen(0, common.mustCall(() => {
     ':authority': `localhost:${port}`
   });
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
   }));
   req.resume();
   req.end();
diff --git a/test/parallel/test-http2-compat-serverrequest-trailers.js b/test/parallel/test-http2-compat-serverrequest-trailers.js
index b4d90281918d9e..285178cab66816 100644
--- a/test/parallel/test-http2-compat-serverrequest-trailers.js
+++ b/test/parallel/test-http2-compat-serverrequest-trailers.js
@@ -62,7 +62,7 @@ server.listen(0, common.mustCall(function() {
     request.resume();
     request.on('end', common.mustCall(function() {
       server.close();
-      client.destroy();
+      client.close();
     }));
     request.write('test\n');
     request.end('test');
diff --git a/test/parallel/test-http2-compat-serverrequest.js b/test/parallel/test-http2-compat-serverrequest.js
index edcd7a8f8cdea4..d92da61d943cb7 100644
--- a/test/parallel/test-http2-compat-serverrequest.js
+++ b/test/parallel/test-http2-compat-serverrequest.js
@@ -46,7 +46,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-close.js b/test/parallel/test-http2-compat-serverresponse-close.js
index 35e39b9670868e..0ff6bd3a83f600 100644
--- a/test/parallel/test-http2-compat-serverresponse-close.js
+++ b/test/parallel/test-http2-compat-serverresponse-close.js
@@ -16,26 +16,17 @@ const server = h2.createServer(common.mustCall((req, res) => {
 
   req.on('close', common.mustCall());
   res.on('close', common.mustCall());
+  req.on('error', common.mustNotCall());
 }));
 server.listen(0);
 
-server.on('listening', function() {
-  const port = server.address().port;
-
-  const url = `http://localhost:${port}`;
-  const client = h2.connect(url, common.mustCall(function() {
-    const headers = {
-      ':path': '/foobar',
-      ':method': 'GET',
-      ':scheme': 'http',
-      ':authority': `localhost:${port}`,
-    };
-    const request = client.request(headers);
+server.on('listening', () => {
+  const url = `http://localhost:${server.address().port}`;
+  const client = h2.connect(url, common.mustCall(() => {
+    const request = client.request();
     request.on('data', common.mustCall(function(chunk) {
-      // cause an error on the server side
       client.destroy();
       server.close();
     }));
-    request.end();
   }));
 });
diff --git a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js
index da7c4adbbfff16..18b3ba15be841c 100755
--- a/test/parallel/test-http2-compat-serverresponse-createpushresponse.js
+++ b/test/parallel/test-http2-compat-serverresponse-createpushresponse.js
@@ -43,7 +43,7 @@ const server = h2.createServer((request, response) => {
         ':path': '/pushed',
         ':method': 'GET'
       }, common.mustCall((error) => {
-        assert.strictEqual(error.code, 'ERR_HTTP2_STREAM_CLOSED');
+        assert.strictEqual(error.code, 'ERR_HTTP2_INVALID_STREAM');
       }));
     });
   }));
@@ -61,7 +61,7 @@ server.listen(0, common.mustCall(() => {
     let remaining = 2;
     function maybeClose() {
       if (--remaining === 0) {
-        client.destroy();
+        client.close();
         server.close();
       }
     }
diff --git a/test/parallel/test-http2-compat-serverresponse-destroy.js b/test/parallel/test-http2-compat-serverresponse-destroy.js
index 77e761b6227702..54214737840061 100644
--- a/test/parallel/test-http2-compat-serverresponse-destroy.js
+++ b/test/parallel/test-http2-compat-serverresponse-destroy.js
@@ -5,6 +5,7 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const http2 = require('http2');
+const Countdown = require('../common/countdown');
 
 // Check that destroying the Http2ServerResponse stream produces
 // the expected result, including the ability to throw an error
@@ -30,63 +31,54 @@ const server = http2.createServer(common.mustCall((req, res) => {
   if (req.url !== '/') {
     nextError = errors.shift();
   }
+
   res.destroy(nextError);
 }, 3));
 
-server.on(
-  'streamError',
-  common.mustCall((err) => assert.strictEqual(err, nextError), 2)
-);
-
 server.listen(0, common.mustCall(() => {
-  const port = server.address().port;
-  const client = http2.connect(`http://localhost:${port}`);
-  const req = client.request({
-    ':path': '/',
-    ':method': 'GET',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  });
+  const client = http2.connect(`http://localhost:${server.address().port}`);
 
-  req.on('response', common.mustNotCall());
-  req.on('error', common.mustNotCall());
-  req.on('end', common.mustCall());
+  const countdown = new Countdown(3, () => {
+    server.close();
+    client.close();
+  });
 
-  req.resume();
-  req.end();
+  {
+    const req = client.request();
+    req.on('response', common.mustNotCall());
+    req.on('error', common.mustNotCall());
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => countdown.dec()));
+    req.resume();
+  }
 
-  const req2 = client.request({
-    ':path': '/error',
-    ':method': 'GET',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  });
+  {
+    const req = client.request({ ':path': '/error' });
 
-  req2.on('response', common.mustNotCall());
-  req2.on('error', common.mustNotCall());
-  req2.on('end', common.mustCall());
+    req.on('response', common.mustNotCall());
+    req.on('error', common.expectsError({
+      code: 'ERR_HTTP2_STREAM_ERROR',
+      type: Error,
+      message: 'Stream closed with error code 2'
+    }));
+    req.on('close', common.mustCall(() => countdown.dec()));
 
-  req2.resume();
-  req2.end();
+    req.resume();
+    req.on('end', common.mustCall());
+  }
 
-  const req3 = client.request({
-    ':path': '/error',
-    ':method': 'GET',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  });
+  {
+    const req = client.request({ ':path': '/error' });
 
-  req3.on('response', common.mustNotCall());
-  req3.on('error', common.expectsError({
-    code: 'ERR_HTTP2_STREAM_ERROR',
-    type: Error,
-    message: 'Stream closed with error code 2'
-  }));
-  req3.on('end', common.mustCall(() => {
-    server.close();
-    client.destroy();
-  }));
+    req.on('response', common.mustNotCall());
+    req.on('error', common.expectsError({
+      code: 'ERR_HTTP2_STREAM_ERROR',
+      type: Error,
+      message: 'Stream closed with error code 2'
+    }));
+    req.on('close', common.mustCall(() => countdown.dec()));
 
-  req3.resume();
-  req3.end();
+    req.resume();
+    req.on('end', common.mustCall());
+  }
 }));
diff --git a/test/parallel/test-http2-compat-serverresponse-drain.js b/test/parallel/test-http2-compat-serverresponse-drain.js
index e2465cfa00d1f6..7ccbb1f4d21209 100644
--- a/test/parallel/test-http2-compat-serverresponse-drain.js
+++ b/test/parallel/test-http2-compat-serverresponse-drain.js
@@ -37,7 +37,7 @@ server.listen(0, common.mustCall(() => {
 
   request.on('end', common.mustCall(function() {
     assert.strictEqual(data, testString.repeat(2));
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/parallel/test-http2-compat-serverresponse-end.js b/test/parallel/test-http2-compat-serverresponse-end.js
index 366d52321554fe..0e846a5948e3cc 100644
--- a/test/parallel/test-http2-compat-serverresponse-end.js
+++ b/test/parallel/test-http2-compat-serverresponse-end.js
@@ -52,7 +52,7 @@ const {
       request.on('data', (chunk) => (data += chunk));
       request.on('end', mustCall(() => {
         strictEqual(data, 'end');
-        client.destroy();
+        client.close();
       }));
       request.end();
       request.resume();
@@ -83,7 +83,7 @@ const {
       request.on('data', (chunk) => (data += chunk));
       request.on('end', mustCall(() => {
         strictEqual(data, 'test\uD83D\uDE00');
-        client.destroy();
+        client.close();
       }));
       request.end();
       request.resume();
@@ -110,7 +110,7 @@ const {
       };
       const request = client.request(headers);
       request.on('data', mustNotCall());
-      request.on('end', mustCall(() => client.destroy()));
+      request.on('end', mustCall(() => client.close()));
       request.end();
       request.resume();
     }));
@@ -143,7 +143,7 @@ const {
       }));
       request.on('data', mustNotCall());
       request.on('end', mustCall(() => {
-        client.destroy();
+        client.close();
         server.close();
       }));
       request.end();
@@ -172,7 +172,7 @@ const {
       const request = client.request(headers);
       request.on('data', mustNotCall());
       request.on('end', mustCall(() => {
-        client.destroy();
+        client.close();
         server.close();
       }));
       request.end();
@@ -208,7 +208,7 @@ const {
       }));
       request.on('data', mustNotCall());
       request.on('end', mustCall(() => {
-        client.destroy();
+        client.close();
         server.close();
       }));
       request.end();
@@ -243,7 +243,7 @@ const {
       }));
       request.on('data', mustNotCall());
       request.on('end', mustCall(() => {
-        client.destroy();
+        client.close();
         server.close();
       }));
       request.end();
@@ -283,7 +283,7 @@ const {
       }));
       request.on('data', mustNotCall());
       request.on('end', mustCall(() => {
-        client.destroy();
+        client.close();
         server.close();
       }));
       request.end();
@@ -315,7 +315,7 @@ const {
       }));
       request.on('data', mustNotCall());
       request.on('end', mustCall(() => {
-        client.destroy();
+        client.close();
         server.close();
       }));
       request.end();
diff --git a/test/parallel/test-http2-compat-serverresponse-finished.js b/test/parallel/test-http2-compat-serverresponse-finished.js
index b816b922202dd6..ceaa6eb5c3cf2c 100644
--- a/test/parallel/test-http2-compat-serverresponse-finished.js
+++ b/test/parallel/test-http2-compat-serverresponse-finished.js
@@ -39,7 +39,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-flushheaders.js b/test/parallel/test-http2-compat-serverresponse-flushheaders.js
index 68d4789f69be53..d155b07863d26c 100644
--- a/test/parallel/test-http2-compat-serverresponse-flushheaders.js
+++ b/test/parallel/test-http2-compat-serverresponse-flushheaders.js
@@ -51,7 +51,7 @@ server.listen(0, common.mustCall(function() {
       serverResponse.end();
     }, 1));
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js b/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js
index 8bfd64ebec6dec..171eb7e27f32b1 100644
--- a/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js
+++ b/test/parallel/test-http2-compat-serverresponse-headers-after-destroy.js
@@ -14,8 +14,6 @@ const server = h2.createServer();
 server.listen(0, common.mustCall(function() {
   const port = server.address().port;
   server.once('request', common.mustCall(function(request, response) {
-    response.destroy();
-
     response.on('finish', common.mustCall(() => {
       assert.strictEqual(response.headersSent, false);
       assert.doesNotThrow(() => response.setHeader('test', 'value'));
@@ -28,6 +26,8 @@ server.listen(0, common.mustCall(function() {
         server.close();
       });
     }));
+
+    response.destroy();
   }));
 
   const url = `http://localhost:${port}`;
@@ -40,7 +40,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-headers.js b/test/parallel/test-http2-compat-serverresponse-headers.js
index ec1071bc34ffa1..2b7252135917ac 100644
--- a/test/parallel/test-http2-compat-serverresponse-headers.js
+++ b/test/parallel/test-http2-compat-serverresponse-headers.js
@@ -179,7 +179,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-settimeout.js b/test/parallel/test-http2-compat-serverresponse-settimeout.js
index 6d06d07f7dc0ab..bb09633727ccf7 100644
--- a/test/parallel/test-http2-compat-serverresponse-settimeout.js
+++ b/test/parallel/test-http2-compat-serverresponse-settimeout.js
@@ -12,7 +12,6 @@ const server = http2.createServer();
 server.on('request', (req, res) => {
   res.setTimeout(msecs, common.mustCall(() => {
     res.end();
-    res.setTimeout(msecs, common.mustNotCall());
   }));
   res.on('finish', common.mustCall(() => {
     res.setTimeout(msecs, common.mustNotCall());
@@ -35,7 +34,7 @@ server.listen(0, common.mustCall(() => {
     ':authority': `localhost:${port}`
   });
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
   }));
   req.resume();
   req.end();
diff --git a/test/parallel/test-http2-compat-serverresponse-statuscode.js b/test/parallel/test-http2-compat-serverresponse-statuscode.js
index 96b033328f2855..0c2a96f55953b8 100644
--- a/test/parallel/test-http2-compat-serverresponse-statuscode.js
+++ b/test/parallel/test-http2-compat-serverresponse-statuscode.js
@@ -69,7 +69,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js
index 45a876d674313b..87e172402899f2 100644
--- a/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js
+++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property-set.js
@@ -42,7 +42,7 @@ server.listen(0, common.mustCall(function() {
       assert.strictEqual(headers[':status'], 200);
     }, 1));
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js
index 21a5b6ea4e2820..8a083cf3ba1638 100644
--- a/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js
+++ b/test/parallel/test-http2-compat-serverresponse-statusmessage-property.js
@@ -41,7 +41,7 @@ server.listen(0, common.mustCall(function() {
       assert.strictEqual(headers[':status'], 200);
     }, 1));
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-statusmessage.js b/test/parallel/test-http2-compat-serverresponse-statusmessage.js
index 841bafe724a7a8..dee916d1aeef54 100644
--- a/test/parallel/test-http2-compat-serverresponse-statusmessage.js
+++ b/test/parallel/test-http2-compat-serverresponse-statusmessage.js
@@ -45,7 +45,7 @@ server.listen(0, common.mustCall(function() {
       assert.strictEqual(headers['foo-bar'], 'abc123');
     }, 1));
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-serverresponse-trailers.js b/test/parallel/test-http2-compat-serverresponse-trailers.js
index 7332f9e8d0b63d..66ad8843fa33b9 100755
--- a/test/parallel/test-http2-compat-serverresponse-trailers.js
+++ b/test/parallel/test-http2-compat-serverresponse-trailers.js
@@ -68,7 +68,7 @@ server.listen(0, common.mustCall(() => {
     }));
     request.resume();
     request.on('end', common.mustCall(() => {
-      client.destroy();
+      client.close();
       server.close();
     }));
   }));
diff --git a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js
index 58a4ca053d222c..a62bb1b0ac78f1 100644
--- a/test/parallel/test-http2-compat-serverresponse-write-no-cb.js
+++ b/test/parallel/test-http2-compat-serverresponse-write-no-cb.js
@@ -6,44 +6,33 @@ const { mustCall,
         hasCrypto, skip } = require('../common');
 if (!hasCrypto)
   skip('missing crypto');
-const { throws } = require('assert');
 const { createServer, connect } = require('http2');
 
 // Http2ServerResponse.write does not imply there is a callback
 
-const expectedError = expectsError({
-  code: 'ERR_HTTP2_STREAM_CLOSED',
-  message: 'The stream is already closed'
-}, 2);
-
 {
   const server = createServer();
   server.listen(0, mustCall(() => {
     const port = server.address().port;
     const url = `http://localhost:${port}`;
     const client = connect(url, mustCall(() => {
-      const headers = {
-        ':path': '/',
-        ':method': 'GET',
-        ':scheme': 'http',
-        ':authority': `localhost:${port}`
-      };
-      const request = client.request(headers);
-      request.end();
+      const request = client.request();
       request.resume();
+      request.on('end', mustCall());
+      request.on('close', mustCall(() => {
+        client.close();
+      }));
     }));
 
     server.once('request', mustCall((request, response) => {
       client.destroy();
       response.stream.session.on('close', mustCall(() => {
         response.on('error', mustNotCall());
-        throws(
+        expectsError(
           () => { response.write('muahaha'); },
-          expectsError({
-            code: 'ERR_HTTP2_STREAM_CLOSED',
-            type: Error,
-            message: 'The stream is already closed'
-          })
+          {
+            code: 'ERR_HTTP2_INVALID_STREAM'
+          }
         );
         server.close();
       }));
@@ -57,21 +46,21 @@ const expectedError = expectsError({
     const port = server.address().port;
     const url = `http://localhost:${port}`;
     const client = connect(url, mustCall(() => {
-      const headers = {
-        ':path': '/',
-        ':method': 'get',
-        ':scheme': 'http',
-        ':authority': `localhost:${port}`
-      };
-      const request = client.request(headers);
-      request.end();
+      const request = client.request();
       request.resume();
+      request.on('end', mustCall());
+      request.on('close', mustCall(() => client.close()));
     }));
 
     server.once('request', mustCall((request, response) => {
       client.destroy();
       response.stream.session.on('close', mustCall(() => {
-        response.write('muahaha', mustCall(expectedError));
+        expectsError(
+          () => response.write('muahaha'),
+          {
+            code: 'ERR_HTTP2_INVALID_STREAM'
+          }
+        );
         server.close();
       }));
     }));
@@ -84,20 +73,20 @@ const expectedError = expectsError({
     const port = server.address().port;
     const url = `http://localhost:${port}`;
     const client = connect(url, mustCall(() => {
-      const headers = {
-        ':path': '/',
-        ':method': 'get',
-        ':scheme': 'http',
-        ':authority': `localhost:${port}`
-      };
-      const request = client.request(headers);
-      request.end();
+      const request = client.request();
       request.resume();
+      request.on('end', mustCall());
+      request.on('close', mustCall(() => client.close()));
     }));
 
     server.once('request', mustCall((request, response) => {
       response.stream.session.on('close', mustCall(() => {
-        response.write('muahaha', 'utf8', mustCall(expectedError));
+        expectsError(
+          () => response.write('muahaha', 'utf8'),
+          {
+            code: 'ERR_HTTP2_INVALID_STREAM'
+          }
+        );
         server.close();
       }));
       client.destroy();
diff --git a/test/parallel/test-http2-compat-serverresponse-writehead.js b/test/parallel/test-http2-compat-serverresponse-writehead.js
index 704f199ca27e99..5fd787e100350c 100644
--- a/test/parallel/test-http2-compat-serverresponse-writehead.js
+++ b/test/parallel/test-http2-compat-serverresponse-writehead.js
@@ -23,7 +23,7 @@ server.listen(0, common.mustCall(function() {
       server.close();
       process.nextTick(common.mustCall(() => {
         common.expectsError(() => { response.writeHead(300); }, {
-          code: 'ERR_HTTP2_STREAM_CLOSED'
+          code: 'ERR_HTTP2_INVALID_STREAM'
         });
       }));
     }));
@@ -44,7 +44,7 @@ server.listen(0, common.mustCall(function() {
       assert.strictEqual(headers[':status'], 418);
     }, 1));
     request.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-compat-socket-set.js b/test/parallel/test-http2-compat-socket-set.js
index f62c782a45d8ea..c6107564319143 100644
--- a/test/parallel/test-http2-compat-socket-set.js
+++ b/test/parallel/test-http2-compat-socket-set.js
@@ -99,7 +99,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(() => {
-      client.destroy();
+      client.close();
       server.close();
     }));
     request.end();
diff --git a/test/parallel/test-http2-compat-socket.js b/test/parallel/test-http2-compat-socket.js
index c6a09802981d16..9b98d328111633 100644
--- a/test/parallel/test-http2-compat-socket.js
+++ b/test/parallel/test-http2-compat-socket.js
@@ -82,7 +82,7 @@ server.listen(0, common.mustCall(function() {
     };
     const request = client.request(headers);
     request.on('end', common.mustCall(() => {
-      client.destroy();
+      client.close();
     }));
     request.end();
     request.resume();
diff --git a/test/parallel/test-http2-connect-method.js b/test/parallel/test-http2-connect-method.js
index 78c9a345293c12..b425cafb1478d3 100644
--- a/test/parallel/test-http2-connect-method.js
+++ b/test/parallel/test-http2-connect-method.js
@@ -13,7 +13,8 @@ const {
   HTTP2_HEADER_AUTHORITY,
   HTTP2_HEADER_SCHEME,
   HTTP2_HEADER_PATH,
-  NGHTTP2_CONNECT_ERROR
+  NGHTTP2_CONNECT_ERROR,
+  NGHTTP2_REFUSED_STREAM
 } = http2.constants;
 
 const server = net.createServer(common.mustCall((socket) => {
@@ -34,7 +35,7 @@ server.listen(0, common.mustCall(() => {
   const proxy = http2.createServer();
   proxy.on('stream', common.mustCall((stream, headers) => {
     if (headers[HTTP2_HEADER_METHOD] !== 'CONNECT') {
-      stream.rstWithRefused();
+      stream.close(NGHTTP2_REFUSED_STREAM);
       return;
     }
     const auth = new URL(`tcp://${headers[HTTP2_HEADER_AUTHORITY]}`);
@@ -47,7 +48,7 @@ server.listen(0, common.mustCall(() => {
     });
     socket.on('close', common.mustCall());
     socket.on('error', (error) => {
-      stream.rstStream(NGHTTP2_CONNECT_ERROR);
+      stream.close(NGHTTP2_CONNECT_ERROR);
     });
   }));
 
@@ -99,7 +100,7 @@ server.listen(0, common.mustCall(() => {
     req.on('data', (chunk) => data += chunk);
     req.on('end', common.mustCall(() => {
       assert.strictEqual(data, 'hello');
-      client.destroy();
+      client.close();
       proxy.close();
       server.close();
     }));
diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js
index e5a4e429907090..894c51fe3d9330 100644
--- a/test/parallel/test-http2-connect.js
+++ b/test/parallel/test-http2-connect.js
@@ -20,7 +20,7 @@ const { createServer, connect } = require('http2');
 
     for (const client of clients) {
       client.once('connect', mustCall((headers) => {
-        client.destroy();
+        client.close();
         clients.delete(client);
         if (clients.size === 0) {
           server.close();
@@ -33,7 +33,11 @@ const { createServer, connect } = require('http2');
 // check for https as protocol
 {
   const authority = 'https://localhost';
-  doesNotThrow(() => connect(authority));
+  doesNotThrow(() => {
+    // A socket error may or may not be reported, keep this as a non-op
+    // instead of a mustCall or mustNotCall
+    connect(authority).on('error', () => {});
+  });
 }
 
 // check for error for an invalid protocol (not http or https)
diff --git a/test/parallel/test-http2-cookies.js b/test/parallel/test-http2-cookies.js
index 48b08b6367b4b3..cf763915389287 100644
--- a/test/parallel/test-http2-cookies.js
+++ b/test/parallel/test-http2-cookies.js
@@ -54,7 +54,7 @@ server.on('listening', common.mustCall(() => {
 
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 
diff --git a/test/parallel/test-http2-create-client-connect.js b/test/parallel/test-http2-create-client-connect.js
index 373c258c7dd286..02c6c70642acb0 100644
--- a/test/parallel/test-http2-create-client-connect.js
+++ b/test/parallel/test-http2-create-client-connect.js
@@ -30,7 +30,7 @@ const URL = url.URL;
                                       () => setImmediate(() => server.close()));
 
     const maybeClose = common.mustCall((client) => {
-      client.destroy();
+      client.close();
       serverClose.dec();
     }, items.length);
 
@@ -42,7 +42,7 @@ const URL = url.URL;
 
     // Will fail because protocol does not match the server.
     h2.connect({ port: port, protocol: 'https:' })
-      .on('socketError', common.mustCall(() => serverClose.dec()));
+      .on('error', common.mustCall(() => serverClose.dec()));
   }));
 }
 
@@ -55,10 +55,8 @@ const URL = url.URL;
   };
 
   const server = h2.createSecureServer(options);
-  server.listen(0);
-
-  server.on('listening', common.mustCall(function() {
-    const port = this.address().port;
+  server.listen(0, common.mustCall(() => {
+    const port = server.address().port;
 
     const opts = { rejectUnauthorized: false };
 
@@ -74,7 +72,7 @@ const URL = url.URL;
                                       () => setImmediate(() => server.close()));
 
     const maybeClose = common.mustCall((client) => {
-      client.destroy();
+      client.close();
       serverClose.dec();
     }, items.length);
 
diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js
index 149b5164231a21..c5e9468f9bd15b 100644
--- a/test/parallel/test-http2-create-client-session.js
+++ b/test/parallel/test-http2-create-client-session.js
@@ -5,6 +5,8 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const h2 = require('http2');
+const Countdown = require('../common/countdown');
+
 const body =
   '<html><head></head><body><h1>this is some data</h2></body></html>';
 
@@ -23,21 +25,26 @@ function onStream(stream, headers, flags) {
     'content-type': 'text/html',
     ':status': 200
   });
-  stream.end(body);
+  stream.write(body.slice(0, 20));
+  stream.end(body.slice(20));
 }
 
 server.listen(0);
 
-let expected = count;
+server.on('listening', common.mustCall(() => {
 
-server.on('listening', common.mustCall(function() {
+  const client = h2.connect(`http://localhost:${server.address().port}`);
+  client.setMaxListeners(100);
 
-  const client = h2.connect(`http://localhost:${this.address().port}`);
+  client.on('goaway', console.log);
 
-  const headers = { ':path': '/' };
+  const countdown = new Countdown(count, () => {
+    client.close();
+    server.close();
+  });
 
   for (let n = 0; n < count; n++) {
-    const req = client.request(headers);
+    const req = client.request();
 
     req.on('response', common.mustCall(function(headers) {
       assert.strictEqual(headers[':status'], 200, 'status code is set');
@@ -51,12 +58,7 @@ server.on('listening', common.mustCall(function() {
     req.on('data', (d) => data += d);
     req.on('end', common.mustCall(() => {
       assert.strictEqual(body, data);
-      if (--expected === 0) {
-        server.close();
-        client.destroy();
-      }
     }));
-    req.end();
+    req.on('close', common.mustCall(() => countdown.dec()));
   }
-
 }));
diff --git a/test/parallel/test-http2-createsecureserver-nooptions.js b/test/parallel/test-http2-createsecureserver-nooptions.js
index 05029cba2bb638..58e1600790041e 100644
--- a/test/parallel/test-http2-createsecureserver-nooptions.js
+++ b/test/parallel/test-http2-createsecureserver-nooptions.js
@@ -6,7 +6,7 @@ if (!common.hasCrypto)
 
 const http2 = require('http2');
 
-const invalidOptions = [() => {}, 1, 'test', null, undefined];
+const invalidOptions = [() => {}, 1, 'test', null];
 const invalidArgTypeError = {
   type: TypeError,
   code: 'ERR_INVALID_ARG_TYPE',
@@ -14,9 +14,9 @@ const invalidArgTypeError = {
 };
 
 // Error if options are not passed to createSecureServer
-invalidOptions.forEach((invalidOption) =>
+invalidOptions.forEach((invalidOption) => {
   common.expectsError(
     () => http2.createSecureServer(invalidOption),
     invalidArgTypeError
-  )
-);
+  );
+});
diff --git a/test/parallel/test-http2-createwritereq.js b/test/parallel/test-http2-createwritereq.js
index ca394a5d425470..40d0ddc117c5ad 100644
--- a/test/parallel/test-http2-createwritereq.js
+++ b/test/parallel/test-http2-createwritereq.js
@@ -54,7 +54,7 @@ server.listen(0, common.mustCall(function() {
     req.resume();
 
     req.on('end', common.mustCall(function() {
-      client.destroy();
+      client.close();
       testsFinished++;
 
       if (testsFinished === testsToRun) {
diff --git a/test/parallel/test-http2-date-header.js b/test/parallel/test-http2-date-header.js
index ab0654e64cbcd7..2b63e1b7899a2e 100644
--- a/test/parallel/test-http2-date-header.js
+++ b/test/parallel/test-http2-date-header.js
@@ -24,6 +24,6 @@ server.listen(0, common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
 }));
diff --git a/test/parallel/test-http2-dont-lose-data.js b/test/parallel/test-http2-dont-lose-data.js
new file mode 100644
index 00000000000000..eb85277b7b124c
--- /dev/null
+++ b/test/parallel/test-http2-dont-lose-data.js
@@ -0,0 +1,58 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+
+const server = http2.createServer();
+
+server.on('stream', (s) => {
+  assert(s.pushAllowed);
+
+  s.pushStream({ ':path': '/file' }, common.mustCall((err, pushStream) => {
+    assert.ifError(err);
+    pushStream.respond();
+    pushStream.end('a push stream');
+  }));
+
+  s.respond();
+  s.end('hello world');
+});
+
+server.listen(0, () => {
+  server.unref();
+
+  const url = `http://localhost:${server.address().port}`;
+
+  const client = http2.connect(url);
+  const req = client.request();
+
+  let pushStream;
+
+  client.on('stream', common.mustCall((s, headers) => {
+    assert.strictEqual(headers[':path'], '/file');
+    pushStream = s;
+  }));
+
+  req.on('response', common.mustCall((headers) => {
+    let pushData = '';
+    pushStream.setEncoding('utf8');
+    pushStream.on('data', (d) => pushData += d);
+    pushStream.on('end', common.mustCall(() => {
+      assert.strictEqual(pushData, 'a push stream');
+
+      // removing the setImmediate causes the test to pass
+      setImmediate(function() {
+        let data = '';
+        req.setEncoding('utf8');
+        req.on('data', (d) => data += d);
+        req.on('end', common.mustCall(() => {
+          assert.strictEqual(data, 'hello world');
+          client.close();
+        }));
+      });
+    }));
+  }));
+});
diff --git a/test/parallel/test-http2-dont-override.js b/test/parallel/test-http2-dont-override.js
index cc60a2fe802a82..b45713deb3ca60 100644
--- a/test/parallel/test-http2-dont-override.js
+++ b/test/parallel/test-http2-dont-override.js
@@ -44,6 +44,6 @@ server.listen(0, common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
 }));
diff --git a/test/parallel/test-http2-generic-streams-sendfile.js b/test/parallel/test-http2-generic-streams-sendfile.js
index 1054574a8b1ca2..b752b0fdcb815a 100644
--- a/test/parallel/test-http2-generic-streams-sendfile.js
+++ b/test/parallel/test-http2-generic-streams-sendfile.js
@@ -20,7 +20,7 @@ const makeDuplexPair = require('../common/duplexpair');
     createConnection: common.mustCall(() => clientSide)
   });
 
-  const req = client.request({ ':path': '/' });
+  const req = client.request();
 
   req.on('response', common.mustCall((headers) => {
     assert.strictEqual(headers[':status'], 200);
@@ -28,9 +28,7 @@ const makeDuplexPair = require('../common/duplexpair');
 
   req.setEncoding('utf8');
   let data = '';
-  req.on('data', (chunk) => {
-    data += chunk;
-  });
+  req.on('data', (chunk) => data += chunk);
   req.on('end', common.mustCall(() => {
     assert.strictEqual(data, fs.readFileSync(__filename, 'utf8'));
     clientSide.destroy();
diff --git a/test/parallel/test-http2-goaway-opaquedata.js b/test/parallel/test-http2-goaway-opaquedata.js
index d8895a82edf464..3f1fb4d7954414 100644
--- a/test/parallel/test-http2-goaway-opaquedata.js
+++ b/test/parallel/test-http2-goaway-opaquedata.js
@@ -10,32 +10,23 @@ const server = http2.createServer();
 const data = Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5]);
 
 server.on('stream', common.mustCall((stream) => {
-  stream.session.shutdown({
-    errorCode: 1,
-    opaqueData: data
-  });
+  stream.session.goaway(0, 0, data);
+  stream.respond();
   stream.end();
-  stream.on('error', common.mustCall(common.expectsError({
-    code: 'ERR_HTTP2_STREAM_ERROR',
-    type: Error,
-    message: 'Stream closed with error code 7'
-  })));
 }));
 
 server.listen(0, () => {
 
   const client = http2.connect(`http://localhost:${server.address().port}`);
-  client.on('goaway', common.mustCall((code, lastStreamID, buf) => {
-    assert.deepStrictEqual(code, 1);
-    assert.deepStrictEqual(lastStreamID, 0);
+  client.once('goaway', common.mustCall((code, lastStreamID, buf) => {
+    assert.deepStrictEqual(code, 0);
+    assert.deepStrictEqual(lastStreamID, 1);
     assert.deepStrictEqual(data, buf);
-    // Call shutdown() here so that emitGoaway calls destroy()
-    client.shutdown();
     server.close();
   }));
-  const req = client.request({ ':path': '/' });
+  const req = client.request();
   req.resume();
   req.on('end', common.mustCall());
+  req.on('close', common.mustCall());
   req.end();
-
 });
diff --git a/test/parallel/test-http2-head-request.js b/test/parallel/test-http2-head-request.js
index 8c91132b5fdeeb..f780394f3d6289 100644
--- a/test/parallel/test-http2-head-request.js
+++ b/test/parallel/test-http2-head-request.js
@@ -53,6 +53,6 @@ server.listen(0, () => {
   req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
 });
diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js
index 04e9ca480c9099..01b694e586dd49 100644
--- a/test/parallel/test-http2-https-fallback.js
+++ b/test/parallel/test-http2-https-fallback.js
@@ -52,7 +52,7 @@ function onSession(session) {
     strictEqual(alpnProtocol, 'h2');
     strictEqual(httpVersion, '2.0');
 
-    session.destroy();
+    session.close();
     this.cleanup();
   }));
   request.end();
diff --git a/test/parallel/test-http2-info-headers-errors.js b/test/parallel/test-http2-info-headers-errors.js
index 83f85b279d5c6e..555b22242664ae 100644
--- a/test/parallel/test-http2-info-headers-errors.js
+++ b/test/parallel/test-http2-info-headers-errors.js
@@ -49,10 +49,6 @@ server.on('stream', common.mustCall((stream, headers) => {
   if (currentError.type === 'stream') {
     stream.session.on('error', errorMustNotCall);
     stream.on('error', errorMustCall);
-    stream.on('error', common.mustCall(() => {
-      stream.respond();
-      stream.end();
-    }));
   } else {
     stream.session.once('error', errorMustCall);
     stream.on('error', errorMustNotCall);
@@ -64,24 +60,21 @@ server.on('stream', common.mustCall((stream, headers) => {
 server.listen(0, common.mustCall(() => runTest(tests.shift())));
 
 function runTest(test) {
-  const port = server.address().port;
-  const url = `http://localhost:${port}`;
-  const headers = {
-    ':path': '/',
-    ':method': 'POST',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  };
-
-  const client = http2.connect(url);
-  const req = client.request(headers);
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request({ ':method': 'POST' });
 
   currentError = test;
   req.resume();
   req.end();
 
-  req.on('end', common.mustCall(() => {
-    client.destroy();
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 2'
+  }));
+
+  req.on('close', common.mustCall(() => {
+    client.close();
 
     if (!tests.length) {
       server.close();
diff --git a/test/parallel/test-http2-info-headers.js b/test/parallel/test-http2-info-headers.js
index 609f56e8b8566c..a71a3121b53c26 100644
--- a/test/parallel/test-http2-info-headers.js
+++ b/test/parallel/test-http2-info-headers.js
@@ -88,7 +88,7 @@ server.on('listening', common.mustCall(() => {
 
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 
diff --git a/test/parallel/test-http2-invalidargtypes-errors.js b/test/parallel/test-http2-invalidargtypes-errors.js
index 3471e46fdf4ca4..ff189a2977559f 100644
--- a/test/parallel/test-http2-invalidargtypes-errors.js
+++ b/test/parallel/test-http2-invalidargtypes-errors.js
@@ -7,29 +7,25 @@ const http2 = require('http2');
 
 const server = http2.createServer();
 
-server.on(
-  'stream',
-  common.mustCall((stream) => {
-    const invalidArgTypeError = (param, type) => ({
-      type: TypeError,
+server.on('stream', common.mustCall((stream) => {
+  common.expectsError(
+    () => stream.close('string'),
+    {
       code: 'ERR_INVALID_ARG_TYPE',
-      message: `The "${param}" argument must be of type ${type}`
-    });
-    common.expectsError(
-      () => stream.rstStream('string'),
-      invalidArgTypeError('code', 'number')
-    );
-    stream.session.destroy();
-  })
-);
+      type: TypeError,
+      message: 'The "code" argument must be of type number'
+    }
+  );
+  stream.respond();
+  stream.end('ok');
+}));
 
-server.listen(
-  0,
-  common.mustCall(() => {
-    const client = http2.connect(`http://localhost:${server.address().port}`);
-    const req = client.request();
-    req.resume();
-    req.on('end', common.mustCall(() => server.close()));
-    req.end();
-  })
-);
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request();
+  req.resume();
+  req.on('close', common.mustCall(() => {
+    server.close();
+    client.close();
+  }));
+}));
diff --git a/test/parallel/test-http2-max-concurrent-streams.js b/test/parallel/test-http2-max-concurrent-streams.js
index a65ac90c535b03..ffc04e98f134b2 100644
--- a/test/parallel/test-http2-max-concurrent-streams.js
+++ b/test/parallel/test-http2-max-concurrent-streams.js
@@ -5,64 +5,52 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const h2 = require('http2');
-
-const {
-  HTTP2_HEADER_METHOD,
-  HTTP2_HEADER_STATUS,
-  HTTP2_HEADER_PATH,
-  HTTP2_METHOD_POST
-} = h2.constants;
+const Countdown = require('../common/countdown');
 
 // Only allow one stream to be open at a time
 const server = h2.createServer({ settings: { maxConcurrentStreams: 1 } });
 
 // The stream handler must be called only once
 server.on('stream', common.mustCall((stream) => {
-  stream.respond({ [HTTP2_HEADER_STATUS]: 200 });
+  stream.respond();
   stream.end('hello world');
 }));
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
 
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
 
-  let reqs = 2;
-  function onEnd() {
-    if (--reqs === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
+  const countdown = new Countdown(2, () => {
+    server.close();
+    client.close();
+  });
 
   client.on('remoteSettings', common.mustCall((settings) => {
     assert.strictEqual(settings.maxConcurrentStreams, 1);
   }));
 
   // This one should go through with no problems
-  const req1 = client.request({
-    [HTTP2_HEADER_PATH]: '/',
-    [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST
-  });
-  req1.on('aborted', common.mustNotCall());
-  req1.on('response', common.mustCall());
-  req1.resume();
-  req1.on('end', onEnd);
-  req1.end();
-
-  // This one should be aborted
-  const req2 = client.request({
-    [HTTP2_HEADER_PATH]: '/',
-    [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST
-  });
-  req2.on('aborted', common.mustCall());
-  req2.on('response', common.mustNotCall());
-  req2.resume();
-  req2.on('end', onEnd);
-  req2.on('error', common.mustCall(common.expectsError({
-    code: 'ERR_HTTP2_STREAM_ERROR',
-    type: Error,
-    message: 'Stream closed with error code 7'
-  })));
+  {
+    const req = client.request({ ':method': 'POST' });
+    req.on('aborted', common.mustNotCall());
+    req.on('response', common.mustCall());
+    req.resume();
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => countdown.dec()));
+    req.end();
+  }
 
+  {
+    // This one should be aborted
+    const req = client.request({ ':method': 'POST' });
+    req.on('aborted', common.mustCall());
+    req.on('response', common.mustNotCall());
+    req.resume();
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => countdown.dec()));
+    req.on('error', common.expectsError({
+      code: 'ERR_HTTP2_STREAM_ERROR',
+      type: Error,
+      message: 'Stream closed with error code 7'
+    }));
+  }
 }));
diff --git a/test/parallel/test-http2-methods.js b/test/parallel/test-http2-methods.js
index 36f64f13abcf86..a291bdf00800d5 100644
--- a/test/parallel/test-http2-methods.js
+++ b/test/parallel/test-http2-methods.js
@@ -41,7 +41,7 @@ server.on('listening', common.mustCall(() => {
     req.on('end', common.mustCall(() => {
       if (--expected === 0) {
         server.close();
-        client.destroy();
+        client.close();
       }
     }));
     req.end();
diff --git a/test/parallel/test-http2-misbehaving-flow-control-paused.js b/test/parallel/test-http2-misbehaving-flow-control-paused.js
index ee799b1d5a27d3..0b7299d5ac80a8 100644
--- a/test/parallel/test-http2-misbehaving-flow-control-paused.js
+++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js
@@ -56,32 +56,24 @@ let client;
 
 const server = h2.createServer({ settings: { initialWindowSize: 36 } });
 server.on('stream', (stream) => {
-
-  // Not reading causes the flow control window to get backed up.
   stream.pause();
-
-  stream.on('error', common.mustCall((err) => {
-    common.expectsError({
-      code: 'ERR_HTTP2_STREAM_ERROR',
-      type: Error,
-      message: 'Stream closed with error code 3'
-    })(err);
+  stream.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 3'
+  }));
+  stream.on('close', common.mustCall(() => {
     server.close();
     client.destroy();
   }));
-
   stream.on('end', common.mustNotCall());
-
   stream.respond();
   stream.end('ok');
 });
 
 server.listen(0, () => {
   client = net.connect(server.address().port, () => {
-    client.on('error', console.log);
-
     client.write(preamble);
-
     client.write(data);
     client.write(data);
     client.write(data);
diff --git a/test/parallel/test-http2-misbehaving-flow-control.js b/test/parallel/test-http2-misbehaving-flow-control.js
index 010e07741316b6..8a0b411b8de65c 100644
--- a/test/parallel/test-http2-misbehaving-flow-control.js
+++ b/test/parallel/test-http2-misbehaving-flow-control.js
@@ -29,6 +29,21 @@ const preamble = Buffer.from([
 ]);
 
 const data = Buffer.from([
+  0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c,
+  0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a,
+  0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c,
+  0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a,
+  0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c,
+  0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a,
+  0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c,
+  0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a,
+  0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
+  0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c,
+  0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a,
   0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d,
   0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c,
   0x6c, 0x6f, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a
@@ -51,30 +66,23 @@ const data = Buffer.from([
 let client;
 const server = h2.createServer({ settings: { initialWindowSize: 18 } });
 server.on('stream', (stream) => {
-
-  stream.resume();
-
-  stream.on('error', common.mustCall((err) => {
-    common.expectsError({
-      code: 'ERR_HTTP2_STREAM_ERROR',
-      type: Error,
-      message: 'Stream closed with error code 3'
-    })(err);
+  stream.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 3'
+  }));
+  stream.on('close', common.mustCall(() => {
     server.close();
     client.destroy();
   }));
-
+  stream.resume();
   stream.respond();
   stream.end('ok');
 });
 
 server.listen(0, () => {
   client = net.connect(server.address().port, () => {
-    client.on('error', console.log);
-
     client.write(preamble);
-
-    client.write(data);
     client.write(data);
     client.write(data);
   });
diff --git a/test/parallel/test-http2-misused-pseudoheaders.js b/test/parallel/test-http2-misused-pseudoheaders.js
index 2ccb676b2199e5..fc53d01a2f6bb0 100644
--- a/test/parallel/test-http2-misused-pseudoheaders.js
+++ b/test/parallel/test-http2-misused-pseudoheaders.js
@@ -8,11 +8,7 @@ const h2 = require('http2');
 
 const server = h2.createServer();
 
-// we use the lower-level API here
-server.on('stream', common.mustCall(onStream));
-
-function onStream(stream, headers, flags) {
-
+server.on('stream', common.mustCall((stream) => {
   [
     ':path',
     ':authority',
@@ -25,10 +21,7 @@ function onStream(stream, headers, flags) {
                   }));
   });
 
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  }, {
+  stream.respond({}, {
     getTrailers: common.mustCall((trailers) => {
       trailers[':status'] = 'bar';
     })
@@ -39,22 +32,24 @@ function onStream(stream, headers, flags) {
   }));
 
   stream.end('hello world');
-}
-
-server.listen(0);
+}));
 
-server.on('listening', common.mustCall(() => {
 
+server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request();
 
-  const req = client.request({ ':path': '/' });
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 2'
+  }));
 
   req.on('response', common.mustCall());
   req.resume();
-  req.on('end', common.mustCall(() => {
+  req.on('end', common.mustCall());
+  req.on('close', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
-  req.end();
-
 }));
diff --git a/test/parallel/test-http2-multi-content-length.js b/test/parallel/test-http2-multi-content-length.js
index d0f0094d2408aa..4d18356f127da0 100644
--- a/test/parallel/test-http2-multi-content-length.js
+++ b/test/parallel/test-http2-multi-content-length.js
@@ -4,6 +4,7 @@ const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
 const http2 = require('http2');
+const Countdown = require('../common/countdown');
 
 const server = http2.createServer();
 
@@ -15,29 +16,25 @@ server.on('stream', common.mustCall((stream) => {
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
 
-  let remaining = 3;
-  function maybeClose() {
-    if (--remaining === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
+  const countdown = new Countdown(2, () => {
+    server.close();
+    client.close();
+  });
 
-  {
-    // Request 1 will fail because there are two content-length header values
-    const req = client.request({
-      ':method': 'POST',
-      'content-length': 1,
-      'Content-Length': 2
-    });
-    req.on('error', common.expectsError({
+  // Request 1 will fail because there are two content-length header values
+  common.expectsError(
+    () => {
+      client.request({
+        ':method': 'POST',
+        'content-length': 1,
+        'Content-Length': 2
+      });
+    }, {
       code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
       type: Error,
       message: 'Header field "content-length" must have only a single value'
-    }));
-    req.on('error', common.mustCall(maybeClose));
-    req.end('a');
-  }
+    }
+  );
 
   {
     // Request 2 will succeed
@@ -46,7 +43,8 @@ server.listen(0, common.mustCall(() => {
       'content-length': 1
     });
     req.resume();
-    req.on('end', common.mustCall(maybeClose));
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => countdown.dec()));
     req.end('a');
   }
 
@@ -55,7 +53,8 @@ server.listen(0, common.mustCall(() => {
     // header to be set for non-payload bearing requests...
     const req = client.request({ 'content-length': 1 });
     req.resume();
-    req.on('end', common.mustCall(maybeClose));
+    req.on('end', common.mustCall());
+    req.on('close', common.mustCall(() => countdown.dec()));
     req.on('error', common.expectsError({
       code: 'ERR_HTTP2_STREAM_ERROR',
       type: Error,
diff --git a/test/parallel/test-http2-multiheaders-raw.js b/test/parallel/test-http2-multiheaders-raw.js
index c06bf23bff3071..50486450d5aeb7 100644
--- a/test/parallel/test-http2-multiheaders-raw.js
+++ b/test/parallel/test-http2-multiheaders-raw.js
@@ -44,6 +44,6 @@ server.listen(0, common.mustCall(() => {
   const req = client.request(src);
   req.on('close', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
 }));
diff --git a/test/parallel/test-http2-multiheaders.js b/test/parallel/test-http2-multiheaders.js
index 5e477104091cb1..9bf8f76d22e60e 100644
--- a/test/parallel/test-http2-multiheaders.js
+++ b/test/parallel/test-http2-multiheaders.js
@@ -56,6 +56,6 @@ server.listen(0, common.mustCall(() => {
   req.on('response', common.mustCall(checkHeaders));
   req.on('close', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
 }));
diff --git a/test/parallel/test-http2-multiplex.js b/test/parallel/test-http2-multiplex.js
index c818a28572eca7..1778bced5f92f4 100644
--- a/test/parallel/test-http2-multiplex.js
+++ b/test/parallel/test-http2-multiplex.js
@@ -8,6 +8,7 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const http2 = require('http2');
+const Countdown = require('../common/countdown');
 
 const server = http2.createServer();
 
@@ -20,15 +21,12 @@ server.on('stream', common.mustCall((stream) => {
 
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
+  client.setMaxListeners(100);
 
-  let remaining = count;
-
-  function maybeClose() {
-    if (--remaining === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
+  const countdown = new Countdown(count, () => {
+    server.close();
+    client.close();
+  });
 
   function doRequest() {
     const req = client.request({ ':method': 'POST ' });
@@ -38,8 +36,8 @@ server.listen(0, common.mustCall(() => {
     req.on('data', (chunk) => data += chunk);
     req.on('end', common.mustCall(() => {
       assert.strictEqual(data, 'abcdefghij');
-      maybeClose();
     }));
+    req.on('close', common.mustCall(() => countdown.dec()));
 
     let n = 0;
     function writeChunk() {
diff --git a/test/parallel/test-http2-no-more-streams.js b/test/parallel/test-http2-no-more-streams.js
index 6f4169756c0b4a..dd06a709f23023 100644
--- a/test/parallel/test-http2-no-more-streams.js
+++ b/test/parallel/test-http2-no-more-streams.js
@@ -25,7 +25,7 @@ server.listen(0, common.mustCall(() => {
 
     const countdown = new Countdown(2, common.mustCall(() => {
       server.close();
-      client.destroy();
+      client.close();
     }));
 
     {
diff --git a/test/parallel/test-http2-options-max-headers-block-length.js b/test/parallel/test-http2-options-max-headers-block-length.js
index 402803dd33a73a..a728c28c6576d4 100644
--- a/test/parallel/test-http2-options-max-headers-block-length.js
+++ b/test/parallel/test-http2-options-max-headers-block-length.js
@@ -10,9 +10,7 @@ const server = h2.createServer();
 
 // we use the lower-level API here
 server.on('stream', common.mustNotCall());
-server.listen(0);
-
-server.on('listening', common.mustCall(() => {
+server.listen(0, common.mustCall(() => {
 
   // Setting the maxSendHeaderBlockLength, then attempting to send a
   // headers block that is too big should cause a 'frameError' to
@@ -24,13 +22,13 @@ server.on('listening', common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`,
                             options);
 
-  const req = client.request({ ':path': '/' });
-
+  const req = client.request();
   req.on('response', common.mustNotCall());
 
   req.resume();
-  req.on('end', common.mustCall(() => {
-    client.destroy();
+  req.on('close', common.mustCall(() => {
+    client.close();
+    server.close();
   }));
 
   req.on('frameError', common.mustCall((type, code) => {
@@ -42,33 +40,4 @@ server.on('listening', common.mustCall(() => {
     type: Error,
     message: 'Stream closed with error code 7'
   }));
-
-  req.end();
-
-  // if no frameError listener, should emit 'error' with
-  // code ERR_HTTP2_FRAME_ERROR
-  const req2 = client.request({ ':path': '/' });
-
-  req2.on('response', common.mustNotCall());
-
-  req2.resume();
-  req2.on('end', common.mustCall(() => {
-    server.close();
-    client.destroy();
-  }));
-
-  req2.once('error', common.mustCall((err) => {
-    common.expectsError({
-      code: 'ERR_HTTP2_FRAME_ERROR',
-      type: Error
-    })(err);
-    req2.on('error', common.expectsError({
-      code: 'ERR_HTTP2_STREAM_ERROR',
-      type: Error,
-      message: 'Stream closed with error code 7'
-    }));
-  }));
-
-  req2.end();
-
 }));
diff --git a/test/parallel/test-http2-options-max-reserved-streams.js b/test/parallel/test-http2-options-max-reserved-streams.js
index d54ca6a7886b3c..994a8817451686 100644
--- a/test/parallel/test-http2-options-max-reserved-streams.js
+++ b/test/parallel/test-http2-options-max-reserved-streams.js
@@ -5,20 +5,24 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const h2 = require('http2');
+const Countdown = require('../common/countdown');
 
 const server = h2.createServer();
+let client;
+
+const countdown = new Countdown(3, () => {
+  server.close();
+  client.close();
+});
 
 // we use the lower-level API here
 server.on('stream', common.mustCall((stream) => {
-  stream.respond({ ':status': 200 });
-
   // The first pushStream will complete as normal
   stream.pushStream({
-    ':scheme': 'http',
     ':path': '/foobar',
-    ':authority': `localhost:${server.address().port}`,
-  }, common.mustCall((pushedStream) => {
-    pushedStream.respond({ ':status': 200 });
+  }, common.mustCall((err, pushedStream) => {
+    assert.ifError(err);
+    pushedStream.respond();
     pushedStream.end();
     pushedStream.on('aborted', common.mustNotCall());
   }));
@@ -27,52 +31,41 @@ server.on('stream', common.mustCall((stream) => {
   // will reject it due to the maxReservedRemoteStreams option
   // being set to only 1
   stream.pushStream({
-    ':scheme': 'http',
     ':path': '/foobar',
-    ':authority': `localhost:${server.address().port}`,
-  }, common.mustCall((pushedStream) => {
-    pushedStream.respond({ ':status': 200 });
+  }, common.mustCall((err, pushedStream) => {
+    assert.ifError(err);
+    pushedStream.respond();
     pushedStream.on('aborted', common.mustCall());
     pushedStream.on('error', common.mustNotCall());
-    pushedStream.on('close',
-                    common.mustCall((code) => assert.strictEqual(code, 8)));
+    pushedStream.on('close', common.mustCall((code) => {
+      assert.strictEqual(code, 8);
+      countdown.dec();
+    }));
   }));
 
+  stream.respond();
   stream.end('hello world');
 }));
 server.listen(0);
 
 server.on('listening', common.mustCall(() => {
+  client = h2.connect(`http://localhost:${server.address().port}`,
+                      { maxReservedRemoteStreams: 1 });
 
-  const options = {
-    maxReservedRemoteStreams: 1
-  };
-
-  const client = h2.connect(`http://localhost:${server.address().port}`,
-                            options);
-
-  let remaining = 2;
-  function maybeClose() {
-    if (--remaining === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
-
-  const req = client.request({ ':path': '/' });
+  const req = client.request();
 
   // Because maxReservedRemoteStream is 1, the stream event
   // must only be emitted once, even tho the server sends
   // two push streams.
   client.on('stream', common.mustCall((stream) => {
     stream.resume();
+    stream.on('push', common.mustCall());
     stream.on('end', common.mustCall());
-    stream.on('close', common.mustCall(maybeClose));
+    stream.on('close', common.mustCall(() => countdown.dec()));
   }));
 
   req.on('response', common.mustCall());
-
   req.resume();
   req.on('end', common.mustCall());
-  req.on('close', common.mustCall(maybeClose));
+  req.on('close', common.mustCall(() => countdown.dec()));
 }));
diff --git a/test/parallel/test-http2-padding-callback.js b/test/parallel/test-http2-padding-callback.js
index af547ad498da1b..6d6a6b27221b07 100644
--- a/test/parallel/test-http2-padding-callback.js
+++ b/test/parallel/test-http2-padding-callback.js
@@ -45,7 +45,7 @@ server.on('listening', common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 }));
diff --git a/test/parallel/test-http2-ping.js b/test/parallel/test-http2-ping.js
index 4892d67b4d738d..32fb8926e4716c 100644
--- a/test/parallel/test-http2-ping.js
+++ b/test/parallel/test-http2-ping.js
@@ -80,7 +80,7 @@ server.listen(0, common.mustCall(() => {
     const req = client.request();
     req.resume();
     req.on('end', common.mustCall(() => {
-      client.destroy();
+      client.close();
       server.close();
     }));
   }));
diff --git a/test/parallel/test-http2-pipe.js b/test/parallel/test-http2-pipe.js
index a8e188a58a2d4d..2a759f9848721b 100644
--- a/test/parallel/test-http2-pipe.js
+++ b/test/parallel/test-http2-pipe.js
@@ -8,7 +8,6 @@ const assert = require('assert');
 const http2 = require('http2');
 const fs = require('fs');
 const path = require('path');
-const Countdown = require('../common/countdown');
 
 // piping should work as expected with createWriteStream
 
@@ -21,28 +20,28 @@ const server = http2.createServer();
 
 server.on('stream', common.mustCall((stream) => {
   const dest = stream.pipe(fs.createWriteStream(fn));
-  dest.on('finish', common.mustCall(() => {
-    assert.strictEqual(fs.readFileSync(loc).length, fs.readFileSync(fn).length);
-    fs.unlinkSync(fn);
-    stream.respond();
-    stream.end();
-  }));
+
+  dest.on('finish', () => {
+    assert.strictEqual(fs.readFileSync(loc).length,
+                       fs.readFileSync(fn).length);
+  });
+  stream.respond();
+  stream.end();
 }));
 
 server.listen(0, common.mustCall(() => {
-  const port = server.address().port;
-  const client = http2.connect(`http://localhost:${port}`);
-
-  const countdown = new Countdown(2, common.mustCall(() => {
-    server.close();
-    client.destroy();
-  }));
+  const client = http2.connect(`http://localhost:${server.address().port}`);
 
   const req = client.request({ ':method': 'POST' });
   req.on('response', common.mustCall());
   req.resume();
-  req.on('end', common.mustCall(() => countdown.dec()));
+
+  req.on('close', common.mustCall(() => {
+    server.close();
+    client.close();
+  }));
+
   const str = fs.createReadStream(loc);
-  str.on('end', common.mustCall(() => countdown.dec()));
+  str.on('end', common.mustCall());
   str.pipe(req);
 }));
diff --git a/test/parallel/test-http2-priority-event.js b/test/parallel/test-http2-priority-event.js
index b0704902d31101..fe04ffb342d70d 100644
--- a/test/parallel/test-http2-priority-event.js
+++ b/test/parallel/test-http2-priority-event.js
@@ -54,7 +54,7 @@ server.on('listening', common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 
diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js
index 45dbe8530e9018..629fec4fa684d2 100644
--- a/test/parallel/test-http2-respond-errors.js
+++ b/test/parallel/test-http2-respond-errors.js
@@ -75,13 +75,18 @@ function runTest(test) {
 
   const client = http2.connect(url);
   const req = client.request(headers);
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 2'
+  }));
 
   currentError = test;
   req.resume();
   req.end();
 
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
 
     if (!tests.length) {
       server.close();
diff --git a/test/parallel/test-http2-respond-file-204.js b/test/parallel/test-http2-respond-file-204.js
index 8181dbb317dab2..1171866e9373ab 100644
--- a/test/parallel/test-http2-respond-file-204.js
+++ b/test/parallel/test-http2-respond-file-204.js
@@ -35,7 +35,7 @@ server.listen(0, () => {
   req.on('response', common.mustCall());
   req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-304.js b/test/parallel/test-http2-respond-file-304.js
index e6e0842c7f9448..536c48c624e73c 100644
--- a/test/parallel/test-http2-respond-file-304.js
+++ b/test/parallel/test-http2-respond-file-304.js
@@ -38,7 +38,7 @@ server.listen(0, () => {
 
   req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-404.js b/test/parallel/test-http2-respond-file-404.js
index ba62f384485bc0..60bc21f185dd5c 100644
--- a/test/parallel/test-http2-respond-file-404.js
+++ b/test/parallel/test-http2-respond-file-404.js
@@ -40,7 +40,7 @@ server.listen(0, () => {
   }));
   req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-compat.js b/test/parallel/test-http2-respond-file-compat.js
index 0f6e3199d68ab2..0205f2d0d85aaf 100644
--- a/test/parallel/test-http2-respond-file-compat.js
+++ b/test/parallel/test-http2-respond-file-compat.js
@@ -16,7 +16,7 @@ server.listen(0, () => {
   const req = client.request();
   req.on('response', common.mustCall());
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-error-dir.js b/test/parallel/test-http2-respond-file-error-dir.js
index 18a9540451f865..6818616227df89 100644
--- a/test/parallel/test-http2-respond-file-error-dir.js
+++ b/test/parallel/test-http2-respond-file-error-dir.js
@@ -6,14 +6,10 @@ if (!common.hasCrypto)
 const http2 = require('http2');
 const assert = require('assert');
 
-const {
-  HTTP2_HEADER_CONTENT_TYPE
-} = http2.constants;
-
 const server = http2.createServer();
 server.on('stream', (stream) => {
   stream.respondWithFile(process.cwd(), {
-    [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+    'content-type': 'text/plain'
   }, {
     onError(err) {
       common.expectsError({
@@ -38,7 +34,7 @@ server.listen(0, () => {
   }));
   req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-errors.js b/test/parallel/test-http2-respond-file-errors.js
index c2c749873c82ac..83d3900bc5c288 100644
--- a/test/parallel/test-http2-respond-file-errors.js
+++ b/test/parallel/test-http2-respond-file-errors.js
@@ -6,11 +6,6 @@ if (!common.hasCrypto)
 const fixtures = require('../common/fixtures');
 const http2 = require('http2');
 
-const {
-  HTTP2_HEADER_CONTENT_TYPE,
-  HTTP2_HEADER_METHOD
-} = http2.constants;
-
 const optionsWithTypeError = {
   offset: 'number',
   length: 'number',
@@ -33,6 +28,7 @@ const fname = fixtures.path('elipses.txt');
 const server = http2.createServer();
 
 server.on('stream', common.mustCall((stream) => {
+
   // Check for all possible TypeError triggers on options
   Object.keys(optionsWithTypeError).forEach((option) => {
     Object.keys(types).forEach((type) => {
@@ -42,7 +38,7 @@ server.on('stream', common.mustCall((stream) => {
 
       common.expectsError(
         () => stream.respondWithFile(fname, {
-          [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+          'content-type': 'text/plain'
         }, {
           [option]: types[type]
         }),
@@ -59,7 +55,7 @@ server.on('stream', common.mustCall((stream) => {
   // Should throw if :status 204, 205 or 304
   [204, 205, 304].forEach((status) => common.expectsError(
     () => stream.respondWithFile(fname, {
-      [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
+      'content-type': 'text/plain',
       ':status': status,
     }),
     {
@@ -68,31 +64,11 @@ server.on('stream', common.mustCall((stream) => {
     }
   ));
 
-  // should emit an error on the stream if headers aren't valid
-  stream.respondWithFile(fname, {
-    [HTTP2_HEADER_METHOD]: 'POST'
-  }, {
-    statCheck: common.mustCall(() => {
-      // give time to the current test case to finish
-      process.nextTick(continueTest, stream);
-      return true;
-    })
-  });
-  stream.once('error', common.expectsError({
-    code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
-    type: Error,
-    message: '":method" is an invalid pseudoheader or is used incorrectly'
-  }));
-}));
-
-function continueTest(stream) {
   // Should throw if headers already sent
-  stream.respond({
-    ':status': 200,
-  });
+  stream.respond({ ':status': 200 });
   common.expectsError(
     () => stream.respondWithFile(fname, {
-      [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+      'content-type': 'text/plain'
     }),
     {
       code: 'ERR_HTTP2_HEADERS_SENT',
@@ -104,21 +80,21 @@ function continueTest(stream) {
   stream.destroy();
   common.expectsError(
     () => stream.respondWithFile(fname, {
-      [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+      'content-type': 'text/plain'
     }),
     {
       code: 'ERR_HTTP2_INVALID_STREAM',
       message: 'The stream has been destroyed'
     }
   );
-}
+}));
 
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
   const req = client.request();
 
   req.on('close', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-fd-errors.js b/test/parallel/test-http2-respond-file-fd-errors.js
index 9458b2f49af087..44876b60e1c4cb 100644
--- a/test/parallel/test-http2-respond-file-fd-errors.js
+++ b/test/parallel/test-http2-respond-file-fd-errors.js
@@ -7,11 +7,6 @@ const fixtures = require('../common/fixtures');
 const http2 = require('http2');
 const fs = require('fs');
 
-const {
-  HTTP2_HEADER_CONTENT_TYPE,
-  HTTP2_HEADER_METHOD
-} = http2.constants;
-
 const optionsWithTypeError = {
   offset: 'number',
   length: 'number',
@@ -43,7 +38,7 @@ server.on('stream', common.mustCall((stream) => {
 
     common.expectsError(
       () => stream.respondWithFD(types[type], {
-        [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+        'content-type': 'text/plain'
       }),
       {
         type: TypeError,
@@ -62,7 +57,7 @@ server.on('stream', common.mustCall((stream) => {
 
       common.expectsError(
         () => stream.respondWithFD(fd, {
-          [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+          'content-type': 'text/plain'
         }, {
           [option]: types[type]
         }),
@@ -79,7 +74,7 @@ server.on('stream', common.mustCall((stream) => {
   // Should throw if :status 204, 205 or 304
   [204, 205, 304].forEach((status) => common.expectsError(
     () => stream.respondWithFD(fd, {
-      [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
+      'content-type': 'text/plain',
       ':status': status,
     }),
     {
@@ -89,35 +84,11 @@ server.on('stream', common.mustCall((stream) => {
     }
   ));
 
-  // should emit an error on the stream if headers aren't valid
-  stream.respondWithFD(fd, {
-    [HTTP2_HEADER_METHOD]: 'POST'
-  }, {
-    statCheck() {
-      return true;
-    }
-  });
-  stream.once('error', common.expectsError({
-    code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
-    type: Error,
-    message: '":method" is an invalid pseudoheader or is used incorrectly'
-  }));
-  stream.respondWithFD(fd, {
-    [HTTP2_HEADER_METHOD]: 'POST'
-  });
-  stream.once('error', common.expectsError({
-    code: 'ERR_HTTP2_INVALID_PSEUDOHEADER',
-    type: Error,
-    message: '":method" is an invalid pseudoheader or is used incorrectly'
-  }));
-
   // Should throw if headers already sent
-  stream.respond({
-    ':status': 200,
-  });
+  stream.respond();
   common.expectsError(
     () => stream.respondWithFD(fd, {
-      [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+      'content-type': 'text/plain'
     }),
     {
       code: 'ERR_HTTP2_HEADERS_SENT',
@@ -130,7 +101,7 @@ server.on('stream', common.mustCall((stream) => {
   stream.destroy();
   common.expectsError(
     () => stream.respondWithFD(fd, {
-      [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain'
+      'content-type': 'text/plain'
     }),
     {
       code: 'ERR_HTTP2_INVALID_STREAM',
@@ -145,7 +116,7 @@ server.listen(0, common.mustCall(() => {
   const req = client.request();
 
   req.on('close', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-fd-invalid.js b/test/parallel/test-http2-respond-file-fd-invalid.js
index f3bcab8904bee4..77a4d3df00d0d6 100644
--- a/test/parallel/test-http2-respond-file-fd-invalid.js
+++ b/test/parallel/test-http2-respond-file-fd-invalid.js
@@ -31,7 +31,7 @@ server.listen(0, () => {
   req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => {
     assert.strictEqual(req.rstCode, NGHTTP2_INTERNAL_ERROR);
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-fd-range.js b/test/parallel/test-http2-respond-file-fd-range.js
index 8479dca518558e..2dd73e0001544c 100644
--- a/test/parallel/test-http2-respond-file-fd-range.js
+++ b/test/parallel/test-http2-respond-file-fd-range.js
@@ -9,6 +9,7 @@ const fixtures = require('../common/fixtures');
 const http2 = require('http2');
 const assert = require('assert');
 const fs = require('fs');
+const Countdown = require('../common/countdown');
 
 const {
   HTTP2_HEADER_CONTENT_TYPE,
@@ -39,7 +40,7 @@ server.on('stream', (stream, headers) => {
     statCheck: common.mustCall((stat, headers, options) => {
       assert.strictEqual(options.length, length);
       assert.strictEqual(options.offset, offset);
-      headers[HTTP2_HEADER_CONTENT_LENGTH] =
+      headers['content-length'] =
         Math.min(options.length, stat.size - offset);
     }),
     offset: offset,
@@ -47,23 +48,21 @@ server.on('stream', (stream, headers) => {
   });
 });
 server.on('close', common.mustCall(() => fs.closeSync(fd)));
+
 server.listen(0, () => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
 
-  let remaining = 2;
-  function maybeClose() {
-    if (--remaining === 0) {
-      client.destroy();
-      server.close();
-    }
-  }
+  const countdown = new Countdown(2, () => {
+    client.close();
+    server.close();
+  });
 
   {
     const req = client.request({ range: 'bytes=8-11' });
 
     req.on('response', common.mustCall((headers) => {
-      assert.strictEqual(headers[HTTP2_HEADER_CONTENT_TYPE], 'text/plain');
-      assert.strictEqual(+headers[HTTP2_HEADER_CONTENT_LENGTH], 3);
+      assert.strictEqual(headers['content-type'], 'text/plain');
+      assert.strictEqual(+headers['content-length'], 3);
     }));
     req.setEncoding('utf8');
     let check = '';
@@ -71,7 +70,7 @@ server.listen(0, () => {
     req.on('end', common.mustCall(() => {
       assert.strictEqual(check, data.toString('utf8', 8, 11));
     }));
-    req.on('close', common.mustCall(maybeClose));
+    req.on('close', common.mustCall(() => countdown.dec()));
     req.end();
   }
 
@@ -88,7 +87,7 @@ server.listen(0, () => {
     req.on('end', common.mustCall(() => {
       assert.strictEqual(check, data.toString('utf8', 8, 28));
     }));
-    req.on('close', common.mustCall(maybeClose));
+    req.on('close', common.mustCall(() => countdown.dec()));
     req.end();
   }
 
diff --git a/test/parallel/test-http2-respond-file-fd.js b/test/parallel/test-http2-respond-file-fd.js
index 303d25be3f2b66..7d4395bbc360aa 100644
--- a/test/parallel/test-http2-respond-file-fd.js
+++ b/test/parallel/test-http2-respond-file-fd.js
@@ -40,7 +40,7 @@ server.listen(0, () => {
   req.on('data', (chunk) => check += chunk);
   req.on('end', common.mustCall(() => {
     assert.strictEqual(check, data.toString('utf8'));
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file-push.js b/test/parallel/test-http2-respond-file-push.js
index 4f7b179faf81a8..a5229beb07d1a7 100644
--- a/test/parallel/test-http2-respond-file-push.js
+++ b/test/parallel/test-http2-respond-file-push.js
@@ -29,7 +29,8 @@ server.on('stream', (stream) => {
   stream.pushStream({
     ':path': '/file.txt',
     ':method': 'GET'
-  }, (stream) => {
+  }, (err, stream) => {
+    assert.ifError(err);
     stream.respondWithFD(fd, {
       [HTTP2_HEADER_CONTENT_TYPE]: 'text/plain',
       [HTTP2_HEADER_CONTENT_LENGTH]: stat.size,
@@ -50,7 +51,7 @@ server.listen(0, () => {
   function maybeClose() {
     if (--expected === 0) {
       server.close();
-      client.destroy();
+      client.close();
     }
   }
 
diff --git a/test/parallel/test-http2-respond-file-range.js b/test/parallel/test-http2-respond-file-range.js
index a5995cbba77c1c..4e6a6074514f14 100644
--- a/test/parallel/test-http2-respond-file-range.js
+++ b/test/parallel/test-http2-respond-file-range.js
@@ -46,7 +46,7 @@ server.listen(0, () => {
   req.on('data', (chunk) => check += chunk);
   req.on('end', common.mustCall(() => {
     assert.strictEqual(check, data.toString('utf8', 8, 11));
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-file.js b/test/parallel/test-http2-respond-file.js
index c2f513b7cae2b7..9ad8e7a69648dc 100644
--- a/test/parallel/test-http2-respond-file.js
+++ b/test/parallel/test-http2-respond-file.js
@@ -45,7 +45,7 @@ server.listen(0, () => {
   req.on('data', (chunk) => check += chunk);
   req.on('end', common.mustCall(() => {
     assert.strictEqual(check, data.toString('utf8'));
-    client.destroy();
+    client.close();
     server.close();
   }));
   req.end();
diff --git a/test/parallel/test-http2-respond-no-data.js b/test/parallel/test-http2-respond-no-data.js
index d891fe4e8ddd2b..9572bdffe54927 100644
--- a/test/parallel/test-http2-respond-no-data.js
+++ b/test/parallel/test-http2-respond-no-data.js
@@ -27,7 +27,7 @@ function makeRequest() {
   req.resume();
 
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
 
     if (!status.length) {
       server.close();
diff --git a/test/parallel/test-http2-respond-with-fd-errors.js b/test/parallel/test-http2-respond-with-fd-errors.js
index 1d32a2f45c28bc..b7ff09225b6202 100644
--- a/test/parallel/test-http2-respond-with-fd-errors.js
+++ b/test/parallel/test-http2-respond-with-fd-errors.js
@@ -83,12 +83,18 @@ function runTest(test) {
   const client = http2.connect(url);
   const req = client.request(headers);
 
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 2'
+  }));
+
   currentError = test;
   req.resume();
   req.end();
 
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
 
     if (!tests.length) {
       server.close();
diff --git a/test/parallel/test-http2-response-splitting.js b/test/parallel/test-http2-response-splitting.js
index 1d9b616105f450..9613eca9636ae4 100644
--- a/test/parallel/test-http2-response-splitting.js
+++ b/test/parallel/test-http2-response-splitting.js
@@ -55,7 +55,7 @@ server.listen(0, common.mustCall(() => {
   function maybeClose() {
     if (remaining === 0) {
       server.close();
-      client.destroy();
+      client.close();
     }
   }
 
diff --git a/test/parallel/test-http2-rststream-errors.js b/test/parallel/test-http2-rststream-errors.js
deleted file mode 100644
index eacf7855117503..00000000000000
--- a/test/parallel/test-http2-rststream-errors.js
+++ /dev/null
@@ -1,95 +0,0 @@
-// Flags: --expose-http2
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
-const http2 = require('http2');
-const {
-  constants,
-  Http2Stream,
-  nghttp2ErrorString
-} = process.binding('http2');
-
-// tests error handling within rstStream
-// - every other NGHTTP2 error from binding (should emit stream error)
-
-const specificTestKeys = [];
-const specificTests = [];
-
-const genericTests = Object.getOwnPropertyNames(constants)
-  .filter((key) => (
-    key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
-  ))
-  .map((key) => ({
-    ngError: constants[key],
-    error: {
-      code: 'ERR_HTTP2_ERROR',
-      type: Error,
-      message: nghttp2ErrorString(constants[key])
-    },
-    type: 'stream'
-  }));
-
-
-const tests = specificTests.concat(genericTests);
-
-let currentError;
-
-// mock submitRstStream because we only care about testing error handling
-Http2Stream.prototype.rstStream = () => currentError.ngError;
-
-const server = http2.createServer();
-server.on('stream', common.mustCall((stream, headers) => {
-  const errorMustCall = common.expectsError(currentError.error);
-  const errorMustNotCall = common.mustNotCall(
-    `${currentError.error.code} should emit on ${currentError.type}`
-  );
-
-  if (currentError.type === 'stream') {
-    stream.session.on('error', errorMustNotCall);
-    stream.on('error', errorMustCall);
-    stream.on('error', common.mustCall(() => {
-      stream.session.destroy();
-    }));
-  } else {
-    stream.session.once('error', errorMustCall);
-    stream.on('error', errorMustNotCall);
-  }
-
-  stream.rstStream();
-}, tests.length));
-
-server.listen(0, common.mustCall(() => runTest(tests.shift())));
-
-function runTest(test) {
-  const port = server.address().port;
-  const url = `http://localhost:${port}`;
-  const headers = {
-    ':path': '/',
-    ':method': 'POST',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  };
-
-  const client = http2.connect(url);
-  const req = client.request(headers);
-
-  currentError = test;
-  req.resume();
-  req.end();
-
-  if (currentError.type === 'stream') {
-    req.on('error', common.mustCall());
-  }
-
-  req.on('end', common.mustCall(() => {
-    client.destroy();
-
-    if (!tests.length) {
-      server.close();
-    } else {
-      runTest(tests.shift());
-    }
-  }));
-}
diff --git a/test/parallel/test-http2-serve-file.js b/test/parallel/test-http2-serve-file.js
index af82360e464b31..7b73fe639e0cc5 100644
--- a/test/parallel/test-http2-serve-file.js
+++ b/test/parallel/test-http2-serve-file.js
@@ -48,7 +48,7 @@ server.listen(0, () => {
   let remaining = 2;
   function maybeClose() {
     if (--remaining === 0) {
-      client.destroy();
+      client.close();
       server.close();
     }
   }
diff --git a/test/parallel/test-http2-server-errors.js b/test/parallel/test-http2-server-errors.js
index 7d7db6a24538fd..a3586bd64d46e7 100644
--- a/test/parallel/test-http2-server-errors.js
+++ b/test/parallel/test-http2-server-errors.js
@@ -6,7 +6,6 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const h2 = require('http2');
-const { Http2Stream } = require('internal/http2/core');
 
 // Errors should not be reported both in Http2ServerRequest
 // and Http2ServerResponse
@@ -29,11 +28,6 @@ const { Http2Stream } = require('internal/http2/core');
     server.close();
   }));
 
-  server.on('streamError', common.mustCall(function(err, stream) {
-    assert.strictEqual(err, expected);
-    assert.strictEqual(stream instanceof Http2Stream, true);
-  }));
-
   server.listen(0, common.mustCall(function() {
     const port = server.address().port;
 
@@ -70,11 +64,6 @@ const { Http2Stream } = require('internal/http2/core');
     server.close();
   }));
 
-  server.on('streamError', common.mustCall(function(err, stream) {
-    assert.strictEqual(err, expected);
-    assert.strictEqual(stream instanceof Http2Stream, true);
-  }));
-
   server.listen(0, common.mustCall(function() {
     const port = server.address().port;
 
diff --git a/test/parallel/test-http2-server-http1-client.js b/test/parallel/test-http2-server-http1-client.js
index ef3a79c0fd143a..34a8f48b5e130d 100644
--- a/test/parallel/test-http2-server-http1-client.js
+++ b/test/parallel/test-http2-server-http1-client.js
@@ -12,11 +12,14 @@ const server = http2.createServer();
 server.on('stream', common.mustNotCall());
 server.on('session', common.mustCall((session) => {
   session.on('close', common.mustCall());
+  session.on('error', common.expectsError({
+    code: 'ERR_HTTP2_ERROR',
+    type: Error,
+    message: 'Received bad client magic byte string'
+  }));
 }));
 
 server.listen(0, common.mustCall(() => {
   const req = http.get(`http://localhost:${server.address().port}`);
-  req.on('error', (error) => {
-    server.close();
-  });
+  req.on('error', (error) => server.close());
 }));
diff --git a/test/parallel/test-http2-server-push-disabled.js b/test/parallel/test-http2-server-push-disabled.js
index cd1516f43ba252..eef8194c57e806 100644
--- a/test/parallel/test-http2-server-push-disabled.js
+++ b/test/parallel/test-http2-server-push-disabled.js
@@ -48,7 +48,7 @@ server.listen(0, common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 }));
diff --git a/test/parallel/test-http2-server-push-stream-errors-args.js b/test/parallel/test-http2-server-push-stream-errors-args.js
index f8bd28137c3368..bea187baf31cdc 100644
--- a/test/parallel/test-http2-server-push-stream-errors-args.js
+++ b/test/parallel/test-http2-server-push-stream-errors-args.js
@@ -50,7 +50,7 @@ server.listen(0, common.mustCall(() => {
   req.on('end', common.mustCall(() => {
     assert.strictEqual(data, 'test');
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 }));
diff --git a/test/parallel/test-http2-server-push-stream-errors.js b/test/parallel/test-http2-server-push-stream-errors.js
index 56e329dcff1cd2..7eaf4dc94d15e2 100644
--- a/test/parallel/test-http2-server-push-stream-errors.js
+++ b/test/parallel/test-http2-server-push-stream-errors.js
@@ -34,9 +34,8 @@ const specificTests = [
   {
     ngError: constants.NGHTTP2_ERR_STREAM_CLOSED,
     error: {
-      code: 'ERR_HTTP2_STREAM_CLOSED',
-      type: Error,
-      message: 'The stream is already closed'
+      code: 'ERR_HTTP2_INVALID_STREAM',
+      type: Error
     },
     type: 'stream'
   },
@@ -66,47 +65,25 @@ Http2Stream.prototype.pushPromise = () => currentError.ngError;
 
 const server = http2.createServer();
 server.on('stream', common.mustCall((stream, headers) => {
-  const errorMustCall = common.expectsError(currentError.error);
-  const errorMustNotCall = common.mustNotCall(
-    `${currentError.error.code} should emit on ${currentError.type}`
-  );
-
-  if (currentError.type === 'stream') {
-    stream.session.on('error', errorMustNotCall);
-    stream.on('error', errorMustCall);
-    stream.on('error', common.mustCall(() => {
-      stream.respond();
-      stream.end();
-    }));
-  } else {
-    stream.session.once('error', errorMustCall);
-    stream.on('error', errorMustNotCall);
-  }
-
-  stream.pushStream({}, () => {});
+  stream.pushStream({}, common.expectsError(currentError.error));
+  stream.respond();
+  stream.end();
 }, tests.length));
 
 server.listen(0, common.mustCall(() => runTest(tests.shift())));
 
 function runTest(test) {
-  const port = server.address().port;
-  const url = `http://localhost:${port}`;
-  const headers = {
-    ':path': '/',
-    ':method': 'POST',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  };
+  const url = `http://localhost:${server.address().port}`;
 
   const client = http2.connect(url);
-  const req = client.request(headers);
+  const req = client.request();
 
   currentError = test;
   req.resume();
   req.end();
 
-  req.on('end', common.mustCall(() => {
-    client.destroy();
+  req.on('close', common.mustCall(() => {
+    client.close();
 
     if (!tests.length) {
       server.close();
diff --git a/test/parallel/test-http2-server-push-stream-head.js b/test/parallel/test-http2-server-push-stream-head.js
index c2fc8db4a92eba..cd2276746f4bdd 100644
--- a/test/parallel/test-http2-server-push-stream-head.js
+++ b/test/parallel/test-http2-server-push-stream-head.js
@@ -5,6 +5,7 @@ if (!common.hasCrypto)
   common.skip('missing crypto');
 const assert = require('assert');
 const http2 = require('http2');
+const Countdown = require('../common/countdown');
 
 // Check that pushStream handles method HEAD correctly
 // - stream should end immediately (no body)
@@ -17,8 +18,10 @@ server.on('stream', common.mustCall((stream, headers) => {
       ':scheme': 'http',
       ':method': 'HEAD',
       ':authority': `localhost:${port}`,
-    }, common.mustCall((push, headers) => {
+    }, common.mustCall((err, push, headers) => {
       assert.strictEqual(push._writableState.ended, true);
+      push.respond();
+      assert(!push.write('test'));
       stream.end('test');
     }));
   }
@@ -30,15 +33,26 @@ server.on('stream', common.mustCall((stream, headers) => {
 
 server.listen(0, common.mustCall(() => {
   const port = server.address().port;
-  const headers = { ':path': '/' };
   const client = http2.connect(`http://localhost:${port}`);
-  const req = client.request(headers);
+
+  const countdown = new Countdown(2, () => {
+    server.close();
+    client.close();
+  });
+
+  const req = client.request();
   req.setEncoding('utf8');
 
   client.on('stream', common.mustCall((stream, headers) => {
+    assert.strictEqual(headers[':method'], 'HEAD');
     assert.strictEqual(headers[':scheme'], 'http');
     assert.strictEqual(headers[':path'], '/');
     assert.strictEqual(headers[':authority'], `localhost:${port}`);
+    stream.on('push', common.mustCall(() => {
+      stream.on('data', common.mustNotCall());
+      stream.on('end', common.mustCall());
+    }));
+    stream.on('close', common.mustCall(() => countdown.dec()));
   }));
 
   let data = '';
@@ -46,8 +60,7 @@ server.listen(0, common.mustCall(() => {
   req.on('data', common.mustCall((d) => data += d));
   req.on('end', common.mustCall(() => {
     assert.strictEqual(data, 'test');
-    server.close();
-    client.destroy();
   }));
+  req.on('close', common.mustCall(() => countdown.dec()));
   req.end();
 }));
diff --git a/test/parallel/test-http2-server-push-stream.js b/test/parallel/test-http2-server-push-stream.js
index 395743869198ca..6ac10cae77f951 100644
--- a/test/parallel/test-http2-server-push-stream.js
+++ b/test/parallel/test-http2-server-push-stream.js
@@ -14,7 +14,8 @@ server.on('stream', common.mustCall((stream, headers) => {
       ':scheme': 'http',
       ':path': '/foobar',
       ':authority': `localhost:${port}`,
-    }, common.mustCall((push, headers) => {
+    }, common.mustCall((err, push, headers) => {
+      assert.ifError(err);
       push.respond({
         'content-type': 'text/html',
         ':status': 200,
@@ -53,7 +54,7 @@ server.listen(0, common.mustCall(() => {
   req.on('end', common.mustCall(() => {
     assert.strictEqual(data, 'test');
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 }));
diff --git a/test/parallel/test-http2-server-rst-before-respond.js b/test/parallel/test-http2-server-rst-before-respond.js
index 47ba68bd29ed81..2cdea07a168194 100644
--- a/test/parallel/test-http2-server-rst-before-respond.js
+++ b/test/parallel/test-http2-server-rst-before-respond.js
@@ -12,7 +12,7 @@ const server = h2.createServer();
 server.on('stream', common.mustCall(onStream));
 
 function onStream(stream, headers, flags) {
-  stream.rstStream();
+  stream.close();
 
   assert.throws(() => {
     stream.additionalHeaders({
@@ -28,19 +28,13 @@ function onStream(stream, headers, flags) {
 server.listen(0);
 
 server.on('listening', common.mustCall(() => {
-
   const client = h2.connect(`http://localhost:${server.address().port}`);
-
-  const req = client.request({ ':path': '/' });
-
+  const req = client.request();
   req.on('headers', common.mustNotCall());
-
   req.on('close', common.mustCall((code) => {
     assert.strictEqual(h2.constants.NGHTTP2_NO_ERROR, code);
     server.close();
-    client.destroy();
+    client.close();
   }));
-
   req.on('response', common.mustNotCall());
-
 }));
diff --git a/test/parallel/test-http2-server-rst-stream.js b/test/parallel/test-http2-server-rst-stream.js
index 4b04f29c8ec7c0..c2d938c22f4483 100644
--- a/test/parallel/test-http2-server-rst-stream.js
+++ b/test/parallel/test-http2-server-rst-stream.js
@@ -16,39 +16,38 @@ const {
 } = http2.constants;
 
 const tests = [
-  ['rstStream', NGHTTP2_NO_ERROR, false],
-  ['rstWithNoError', NGHTTP2_NO_ERROR, false],
-  ['rstWithProtocolError', NGHTTP2_PROTOCOL_ERROR, true],
-  ['rstWithCancel', NGHTTP2_CANCEL, false],
-  ['rstWithRefuse', NGHTTP2_REFUSED_STREAM, true],
-  ['rstWithInternalError', NGHTTP2_INTERNAL_ERROR, true]
+  [NGHTTP2_NO_ERROR, false],
+  [NGHTTP2_NO_ERROR, false],
+  [NGHTTP2_PROTOCOL_ERROR, true],
+  [NGHTTP2_CANCEL, false],
+  [NGHTTP2_REFUSED_STREAM, true],
+  [NGHTTP2_INTERNAL_ERROR, true]
 ];
 
 const server = http2.createServer();
 server.on('stream', (stream, headers) => {
-  const method = headers['rstmethod'];
-  stream[method]();
+  stream.close(headers['rstcode'] | 0);
 });
 
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
 
   const countdown = new Countdown(tests.length, common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
 
   tests.forEach((test) => {
     const req = client.request({
       ':method': 'POST',
-      rstmethod: test[0]
+      rstcode: test[0]
     });
     req.on('close', common.mustCall((code) => {
-      assert.strictEqual(code, test[1]);
+      assert.strictEqual(code, test[0]);
       countdown.dec();
     }));
     req.on('aborted', common.mustCall());
-    if (test[2])
+    if (test[1])
       req.on('error', common.mustCall());
     else
       req.on('error', common.mustNotCall());
diff --git a/test/parallel/test-http2-server-sessionerror.js b/test/parallel/test-http2-server-sessionerror.js
new file mode 100644
index 00000000000000..525eb2e6efd11a
--- /dev/null
+++ b/test/parallel/test-http2-server-sessionerror.js
@@ -0,0 +1,48 @@
+// Flags: --expose-internals
+
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const http2 = require('http2');
+const { kSocket } = require('internal/http2/util');
+
+const server = http2.createServer();
+server.on('stream', common.mustNotCall());
+
+let test = 0;
+
+server.on('session', common.mustCall((session) => {
+  switch (++test) {
+    case 1:
+      server.on('error', common.mustNotCall());
+      session.on('error', common.expectsError({
+        type: Error,
+        message: 'test'
+      }));
+      session[kSocket].emit('error', new Error('test'));
+      break;
+    case 2:
+      // If the server does not have a socketError listener,
+      // error will be silent on the server but will close
+      // the session
+      session[kSocket].emit('error', new Error('test'));
+      break;
+  }
+}, 2));
+
+server.listen(0, common.mustCall(() => {
+  const url = `http://localhost:${server.address().port}`;
+  http2.connect(url)
+    // An ECONNRESET error may occur depending on the platform (due largely
+    // to differences in the timing of socket closing). Do not wrap this in
+    // a common must call.
+    .on('error', () => {})
+    .on('close', () => {
+      server.removeAllListeners('error');
+      http2.connect(url)
+        .on('error', () => {})
+        .on('close', () => server.close());
+    });
+}));
diff --git a/test/parallel/test-http2-server-set-header.js b/test/parallel/test-http2-server-set-header.js
index ed27638f6849f4..4b6228053f8ece 100644
--- a/test/parallel/test-http2-server-set-header.js
+++ b/test/parallel/test-http2-server-set-header.js
@@ -29,7 +29,7 @@ server.listen(0, common.mustCall(() => {
   req.on('end', () => {
     assert.strictEqual(body, data);
     server.close();
-    client.destroy();
+    client.close();
   });
   req.end();
 }));
diff --git a/test/parallel/test-http2-server-shutdown-before-respond.js b/test/parallel/test-http2-server-shutdown-before-respond.js
index c3ad9714b5f39b..33f224fc69a9d5 100644
--- a/test/parallel/test-http2-server-shutdown-before-respond.js
+++ b/test/parallel/test-http2-server-shutdown-before-respond.js
@@ -11,24 +11,26 @@ const server = h2.createServer();
 server.on('stream', common.mustCall(onStream));
 
 function onStream(stream, headers, flags) {
-  const session = stream.session;
-  stream.session.shutdown({ graceful: true }, common.mustCall(() => {
-    session.destroy();
-  }));
-  stream.respond({});
+  stream.session.goaway(1);
+  stream.respond();
   stream.end('data');
 }
 
 server.listen(0);
 
 server.on('listening', common.mustCall(() => {
-
   const client = h2.connect(`http://localhost:${server.address().port}`);
 
   client.on('goaway', common.mustCall());
+  client.on('error', common.expectsError({
+    code: 'ERR_HTTP2_SESSION_ERROR'
+  }));
 
   const req = client.request();
-
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_SESSION_ERROR'
+  }));
   req.resume();
+  req.on('data', common.mustNotCall());
   req.on('end', common.mustCall(() => server.close()));
 }));
diff --git a/test/parallel/test-http2-server-shutdown-options-errors.js b/test/parallel/test-http2-server-shutdown-options-errors.js
index 673723e961c87d..2aedec1140701a 100644
--- a/test/parallel/test-http2-server-shutdown-options-errors.js
+++ b/test/parallel/test-http2-server-shutdown-options-errors.js
@@ -8,55 +8,63 @@ const http2 = require('http2');
 
 const server = http2.createServer();
 
-const optionsToTest = {
-  opaqueData: 'Uint8Array',
-  graceful: 'boolean',
-  errorCode: 'number',
-  lastStreamID: 'number'
-};
+const types = [
+  true,
+  {},
+  [],
+  null,
+  new Date()
+];
 
-const types = {
-  boolean: true,
-  number: 1,
-  object: {},
-  array: [],
-  null: null,
-  Uint8Array: Buffer.from([0x1, 0x2, 0x3, 0x4, 0x5])
-};
+server.on('stream', common.mustCall((stream) => {
+  const session = stream.session;
 
-server.on(
-  'stream',
-  common.mustCall((stream) => {
-    Object.keys(optionsToTest).forEach((option) => {
-      Object.keys(types).forEach((type) => {
-        if (type === optionsToTest[option]) {
-          return;
-        }
-        common.expectsError(
-          () =>
-            stream.session.shutdown(
-              { [option]: types[type] },
-              common.mustNotCall()
-            ),
-          {
-            type: TypeError,
-            code: 'ERR_INVALID_OPT_VALUE',
-            message: `The value "${String(types[type])}" is invalid ` +
-            `for option "${option}"`
-          }
-        );
-      });
-    });
-    stream.session.destroy();
-  })
-);
+  types.forEach((i) => {
+    common.expectsError(
+      () => session.goaway(i),
+      {
+        code: 'ERR_INVALID_ARG_TYPE',
+        type: TypeError,
+        message: 'The "code" argument must be of type number'
+      }
+    );
+    common.expectsError(
+      () => session.goaway(0, i),
+      {
+        code: 'ERR_INVALID_ARG_TYPE',
+        type: TypeError,
+        message: 'The "lastStreamID" argument must be of type number'
+      }
+    );
+    common.expectsError(
+      () => session.goaway(0, 0, i),
+      {
+        code: 'ERR_INVALID_ARG_TYPE',
+        type: TypeError,
+        message: 'The "opaqueData" argument must be one of type Buffer, ' +
+                 'TypedArray, or DataView'
+      }
+    );
+  });
+
+  stream.session.destroy();
+}));
 
 server.listen(
   0,
   common.mustCall(() => {
     const client = http2.connect(`http://localhost:${server.address().port}`);
+    // On certain operating systems, an ECONNRESET may occur. We do not need
+    // to test for it here. Do not make this a mustCall
+    client.on('error', () => {});
     const req = client.request();
+    // On certain operating systems, an ECONNRESET may occur. We do not need
+    // to test for it here. Do not make this a mustCall
+    req.on('error', () => {});
     req.resume();
-    req.on('end', common.mustCall(() => server.close()));
+    req.on('close', common.mustCall(() => {
+      client.close();
+      server.close();
+    }));
   })
 );
diff --git a/test/parallel/test-http2-server-shutdown-redundant.js b/test/parallel/test-http2-server-shutdown-redundant.js
index 6740728a06343d..ac0893cd46eab6 100644
--- a/test/parallel/test-http2-server-shutdown-redundant.js
+++ b/test/parallel/test-http2-server-shutdown-redundant.js
@@ -4,27 +4,38 @@
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
-const assert = require('assert');
 const http2 = require('http2');
 
 const server = http2.createServer();
 
-// Test blank return when a stream.session.shutdown is called twice
-// Also tests stream.session.shutdown with just a callback function (no options)
 server.on('stream', common.mustCall((stream) => {
-  stream.session.shutdown(common.mustCall(() => {
-    assert.strictEqual(
-      stream.session.shutdown(common.mustNotCall()),
-      undefined
+  const session = stream.session;
+  session.goaway(1);
+  session.goaway(2);
+  stream.session.on('close', common.mustCall(() => {
+    common.expectsError(
+      () => session.goaway(3),
+      {
+        code: 'ERR_HTTP2_INVALID_SESSION',
+        type: Error
+      }
     );
   }));
-  stream.session.shutdown(common.mustNotCall());
 }));
 
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
+  client.on('error', common.expectsError({
+    code: 'ERR_HTTP2_SESSION_ERROR'
+  }));
 
   const req = client.request();
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_SESSION_ERROR'
+  }));
   req.resume();
-  req.on('end', common.mustCall(() => server.close()));
+  req.on('close', common.mustCall(() => {
+    server.close();
+    client.close();
+  }));
 }));
diff --git a/test/parallel/test-http2-server-socket-destroy.js b/test/parallel/test-http2-server-socket-destroy.js
index 8291c415284571..03afc1957b8af4 100644
--- a/test/parallel/test-http2-server-socket-destroy.js
+++ b/test/parallel/test-http2-server-socket-destroy.js
@@ -9,22 +9,13 @@ const assert = require('assert');
 const h2 = require('http2');
 const { kSocket } = require('internal/http2/util');
 
-const {
-  HTTP2_HEADER_METHOD,
-  HTTP2_HEADER_PATH,
-  HTTP2_METHOD_POST
-} = h2.constants;
-
 const server = h2.createServer();
 
 // we use the lower-level API here
 server.on('stream', common.mustCall(onStream));
 
 function onStream(stream) {
-  stream.respond({
-    'content-type': 'text/html',
-    ':status': 200
-  });
+  stream.respond();
   stream.write('test');
 
   const socket = stream.session[kSocket];
@@ -32,6 +23,7 @@ function onStream(stream) {
   // When the socket is destroyed, the close events must be triggered
   // on the socket, server and session.
   socket.on('close', common.mustCall());
+  stream.on('close', common.mustCall());
   server.on('close', common.mustCall());
   stream.session.on('close', common.mustCall(() => server.close()));
 
@@ -40,23 +32,25 @@ function onStream(stream) {
 
   assert.notStrictEqual(stream.session, undefined);
   socket.destroy();
-  stream.on('destroy', common.mustCall(() => {
-    assert.strictEqual(stream.session, undefined);
-  }));
 }
 
 server.listen(0);
 
 server.on('listening', common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
+  // The client may have an ECONNRESET error here depending on the operating
+  // system, due mainly to differences in the timing of socket closing. Do
+  // not wrap this in a common mustCall.
+  client.on('error', () => {});
+  client.on('close', common.mustCall());
 
-  const req = client.request({
-    [HTTP2_HEADER_PATH]: '/',
-    [HTTP2_HEADER_METHOD]: HTTP2_METHOD_POST });
+  const req = client.request({ ':method': 'POST' });
+  // The client may have an ECONNRESET error here depending on the operating
+  // system, due mainly to differences in the timing of socket closing. Do
+  // not wrap this in a common mustCall.
+  req.on('error', () => {});
 
   req.on('aborted', common.mustCall());
   req.resume();
   req.on('end', common.mustCall());
-
-  client.on('close', common.mustCall());
 }));
diff --git a/test/parallel/test-http2-server-socketerror.js b/test/parallel/test-http2-server-socketerror.js
deleted file mode 100644
index 9f52b9280d2779..00000000000000
--- a/test/parallel/test-http2-server-socketerror.js
+++ /dev/null
@@ -1,56 +0,0 @@
-// Flags: --expose-internals
-
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
-const assert = require('assert');
-const http2 = require('http2');
-const { kSocket } = require('internal/http2/util');
-
-const server = http2.createServer();
-server.on('stream', common.mustCall((stream) => {
-  stream.respond();
-  stream.end('ok');
-}));
-server.on('session', common.mustCall((session) => {
-  // First, test that the socketError event is forwarded to the session object
-  // and not the server object.
-  const handler = common.mustCall((error, socket) => {
-    common.expectsError({
-      type: Error,
-      message: 'test'
-    })(error);
-    assert.strictEqual(socket, session[kSocket]);
-  });
-  const isNotCalled = common.mustNotCall();
-  session.on('socketError', handler);
-  server.on('socketError', isNotCalled);
-  session[kSocket].emit('error', new Error('test'));
-  session.removeListener('socketError', handler);
-  server.removeListener('socketError', isNotCalled);
-
-  // Second, test that the socketError is forwarded to the server object when
-  // no socketError listener is registered for the session
-  server.on('socketError', common.mustCall((error, socket, session) => {
-    common.expectsError({
-      type: Error,
-      message: 'test'
-    })(error);
-    assert.strictEqual(socket, session[kSocket]);
-    assert.strictEqual(session, session);
-  }));
-  session[kSocket].emit('error', new Error('test'));
-}));
-
-server.listen(0, common.mustCall(() => {
-  const client = http2.connect(`http://localhost:${server.address().port}`);
-  const req = client.request();
-  req.resume();
-  req.on('end', common.mustCall());
-  req.on('close', common.mustCall(() => {
-    client.destroy();
-    server.close();
-  }));
-}));
diff --git a/test/parallel/test-http2-server-stream-session-destroy.js b/test/parallel/test-http2-server-stream-session-destroy.js
index 24d064a448f87d..5eb04a8d376635 100644
--- a/test/parallel/test-http2-server-stream-session-destroy.js
+++ b/test/parallel/test-http2-server-stream-session-destroy.js
@@ -8,56 +8,41 @@ const h2 = require('http2');
 
 const server = h2.createServer();
 
-server.on(
-  'stream',
-  common.mustCall((stream) => {
-    stream.session.destroy();
-
-    // Test that stream.state getter returns an empty object
-    // when the stream session has been destroyed
-    assert.deepStrictEqual({}, stream.state);
-
-    // Test that ERR_HTTP2_INVALID_STREAM is thrown while calling
-    // stream operations after the stream session has been destroyed
-    const invalidStreamError = {
-      type: Error,
-      code: 'ERR_HTTP2_INVALID_STREAM',
-      message: 'The stream has been destroyed'
-    };
-    common.expectsError(() => stream.additionalHeaders(), invalidStreamError);
-    common.expectsError(() => stream.priority(), invalidStreamError);
-    common.expectsError(
-      () => stream.pushStream({}, common.mustNotCall()),
-      invalidStreamError
-    );
-    common.expectsError(() => stream.respond(), invalidStreamError);
-    common.expectsError(() => stream.write('data'), invalidStreamError);
-
-    // Test that ERR_HTTP2_INVALID_SESSION is thrown while calling
-    // session operations after the stream session has been destroyed
-    const invalidSessionError = {
-      type: Error,
-      code: 'ERR_HTTP2_INVALID_SESSION',
-      message: 'The session has been destroyed'
-    };
-    common.expectsError(() => stream.session.settings(), invalidSessionError);
-    common.expectsError(() => stream.session.shutdown(), invalidSessionError);
-
-    // Wait for setImmediate call from destroy() to complete
-    // so that state.destroyed is set to true
-    setImmediate((session) => {
-      common.expectsError(() => session.settings(), invalidSessionError);
-      common.expectsError(() => session.shutdown(), invalidSessionError);
-    }, stream.session);
-  })
-);
-
-server.listen(
-  0,
-  common.mustCall(() => {
-    const client = h2.connect(`http://localhost:${server.address().port}`);
-    const req = client.request();
-    req.resume();
-    req.on('end', common.mustCall(() => server.close()));
-  })
-);
+server.on('stream', common.mustCall((stream) => {
+  assert(stream.session);
+  stream.session.destroy();
+  assert.strictEqual(stream.session, undefined);
+
+  // Test that stream.state getter returns an empty object
+  // when the stream session has been destroyed
+  assert.deepStrictEqual({}, stream.state);
+
+  // Test that ERR_HTTP2_INVALID_STREAM is thrown while calling
+  // stream operations after the stream session has been destroyed
+  const invalidStreamError = {
+    type: Error,
+    code: 'ERR_HTTP2_INVALID_STREAM',
+    message: 'The stream has been destroyed'
+  };
+  common.expectsError(() => stream.additionalHeaders(), invalidStreamError);
+  common.expectsError(() => stream.priority(), invalidStreamError);
+  common.expectsError(() => stream.respond(), invalidStreamError);
+  common.expectsError(
+    () => stream.pushStream({}, common.mustNotCall()),
+    {
+      code: 'ERR_HTTP2_PUSH_DISABLED',
+      type: Error
+    }
+  );
+  assert.strictEqual(stream.write('data'), false);
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = h2.connect(`http://localhost:${server.address().port}`);
+  client.on('error', () => {});
+  const req = client.request();
+  req.resume();
+  req.on('end', common.mustCall());
+  req.on('close', common.mustCall(() => server.close()));
+  req.on('error', () => {});
+}));
diff --git a/test/parallel/test-http2-server-timeout.js b/test/parallel/test-http2-server-timeout.js
index 28ab6efb87f6c1..581a409ce9171d 100755
--- a/test/parallel/test-http2-server-timeout.js
+++ b/test/parallel/test-http2-server-timeout.js
@@ -9,7 +9,7 @@ const server = http2.createServer();
 server.setTimeout(common.platformTimeout(1));
 
 const onServerTimeout = common.mustCall((session) => {
-  session.destroy();
+  session.close(() => session.destroy());
 });
 
 server.on('stream', common.mustNotCall());
@@ -18,10 +18,14 @@ server.once('timeout', onServerTimeout);
 server.listen(0, common.mustCall(() => {
   const url = `http://localhost:${server.address().port}`;
   const client = http2.connect(url);
+  // Because of the timeout, an ECONRESET error may or may not happen here.
+  // Keep this as a non-op and do not use common.mustCall()
+  client.on('error', () => {});
   client.on('close', common.mustCall(() => {
-
     const client2 = http2.connect(url);
+    // Because of the timeout, an ECONRESET error may or may not happen here.
+    // Keep this as a non-op and do not use common.mustCall()
+    client2.on('error', () => {});
     client2.on('close', common.mustCall(() => server.close()));
-
   }));
 }));
diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js
index 53ff44dd9ec2fe..239e2fa76c9c02 100644
--- a/test/parallel/test-http2-session-settings.js
+++ b/test/parallel/test-http2-session-settings.js
@@ -68,71 +68,62 @@ server.listen(
 
     const req = client.request(headers);
 
-    req.on(
-      'connect',
-      common.mustCall(() => {
-        // pendingSettingsAck will be true if a SETTINGS frame
-        // has been sent but we are still waiting for an acknowledgement
-        assert(client.pendingSettingsAck);
-      })
-    );
+    req.on('ready', common.mustCall(() => {
+      // pendingSettingsAck will be true if a SETTINGS frame
+      // has been sent but we are still waiting for an acknowledgement
+      assert(client.pendingSettingsAck);
+    }));
 
     // State will only be valid after connect event is emitted
-    req.on(
-      'ready',
-      common.mustCall(() => {
-        assert.doesNotThrow(() => {
-          client.settings({
-            maxHeaderListSize: 1
-          });
+    req.on('ready', common.mustCall(() => {
+      assert.doesNotThrow(() => {
+        client.settings({
+          maxHeaderListSize: 1
         });
+      });
 
-        // Verify valid error ranges
-        [
-          ['headerTableSize', -1],
-          ['headerTableSize', 2 ** 32],
-          ['initialWindowSize', -1],
-          ['initialWindowSize', 2 ** 32],
-          ['maxFrameSize', 16383],
-          ['maxFrameSize', 2 ** 24],
-          ['maxHeaderListSize', -1],
-          ['maxHeaderListSize', 2 ** 32]
-        ].forEach((i) => {
-          const settings = {};
-          settings[i[0]] = i[1];
-          common.expectsError(
-            () => client.settings(settings),
-            {
-              type: RangeError,
-              code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
-              message: `Invalid value for setting "${i[0]}": ${i[1]}`
-            }
-          );
-        });
+      // Verify valid error ranges
+      [
+        ['headerTableSize', -1],
+        ['headerTableSize', 2 ** 32],
+        ['initialWindowSize', -1],
+        ['initialWindowSize', 2 ** 32],
+        ['maxFrameSize', 16383],
+        ['maxFrameSize', 2 ** 24],
+        ['maxHeaderListSize', -1],
+        ['maxHeaderListSize', 2 ** 32]
+      ].forEach((i) => {
+        const settings = {};
+        settings[i[0]] = i[1];
+        common.expectsError(
+          () => client.settings(settings),
+          {
+            type: RangeError,
+            code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+            message: `Invalid value for setting "${i[0]}": ${i[1]}`
+          }
+        );
+      });
 
-        // error checks for enablePush
-        [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => {
-          common.expectsError(
-            () => client.settings({ enablePush: i }),
-            {
-              type: TypeError,
-              code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
-              message: `Invalid value for setting "enablePush": ${i}`
-            }
-          );
-        });
-      })
-    );
+      // error checks for enablePush
+      [1, {}, 'test', [], null, Infinity, NaN].forEach((i) => {
+        common.expectsError(
+          () => client.settings({ enablePush: i }),
+          {
+            type: TypeError,
+            code: 'ERR_HTTP2_INVALID_SETTING_VALUE',
+            message: `Invalid value for setting "enablePush": ${i}`
+          }
+        );
+      });
+    }));
 
     req.on('response', common.mustCall());
     req.resume();
-    req.on(
-      'end',
-      common.mustCall(() => {
-        server.close();
-        client.destroy();
-      })
-    );
+    req.on('end', common.mustCall(() => {
+      server.close();
+      client.close();
+    }));
     req.end();
   })
 );
diff --git a/test/parallel/test-http2-session-stream-state.js b/test/parallel/test-http2-session-stream-state.js
index 9bbac3f482cbcf..612feb8cf1e2ca 100644
--- a/test/parallel/test-http2-session-stream-state.js
+++ b/test/parallel/test-http2-session-stream-state.js
@@ -57,7 +57,7 @@ server.on('listening', common.mustCall(() => {
   const req = client.request(headers);
 
   // State will only be valid after connect event is emitted
-  req.on('connect', common.mustCall(() => {
+  req.on('ready', common.mustCall(() => {
 
     // Test Stream State.
     {
@@ -91,7 +91,7 @@ server.on('listening', common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end();
 
diff --git a/test/parallel/test-http2-shutdown-errors.js b/test/parallel/test-http2-shutdown-errors.js
deleted file mode 100644
index 638f9a60f2c395..00000000000000
--- a/test/parallel/test-http2-shutdown-errors.js
+++ /dev/null
@@ -1,76 +0,0 @@
-// Flags: --expose-http2
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
-const http2 = require('http2');
-const {
-  constants,
-  Http2Session,
-  nghttp2ErrorString
-} = process.binding('http2');
-
-// tests error handling within shutdown
-// - should emit ERR_HTTP2_ERROR on session for all errors
-
-const tests = Object.getOwnPropertyNames(constants)
-  .filter((key) => (
-    key.indexOf('NGHTTP2_ERR') === 0
-  ))
-  .map((key) => ({
-    ngError: constants[key],
-    error: {
-      code: 'ERR_HTTP2_ERROR',
-      type: Error,
-      message: nghttp2ErrorString(constants[key])
-    }
-  }));
-
-let currentError;
-
-// mock submitGoaway because we only care about testing error handling
-Http2Session.prototype.goaway = () => currentError.ngError;
-
-const server = http2.createServer();
-server.on('stream', common.mustCall((stream, headers) => {
-  const errorMustCall = common.expectsError(currentError.error);
-  const errorMustNotCall = common.mustNotCall(
-    `${currentError.error.code} should emit on session`
-  );
-
-  stream.session.once('error', errorMustCall);
-  stream.on('error', errorMustNotCall);
-
-  stream.session.shutdown();
-}, tests.length));
-
-server.listen(0, common.mustCall(() => runTest(tests.shift())));
-
-function runTest(test) {
-  const port = server.address().port;
-  const url = `http://localhost:${port}`;
-  const headers = {
-    ':path': '/',
-    ':method': 'POST',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  };
-
-  const client = http2.connect(url);
-  const req = client.request(headers);
-
-  currentError = test;
-  req.resume();
-  req.end();
-
-  req.on('end', common.mustCall(() => {
-    client.destroy();
-
-    if (!tests.length) {
-      server.close();
-    } else {
-      runTest(tests.shift());
-    }
-  }));
-}
diff --git a/test/parallel/test-http2-single-headers.js b/test/parallel/test-http2-single-headers.js
index bb2f57cba1a939..c545b065015050 100644
--- a/test/parallel/test-http2-single-headers.js
+++ b/test/parallel/test-http2-single-headers.js
@@ -26,35 +26,26 @@ server.on('stream', common.mustNotCall());
 server.listen(0, common.mustCall(() => {
   const client = http2.connect(`http://localhost:${server.address().port}`);
 
-  let remaining = singles.length * 2;
-  function maybeClose() {
-    if (--remaining === 0) {
-      server.close();
-      client.destroy();
-    }
-  }
-
   singles.forEach((i) => {
-    const req = client.request({
-      [i]: 'abc',
-      [i.toUpperCase()]: 'xyz'
-    });
-    req.on('error', common.expectsError({
-      code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
-      type: Error,
-      message: `Header field "${i}" must have only a single value`
-    }));
-    req.on('error', common.mustCall(maybeClose));
-
-    const req2 = client.request({
-      [i]: ['abc', 'xyz']
-    });
-    req2.on('error', common.expectsError({
-      code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
-      type: Error,
-      message: `Header field "${i}" must have only a single value`
-    }));
-    req2.on('error', common.mustCall(maybeClose));
+    common.expectsError(
+      () => client.request({ [i]: 'abc', [i.toUpperCase()]: 'xyz' }),
+      {
+        code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
+        type: Error,
+        message: `Header field "${i}" must have only a single value`
+      }
+    );
+
+    common.expectsError(
+      () => client.request({ [i]: ['abc', 'xyz'] }),
+      {
+        code: 'ERR_HTTP2_HEADER_SINGLE_VALUE',
+        type: Error,
+        message: `Header field "${i}" must have only a single value`
+      }
+    );
   });
 
+  server.close();
+  client.close();
 }));
diff --git a/test/parallel/test-http2-socket-proxy.js b/test/parallel/test-http2-socket-proxy.js
index 60f31837790d51..17830495addc63 100644
--- a/test/parallel/test-http2-socket-proxy.js
+++ b/test/parallel/test-http2-socket-proxy.js
@@ -57,7 +57,7 @@ server.on('stream', common.mustCall(function(stream, headers) {
   assert.strictEqual(socket.writable, 0);
   assert.strictEqual(socket.readable, 0);
 
-  stream.session.destroy();
+  stream.end();
 
   socket.setTimeout = undefined;
   assert.strictEqual(session.setTimeout, undefined);
@@ -71,18 +71,11 @@ server.listen(0, common.mustCall(function() {
   const port = server.address().port;
   const url = `http://localhost:${port}`;
   const client = h2.connect(url, common.mustCall(() => {
-    const headers = {
-      ':path': '/',
-      ':method': 'GET',
-      ':scheme': 'http',
-      ':authority': `localhost:${port}`
-    };
-    const request = client.request(headers);
+    const request = client.request();
     request.on('end', common.mustCall(() => {
-      client.destroy();
+      client.close();
       server.close();
     }));
-    request.end();
     request.resume();
   }));
 }));
diff --git a/test/parallel/test-http2-status-code-invalid.js b/test/parallel/test-http2-status-code-invalid.js
index 3a0d882dea19da..3337aad32d7f70 100644
--- a/test/parallel/test-http2-status-code-invalid.js
+++ b/test/parallel/test-http2-status-code-invalid.js
@@ -36,6 +36,6 @@ server.listen(0, common.mustCall(() => {
   req.resume();
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
 }));
diff --git a/test/parallel/test-http2-status-code.js b/test/parallel/test-http2-status-code.js
index e8f64da368a5f5..d3642b4ff0217f 100644
--- a/test/parallel/test-http2-status-code.js
+++ b/test/parallel/test-http2-status-code.js
@@ -22,7 +22,7 @@ server.listen(0, common.mustCall(() => {
   let remaining = codes.length;
   function maybeClose() {
     if (--remaining === 0) {
-      client.destroy();
+      client.close();
       server.close();
     }
   }
diff --git a/test/parallel/test-http2-stream-client.js b/test/parallel/test-http2-stream-client.js
index aa722c5ff2b6d9..3e6c6b2a8a1b5e 100644
--- a/test/parallel/test-http2-stream-client.js
+++ b/test/parallel/test-http2-stream-client.js
@@ -22,7 +22,7 @@ server.listen(0, common.mustCall(() => {
   const req = client.request();
   req.resume();
   req.on('close', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/parallel/test-http2-stream-destroy-event-order.js b/test/parallel/test-http2-stream-destroy-event-order.js
index 6db511f7d11c59..7d4bcb102f0d0a 100644
--- a/test/parallel/test-http2-stream-destroy-event-order.js
+++ b/test/parallel/test-http2-stream-destroy-event-order.js
@@ -4,26 +4,27 @@
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
-const assert = require('assert');
 const http2 = require('http2');
 
 let client;
 let req;
 const server = http2.createServer();
 server.on('stream', common.mustCall((stream) => {
-  stream.on('error', common.mustCall(() => {
-    stream.on('close', common.mustCall((code) => {
-      assert.strictEqual(code, 2);
-      client.destroy();
+  stream.on('close', common.mustCall(() => {
+    stream.on('error', common.mustCall(() => {
       server.close();
     }));
   }));
 
-  req.rstStream(2);
+  req.close(2);
 }));
 server.listen(0, common.mustCall(() => {
   client = http2.connect(`http://localhost:${server.address().port}`);
   req = client.request();
   req.resume();
-  req.on('error', common.mustCall());
+  req.on('close', common.mustCall(() => {
+    req.on('error', common.mustCall(() => {
+      client.close();
+    }));
+  }));
 }));
diff --git a/test/parallel/test-http2-timeouts.js b/test/parallel/test-http2-timeouts.js
index 88ddfcf54c3fa9..c6a7676a472655 100755
--- a/test/parallel/test-http2-timeouts.js
+++ b/test/parallel/test-http2-timeouts.js
@@ -51,7 +51,7 @@ server.on('listening', common.mustCall(() => {
       req.resume();
       req.on('end', common.mustCall(() => {
         server.close();
-        client.destroy();
+        client.close();
       }));
       req.end();
     }));
diff --git a/test/parallel/test-http2-too-large-headers.js b/test/parallel/test-http2-too-large-headers.js
index f7ac25170b846f..7a7082736160f8 100644
--- a/test/parallel/test-http2-too-large-headers.js
+++ b/test/parallel/test-http2-too-large-headers.js
@@ -25,7 +25,7 @@ server.listen(0, common.mustCall(() => {
     req.on('close', common.mustCall((code) => {
       assert.strictEqual(code, NGHTTP2_ENHANCE_YOUR_CALM);
       server.close();
-      client.destroy();
+      client.close();
     }));
   });
 
diff --git a/test/parallel/test-http2-too-many-headers.js b/test/parallel/test-http2-too-many-headers.js
index eff0fa9c351c32..f05511cff657e0 100644
--- a/test/parallel/test-http2-too-many-headers.js
+++ b/test/parallel/test-http2-too-many-headers.js
@@ -28,7 +28,7 @@ server.listen(0, common.mustCall(() => {
   req.on('close', common.mustCall((code) => {
     assert.strictEqual(code, NGHTTP2_ENHANCE_YOUR_CALM);
     server.close();
-    client.destroy();
+    client.close();
   }));
 
 }));
diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js
index 4feda98c05d522..c4f101e8224161 100644
--- a/test/parallel/test-http2-too-many-settings.js
+++ b/test/parallel/test-http2-too-many-settings.js
@@ -43,7 +43,7 @@ server.on('listening', common.mustCall(() => {
   client.on('close', closeServer);
   client.on('localSettings', common.mustCall(() => {
     if (--remaining <= 0) {
-      client.destroy();
+      client.close();
     }
   }, maxPendingAck + 1));
   client.on('connect', common.mustCall(() => doTest(client)));
@@ -56,6 +56,6 @@ server.on('listening', common.mustCall(() => {
 
   client.on('close', closeServer);
   client.on('localSettings', common.mustCall(() => {
-    client.destroy();
+    client.close();
   }));
 }));
diff --git a/test/parallel/test-http2-trailers.js b/test/parallel/test-http2-trailers.js
index 5e0db6a30c3db0..1ca5bdf70d05b0 100644
--- a/test/parallel/test-http2-trailers.js
+++ b/test/parallel/test-http2-trailers.js
@@ -45,7 +45,7 @@ server.on('listening', common.mustCall(function() {
   }));
   req.on('end', common.mustCall(() => {
     server.close();
-    client.destroy();
+    client.close();
   }));
   req.end('data');
 
diff --git a/test/parallel/test-http2-window-size.js b/test/parallel/test-http2-window-size.js
index 381416c0d23cc6..3d1c14de847e48 100644
--- a/test/parallel/test-http2-window-size.js
+++ b/test/parallel/test-http2-window-size.js
@@ -67,7 +67,7 @@ function run(buffers, initialWindowSize) {
             const actualBuffer = Buffer.concat(responses);
             assert.strictEqual(Buffer.compare(actualBuffer, expectedBuffer), 0);
             // shut down
-            client.destroy();
+            client.close();
             server.close(() => {
               resolve();
             });
diff --git a/test/parallel/test-http2-write-callbacks.js b/test/parallel/test-http2-write-callbacks.js
index 44e33573a680b6..eca7f00ea7e292 100644
--- a/test/parallel/test-http2-write-callbacks.js
+++ b/test/parallel/test-http2-write-callbacks.js
@@ -31,7 +31,7 @@ server.listen(0, common.mustCall(() => {
   req.on('data', (chunk) => actual += chunk);
   req.on('end', common.mustCall(() => assert.strictEqual(actual, 'abcxyz')));
   req.on('close', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/parallel/test-http2-write-empty-string.js b/test/parallel/test-http2-write-empty-string.js
index fea261917187da..6e6ce5254ddcfc 100644
--- a/test/parallel/test-http2-write-empty-string.js
+++ b/test/parallel/test-http2-write-empty-string.js
@@ -34,7 +34,7 @@ server.listen(0, common.mustCall(function() {
 
   req.on('end', common.mustCall(function() {
     assert.strictEqual('1\n2\n3\n', res);
-    client.destroy();
+    client.close();
   }));
 
   req.end();
diff --git a/test/parallel/test-http2-zero-length-write.js b/test/parallel/test-http2-zero-length-write.js
index 899c28bace6f53..0b50715330a1c4 100644
--- a/test/parallel/test-http2-zero-length-write.js
+++ b/test/parallel/test-http2-zero-length-write.js
@@ -41,11 +41,12 @@ server.listen(0, common.mustCall(() => {
   let actual = '';
   const req = client.request({ ':method': 'POST' });
   req.on('response', common.mustCall());
+  req.setEncoding('utf8');
   req.on('data', (chunk) => actual += chunk);
   req.on('end', common.mustCall(() => {
     assert.strictEqual(actual, expect);
     server.close();
-    client.destroy();
+    client.close();
   }));
   getSrc().pipe(req);
 }));
diff --git a/test/sequential/test-http2-session-timeout.js b/test/sequential/test-http2-session-timeout.js
index 7a401e90ea4bbc..fce4570563c584 100644
--- a/test/sequential/test-http2-session-timeout.js
+++ b/test/sequential/test-http2-session-timeout.js
@@ -38,7 +38,7 @@ server.listen(0, common.mustCall(() => {
         setTimeout(() => makeReq(attempts - 1), callTimeout);
       } else {
         server.removeListener('timeout', mustNotCall);
-        client.destroy();
+        client.close();
         server.close();
       }
     });
diff --git a/test/sequential/test-http2-timeout-large-write-file.js b/test/sequential/test-http2-timeout-large-write-file.js
index d0737d420ef244..910e7a0fc497bd 100644
--- a/test/sequential/test-http2-timeout-large-write-file.js
+++ b/test/sequential/test-http2-timeout-large-write-file.js
@@ -80,7 +80,7 @@ server.listen(0, common.mustCall(() => {
     }
   }, 1));
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));
diff --git a/test/sequential/test-http2-timeout-large-write.js b/test/sequential/test-http2-timeout-large-write.js
index f0a11b2e44469e..a15fb46af6d28a 100644
--- a/test/sequential/test-http2-timeout-large-write.js
+++ b/test/sequential/test-http2-timeout-large-write.js
@@ -78,7 +78,7 @@ server.listen(0, common.mustCall(() => {
     }
   }, 1));
   req.on('end', common.mustCall(() => {
-    client.destroy();
+    client.close();
     server.close();
   }));
 }));

From 10e9b9db61294e875a5725fd7bb1338c8e6e8888 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sun, 17 Dec 2017 02:17:25 +0100
Subject: [PATCH 13/77] http2: remove redundant write indirection

`nghttp2_stream_write_t` was not a necessary redirection layer
and came with the cost of one additional allocation per stream write.

Also, having both `nghttp2_stream_write` and `nghttp2_stream_write_t`
as identifiers did not help with readability.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17718
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 src/node_http2.cc | 54 ++++++++++++++++-------------------------------
 src/node_http2.h  | 22 +------------------
 2 files changed, 19 insertions(+), 57 deletions(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index ac785de4cd3196..a695e3990dca5a 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -1369,30 +1369,6 @@ inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) {
   data_chunks_.emplace(uv_buf_init(buf, len));
 }
 
-// The Http2Stream class is a subclass of StreamBase. The DoWrite method
-// receives outbound chunks of data to send as outbound DATA frames. These
-// are queued in an internal linked list of uv_buf_t structs that are sent
-// when nghttp2 is ready to serialize the data frame.
-int Http2Stream::DoWrite(WriteWrap* req_wrap,
-                         uv_buf_t* bufs,
-                         size_t count,
-                         uv_stream_t* send_handle) {
-  CHECK(!this->IsDestroyed());
-  session_->SetChunksSinceLastWrite();
-
-  nghttp2_stream_write_t* req = new nghttp2_stream_write_t;
-  req->data = req_wrap;
-
-  auto AfterWrite = [](nghttp2_stream_write_t* req, int status) {
-    WriteWrap* wrap = static_cast<WriteWrap*>(req->data);
-    wrap->Done(status);
-    delete req;
-  };
-  req_wrap->Dispatched();
-  Write(req, bufs, count, AfterWrite);
-  return 0;
-}
-
 
 inline void Http2Stream::Close(int32_t code) {
   CHECK(!this->IsDestroyed());
@@ -1447,7 +1423,7 @@ inline void Http2Stream::Destroy() {
     // we still have qeueued outbound writes.
     while (!stream->queue_.empty()) {
       nghttp2_stream_write* head = stream->queue_.front();
-      head->cb(head->req, UV_ECANCELED);
+      head->req_wrap->Done(UV_ECANCELED);
       delete head;
       stream->queue_.pop();
     }
@@ -1616,26 +1592,32 @@ inline int Http2Stream::ReadStop() {
   return 0;
 }
 
+// The Http2Stream class is a subclass of StreamBase. The DoWrite method
+// receives outbound chunks of data to send as outbound DATA frames. These
+// are queued in an internal linked list of uv_buf_t structs that are sent
+// when nghttp2 is ready to serialize the data frame.
+//
 // Queue the given set of uv_but_t handles for writing to an
-// nghttp2_stream. The callback will be invoked once the chunks
-// of data have been flushed to the underlying nghttp2_session.
+// nghttp2_stream. The WriteWrap's Done callback will be invoked once the
+// chunks of data have been flushed to the underlying nghttp2_session.
 // Note that this does *not* mean that the data has been flushed
 // to the socket yet.
-inline int Http2Stream::Write(nghttp2_stream_write_t* req,
-                              const uv_buf_t bufs[],
-                              unsigned int nbufs,
-                              nghttp2_stream_write_cb cb) {
+inline int Http2Stream::DoWrite(WriteWrap* req_wrap,
+                                uv_buf_t* bufs,
+                                size_t nbufs,
+                                uv_stream_t* send_handle) {
   CHECK(!this->IsDestroyed());
+  CHECK_EQ(send_handle, nullptr);
   Http2Scope h2scope(this);
+  session_->SetChunksSinceLastWrite();
+  req_wrap->Dispatched();
   if (!IsWritable()) {
-    if (cb != nullptr)
-      cb(req, UV_EOF);
+    req_wrap->Done(UV_EOF);
     return 0;
   }
   DEBUG_HTTP2STREAM2(this, "queuing %d buffers to send", id_, nbufs);
   nghttp2_stream_write* item = new nghttp2_stream_write;
-  item->cb = cb;
-  item->req = req;
+  item->req_wrap = req_wrap;
   item->nbufs = nbufs;
   item->bufs.AllocateSufficientStorage(nbufs);
   memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs));
@@ -1824,7 +1806,7 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
       stream->queue_offset_ = 0;
     }
     if (stream->queue_index_ == head->nbufs) {
-      head->cb(head->req, 0);
+      head->req_wrap->Done(0);
       delete head;
       stream->queue_.pop();
       stream->queue_offset_ = 0;
diff --git a/src/node_http2.h b/src/node_http2.h
index 12eacb07659549..4af8c75d5bc1f5 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -91,8 +91,6 @@ void inline debug_vfprintf(const char* format, ...) {
 
 #define MAX_BUFFER_COUNT 16
 
-struct nghttp2_stream_write_t;
-
 enum nghttp2_session_type {
   NGHTTP2_SESSION_SERVER,
   NGHTTP2_SESSION_CLIENT
@@ -127,15 +125,9 @@ enum nghttp2_stream_options {
   STREAM_OPTION_GET_TRAILERS = 0x2,
 };
 
-// Callbacks
-typedef void (*nghttp2_stream_write_cb)(
-    nghttp2_stream_write_t* req,
-    int status);
-
 struct nghttp2_stream_write {
   unsigned int nbufs = 0;
-  nghttp2_stream_write_t* req = nullptr;
-  nghttp2_stream_write_cb cb = nullptr;
+  WriteWrap* req_wrap = nullptr;
   MaybeStackBuffer<uv_buf_t, MAX_BUFFER_COUNT> bufs;
 };
 
@@ -146,11 +138,6 @@ struct nghttp2_header {
 };
 
 
-struct nghttp2_stream_write_t {
-  void* data;
-  int status;
-};
-
 // Unlike the HTTP/1 implementation, the HTTP/2 implementation is not limited
 // to a fixed number of known supported HTTP methods. These constants, therefore
 // are provided strictly as a convenience to users and are exposed via the
@@ -557,13 +544,6 @@ class Http2Stream : public AsyncWrap,
 
   Http2Session* session() { return session_; }
 
-  // Queue outbound chunks of data to be sent on this stream
-  inline int Write(
-      nghttp2_stream_write_t* req,
-      const uv_buf_t bufs[],
-      unsigned int nbufs,
-      nghttp2_stream_write_cb cb);
-
   inline bool HasDataChunks(bool ignore_eos = false);
 
   inline void AddChunk(const uint8_t* data, size_t len);

From 93d00abc1c2589931a77efc8020576397f3e0926 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sun, 17 Dec 2017 06:54:27 +0100
Subject: [PATCH 14/77] http2: refactor outgoing write mechanism
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Only finish outgoing `WriteWrap`s once data has actually been
  passed to the underlying socket.
  - This makes HTTP2 streams respect backpressure
- Use `DoTryWrite` as a shortcut for sending out as much of
  the data synchronously without blocking as possible
- Use `NGHTTP2_DATA_FLAG_NO_COPY` to avoid copying DATA frame
  contents into nghttp2’s buffers before sending them out.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17718
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 src/node_http2.cc | 317 ++++++++++++++++++++++++++++++----------------
 src/node_http2.h  |  29 ++++-
 2 files changed, 231 insertions(+), 115 deletions(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index a695e3990dca5a..56ef79f8a5da03 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -4,7 +4,6 @@
 #include "node_http2.h"
 #include "node_http2_state.h"
 
-#include <queue>
 #include <algorithm>
 
 namespace node {
@@ -23,6 +22,23 @@ using v8::Undefined;
 
 namespace http2 {
 
+namespace {
+
+const char zero_bytes_256[256] = {};
+
+inline Http2Stream* GetStream(Http2Session* session,
+                              int32_t id,
+                              nghttp2_data_source* source) {
+  Http2Stream* stream = static_cast<Http2Stream*>(source->ptr);
+  if (stream == nullptr)
+    stream = session->FindStream(id);
+  CHECK_NE(stream, nullptr);
+  CHECK_EQ(id, stream->id());
+  return stream;
+}
+
+}  // anonymous namespace
+
 // These configure the callbacks required by nghttp2 itself. There are
 // two sets of callback functions, one that is used if a padding callback
 // is set, and other that does not include the padding callback.
@@ -370,6 +386,8 @@ Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
     callbacks, OnInvalidHeader);
   nghttp2_session_callbacks_set_error_callback(
     callbacks, OnNghttpError);
+  nghttp2_session_callbacks_set_send_data_callback(
+    callbacks, OnSendData);
 
   if (kHasGetPaddingCallback) {
     nghttp2_session_callbacks_set_select_padding_callback(
@@ -419,6 +437,9 @@ Http2Session::Http2Session(Environment* env,
   // be catching before it gets this far. Either way, crash if this
   // fails.
   CHECK_EQ(fn(&session_, callbacks, this, *opts), 0);
+
+  outgoing_storage_.reserve(4096);
+  outgoing_buffers_.reserve(32);
 }
 
 void Http2Session::Unconsume() {
@@ -508,6 +529,7 @@ inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
 // not be the preferred option.
 inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
                                                size_t maxPayloadLen) {
+  if (frameLen == 0) return 0;
   DEBUG_HTTP2SESSION(this, "using callback to determine padding");
   Isolate* isolate = env()->isolate();
   HandleScope handle_scope(isolate);
@@ -1033,6 +1055,20 @@ inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
   MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
 }
 
+// Callback used when data has been written to the stream.
+void Http2Session::OnStreamAfterWriteImpl(WriteWrap* w, int status, void* ctx) {
+  Http2Session* session = static_cast<Http2Session*>(ctx);
+  DEBUG_HTTP2SESSION2(session, "write finished with status %d", status);
+
+  // Inform all pending writes about their completion.
+  session->ClearOutgoing(status);
+
+  if (!(session->flags_ & SESSION_STATE_WRITE_SCHEDULED)) {
+    // Schedule a new write if nghttp2 wants to send data.
+    session->MaybeScheduleWrite();
+  }
+}
+
 // If the underlying nghttp2_session struct has data pending in its outbound
 // queue, MaybeScheduleWrite will schedule a SendPendingData() call to occcur
 // on the next iteration of the Node.js event loop (using the SetImmediate
@@ -1040,6 +1076,7 @@ inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
 void Http2Session::MaybeScheduleWrite() {
   CHECK_EQ(flags_ & SESSION_STATE_WRITE_SCHEDULED, 0);
   if (session_ != nullptr && nghttp2_session_want_write(session_)) {
+    DEBUG_HTTP2SESSION(this, "scheduling write");
     flags_ |= SESSION_STATE_WRITE_SCHEDULED;
     env()->SetImmediate([](Environment* env, void* data) {
       Http2Session* session = static_cast<Http2Session*>(data);
@@ -1059,6 +1096,39 @@ void Http2Session::MaybeScheduleWrite() {
   }
 }
 
+// Unset the sending state, finish up all current writes, and reset
+// storage for data and metadata that was associated with these writes.
+void Http2Session::ClearOutgoing(int status) {
+  CHECK_NE(flags_ & SESSION_STATE_SENDING, 0);
+  flags_ &= ~SESSION_STATE_SENDING;
+
+  for (const nghttp2_stream_write& wr : outgoing_buffers_) {
+    WriteWrap* wrap = wr.req_wrap;
+    if (wrap != nullptr)
+      wrap->Done(status);
+  }
+
+  outgoing_buffers_.clear();
+  outgoing_storage_.clear();
+}
+
+// Queue a given block of data for sending. This always creates a copy,
+// so it is used for the cases in which nghttp2 requests sending of a
+// small chunk of data.
+void Http2Session::CopyDataIntoOutgoing(const uint8_t* src, size_t src_length) {
+  size_t offset = outgoing_storage_.size();
+  outgoing_storage_.resize(offset + src_length);
+  memcpy(&outgoing_storage_[offset], src, src_length);
+
+  // Store with a base of `nullptr` initially, since future resizes
+  // of the outgoing_buffers_ vector may invalidate the pointer.
+  // The correct base pointers will be set later, before writing to the
+  // underlying socket.
+  outgoing_buffers_.emplace_back(nghttp2_stream_write {
+    uv_buf_init(nullptr, src_length)
+  });
+}
+
 // Prompts nghttp2 to begin serializing it's pending data and pushes each
 // chunk out to the i/o socket to be sent. This is a particularly hot method
 // that will generally be called at least twice be event loop iteration.
@@ -1075,64 +1145,133 @@ void Http2Session::SendPendingData() {
   // SendPendingData should not be called recursively.
   if (flags_ & SESSION_STATE_SENDING)
     return;
+  // This is cleared by ClearOutgoing().
   flags_ |= SESSION_STATE_SENDING;
 
-  WriteWrap* req = nullptr;
-  char* dest = nullptr;
-  size_t destRemaining = 0;
-  size_t destLength = 0;             // amount of data stored in dest
-  size_t destOffset = 0;             // current write offset of dest
-
-  const uint8_t* src;                // pointer to the serialized data
-  ssize_t srcLength = 0;             // length of serialized data chunk
-
-  // While srcLength is greater than zero
-  while ((srcLength = nghttp2_session_mem_send(session_, &src)) > 0) {
-    if (req == nullptr) {
-      req = AllocateSend();
-      destRemaining = req->ExtraSize();
-      dest = req->Extra();
-    }
-    DEBUG_HTTP2SESSION2(this, "nghttp2 has %d bytes to send", srcLength);
-    size_t srcRemaining = srcLength;
-    size_t srcOffset = 0;
-
-    // The amount of data we have to copy is greater than the space
-    // remaining. Copy what we can into the remaining space, send it,
-    // the proceed with the rest.
-    while (srcRemaining > destRemaining) {
-      DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket",
-                          destLength + destRemaining);
-      memcpy(dest + destOffset, src + srcOffset, destRemaining);
-      destLength += destRemaining;
-      Send(req, dest, destLength);
-      destOffset = 0;
-      destLength = 0;
-      srcRemaining -= destRemaining;
-      srcOffset += destRemaining;
-      req = AllocateSend();
-      destRemaining = req->ExtraSize();
-      dest = req->Extra();
-    }
+  ssize_t src_length;
+  const uint8_t* src;
+
+  CHECK_EQ(outgoing_buffers_.size(), 0);
+  CHECK_EQ(outgoing_storage_.size(), 0);
 
-    if (srcRemaining > 0) {
-      memcpy(dest + destOffset, src + srcOffset, srcRemaining);
-      destLength += srcRemaining;
-      destOffset += srcRemaining;
-      destRemaining -= srcRemaining;
-      srcRemaining = 0;
-      srcOffset = 0;
+  // Part One: Gather data from nghttp2
+
+  while ((src_length = nghttp2_session_mem_send(session_, &src)) > 0) {
+    DEBUG_HTTP2SESSION2(this, "nghttp2 has %d bytes to send", src_length);
+    CopyDataIntoOutgoing(src, src_length);
+  }
+
+  CHECK_NE(src_length, NGHTTP2_ERR_NOMEM);
+
+  if (stream_ == nullptr) {
+    // It would seem nice to bail out earlier, but `nghttp2_session_mem_send()`
+    // does take care of things like closing the individual streams after
+    // a socket has been torn down, so we still need to call it.
+    ClearOutgoing(UV_ECANCELED);
+    return;
+  }
+
+  // Part Two: Pass Data to the underlying stream
+
+  size_t count = outgoing_buffers_.size();
+  if (count == 0) {
+    flags_ &= ~SESSION_STATE_SENDING;
+    return;
+  }
+  MaybeStackBuffer<uv_buf_t, 32> bufs;
+  bufs.AllocateSufficientStorage(count);
+
+  // Set the buffer base pointers for copied data that ended up in the
+  // sessions's own storage since it might have shifted around during gathering.
+  // (Those are marked by having .base == nullptr.)
+  size_t offset = 0;
+  size_t i = 0;
+  for (const nghttp2_stream_write& write : outgoing_buffers_) {
+    if (write.buf.base == nullptr) {
+      bufs[i++] = uv_buf_init(
+          reinterpret_cast<char*>(outgoing_storage_.data() + offset),
+          write.buf.len);
+      offset += write.buf.len;
+    } else {
+      bufs[i++] = write.buf;
     }
   }
-  CHECK_NE(srcLength, NGHTTP2_ERR_NOMEM);
-  if (destLength > 0 && srcLength >= 0) {
-    DEBUG_HTTP2SESSION2(this, "pushing %d bytes to the socket", destLength);
-    Send(req, dest, destLength);
+
+  chunks_sent_since_last_write_++;
+
+  // DoTryWrite may modify both the buffer list start itself and the
+  // base pointers/length of the individual buffers.
+  uv_buf_t* writebufs = *bufs;
+  if (stream_->DoTryWrite(&writebufs, &count) != 0 || count == 0) {
+    // All writes finished synchronously, nothing more to do here.
+    ClearOutgoing(0);
+    return;
+  }
+
+  WriteWrap* req = AllocateSend();
+  if (stream_->DoWrite(req, writebufs, count, nullptr) != 0) {
+    req->Dispose();
   }
+
   DEBUG_HTTP2SESSION2(this, "wants data in return? %d",
                       nghttp2_session_want_read(session_));
+}
 
-  flags_ &= ~SESSION_STATE_SENDING;
+
+// This callback is called from nghttp2 when it wants to send DATA frames for a
+// given Http2Stream, when we set the `NGHTTP2_DATA_FLAG_NO_COPY` flag earlier
+// in the Http2Stream::Provider::Stream::OnRead callback.
+// We take the write information directly out of the stream's data queue.
+int Http2Session::OnSendData(
+      nghttp2_session* session_,
+      nghttp2_frame* frame,
+      const uint8_t* framehd,
+      size_t length,
+      nghttp2_data_source* source,
+      void* user_data) {
+  Http2Session* session = static_cast<Http2Session*>(user_data);
+  Http2Stream* stream = GetStream(session, frame->hd.stream_id, source);
+
+  // Send the frame header + a byte that indicates padding length.
+  session->CopyDataIntoOutgoing(framehd, 9);
+  if (frame->data.padlen > 0) {
+    uint8_t padding_byte = frame->data.padlen - 1;
+    CHECK_EQ(padding_byte, frame->data.padlen - 1);
+    session->CopyDataIntoOutgoing(&padding_byte, 1);
+  }
+
+  DEBUG_HTTP2SESSION2(session, "nghttp2 has %d bytes to send directly", length);
+  while (length > 0) {
+    // nghttp2 thinks that there is data available (length > 0), which means
+    // we told it so, which means that we *should* have data available.
+    CHECK(!stream->queue_.empty());
+
+    nghttp2_stream_write& write = stream->queue_.front();
+    if (write.buf.len <= length) {
+      // This write does not suffice by itself, so we can consume it completely.
+      length -= write.buf.len;
+      session->outgoing_buffers_.emplace_back(std::move(write));
+      stream->queue_.pop();
+      continue;
+    }
+
+    // Slice off `length` bytes of the first write in the queue.
+    session->outgoing_buffers_.emplace_back(nghttp2_stream_write {
+      uv_buf_init(write.buf.base, length)
+    });
+    write.buf.base += length;
+    write.buf.len -= length;
+    break;
+  }
+
+  if (frame->data.padlen > 0) {
+    // Send padding if that was requested.
+    session->outgoing_buffers_.emplace_back(nghttp2_stream_write {
+      uv_buf_init(const_cast<char*>(zero_bytes_256), frame->data.padlen - 1)
+    });
+  }
+
+  return 0;
 }
 
 // Creates a new Http2Stream and submits a new http2 request.
@@ -1163,25 +1302,7 @@ WriteWrap* Http2Session::AllocateSend() {
   Local<Object> obj =
       env()->write_wrap_constructor_function()
           ->NewInstance(env()->context()).ToLocalChecked();
-  // Base the amount allocated on the remote peers max frame size
-  uint32_t size =
-      nghttp2_session_get_remote_settings(
-          session(),
-          NGHTTP2_SETTINGS_MAX_FRAME_SIZE);
-  // Max frame size + 9 bytes for the header
-  return WriteWrap::New(env(), obj, stream_, size + 9);
-}
-
-// Pushes chunks of data to the i/o stream.
-void Http2Session::Send(WriteWrap* req, char* buf, size_t length) {
-  DEBUG_HTTP2SESSION2(this, "attempting to send %d bytes", length);
-  if (stream_ == nullptr)
-    return;
-  chunks_sent_since_last_write_++;
-  uv_buf_t actual = uv_buf_init(buf, length);
-  if (stream_->DoWrite(req, &actual, 1, nullptr)) {
-    req->Dispose();
-  }
+  return WriteWrap::New(env(), obj, stream_);
 }
 
 // Allocates the data buffer used to receive inbound data from the i/o stream
@@ -1255,6 +1376,7 @@ void Http2Session::Consume(Local<External> external) {
   prev_read_cb_ = stream->read_cb();
   stream->set_alloc_cb({ Http2Session::OnStreamAllocImpl, this });
   stream->set_read_cb({ Http2Session::OnStreamReadImpl, this });
+  stream->set_after_write_cb({ Http2Session::OnStreamAfterWriteImpl, this });
   stream->set_destruct_cb({ Http2Session::OnStreamDestructImpl, this });
   DEBUG_HTTP2SESSION(this, "i/o stream consumed");
 }
@@ -1422,9 +1544,9 @@ inline void Http2Stream::Destroy() {
     // here because it's possible for destroy to have been called while
     // we still have qeueued outbound writes.
     while (!stream->queue_.empty()) {
-      nghttp2_stream_write* head = stream->queue_.front();
-      head->req_wrap->Done(UV_ECANCELED);
-      delete head;
+      nghttp2_stream_write& head = stream->queue_.front();
+      if (head.req_wrap != nullptr)
+        head.req_wrap->Done(UV_ECANCELED);
       stream->queue_.pop();
     }
 
@@ -1616,12 +1738,15 @@ inline int Http2Stream::DoWrite(WriteWrap* req_wrap,
     return 0;
   }
   DEBUG_HTTP2STREAM2(this, "queuing %d buffers to send", id_, nbufs);
-  nghttp2_stream_write* item = new nghttp2_stream_write;
-  item->req_wrap = req_wrap;
-  item->nbufs = nbufs;
-  item->bufs.AllocateSufficientStorage(nbufs);
-  memcpy(*(item->bufs), bufs, nbufs * sizeof(*bufs));
-  queue_.push(item);
+  for (size_t i = 0; i < nbufs; ++i) {
+    // Store the req_wrap on the last write info in the queue, so that it is
+    // only marked as finished once all buffers associated with it are finished.
+    queue_.emplace(nghttp2_stream_write {
+      i == nbufs - 1 ? req_wrap : nullptr,
+      bufs[i]
+    });
+    available_outbound_length_ += bufs[i].len;
+  }
   CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM);
   return 0;
 }
@@ -1655,18 +1780,6 @@ inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
   return true;
 }
 
-
-Http2Stream* GetStream(Http2Session* session,
-                       int32_t id,
-                       nghttp2_data_source* source) {
-  Http2Stream* stream = static_cast<Http2Stream*>(source->ptr);
-  if (stream == nullptr)
-    stream = session->FindStream(id);
-  CHECK_NE(stream, nullptr);
-  CHECK_EQ(id, stream->id());
-  return stream;
-}
-
 // A Provider is the thing that provides outbound DATA frame data.
 Http2Stream::Provider::Provider(Http2Stream* stream, int options) {
   CHECK(!stream->IsDestroyed());
@@ -1787,30 +1900,16 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
 
   size_t amount = 0;          // amount of data being sent in this data frame.
 
-  uv_buf_t current;
-
   if (!stream->queue_.empty()) {
     DEBUG_HTTP2SESSION2(session, "stream %d has pending outbound data", id);
-    nghttp2_stream_write* head = stream->queue_.front();
-    current = head->bufs[stream->queue_index_];
-    size_t clen = current.len - stream->queue_offset_;
-    amount = std::min(clen, length);
+    amount = std::min(stream->available_outbound_length_, length);
     DEBUG_HTTP2SESSION2(session, "sending %d bytes for data frame on stream %d",
                         amount, id);
     if (amount > 0) {
-      memcpy(buf, current.base + stream->queue_offset_, amount);
-      stream->queue_offset_ += amount;
-    }
-    if (stream->queue_offset_ == current.len) {
-      stream->queue_index_++;
-      stream->queue_offset_ = 0;
-    }
-    if (stream->queue_index_ == head->nbufs) {
-      head->req_wrap->Done(0);
-      delete head;
-      stream->queue_.pop();
-      stream->queue_offset_ = 0;
-      stream->queue_index_ = 0;
+      // Just return the length, let Http2Session::OnSendData take care of
+      // actually taking the buffers out of the queue.
+      *flags |= NGHTTP2_DATA_FLAG_NO_COPY;
+      stream->available_outbound_length_ -= amount;
     }
   }
 
diff --git a/src/node_http2.h b/src/node_http2.h
index 4af8c75d5bc1f5..8cccfb342db3e0 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -126,9 +126,12 @@ enum nghttp2_stream_options {
 };
 
 struct nghttp2_stream_write {
-  unsigned int nbufs = 0;
   WriteWrap* req_wrap = nullptr;
-  MaybeStackBuffer<uv_buf_t, MAX_BUFFER_COUNT> bufs;
+  uv_buf_t buf;
+
+  inline explicit nghttp2_stream_write(uv_buf_t buf_) : buf(buf_) {}
+  inline nghttp2_stream_write(WriteWrap* req, uv_buf_t buf_) :
+      req_wrap(req), buf(buf_) {}
 };
 
 struct nghttp2_header {
@@ -725,11 +728,12 @@ class Http2Stream : public AsyncWrap,
 
   // Outbound Data... This is the data written by the JS layer that is
   // waiting to be written out to the socket.
-  std::queue<nghttp2_stream_write*> queue_;
-  unsigned int queue_index_ = 0;
-  size_t queue_offset_ = 0;
+  std::queue<nghttp2_stream_write> queue_;
+  size_t available_outbound_length_ = 0;
   int64_t fd_offset_ = 0;
   int64_t fd_length_ = -1;
+
+  friend class Http2Session;
 };
 
 class Http2Stream::Provider {
@@ -860,6 +864,7 @@ class Http2Session : public AsyncWrap {
                                const uv_buf_t* bufs,
                                uv_handle_type pending,
                                void* ctx);
+  static void OnStreamAfterWriteImpl(WriteWrap* w, int status, void* ctx);
   static void OnStreamDestructImpl(void* ctx);
 
   // The JavaScript API
@@ -882,7 +887,6 @@ class Http2Session : public AsyncWrap {
   template <get_setting fn>
   static void GetSettings(const FunctionCallbackInfo<Value>& args);
 
-  void Send(WriteWrap* req, char* buf, size_t length);
   WriteWrap* AllocateSend();
 
   uv_loop_t* event_loop() const {
@@ -957,6 +961,13 @@ class Http2Session : public AsyncWrap {
       const char* message,
       size_t len,
       void* user_data);
+  static inline int OnSendData(
+      nghttp2_session* session,
+      nghttp2_frame* frame,
+      const uint8_t* framehd,
+      size_t length,
+      nghttp2_data_source* source,
+      void* user_data);
 
 
   static inline ssize_t OnStreamReadFD(
@@ -1015,6 +1026,12 @@ class Http2Session : public AsyncWrap {
   size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
   std::queue<Http2Ping*> outstanding_pings_;
 
+  std::vector<nghttp2_stream_write> outgoing_buffers_;
+  std::vector<uint8_t> outgoing_storage_;
+
+  void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length);
+  void ClearOutgoing(int status);
+
   friend class Http2Scope;
 };
 

From f68325c29ef19d479225a92c11e0b7641b1ac64a Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 18 Dec 2017 14:55:16 -0800
Subject: [PATCH 15/77] http2: convert Http2Settings to an AsyncWrap

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17763
Refs: https://github.com/nodejs/node/issues/17746
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 lib/internal/http2/core.js                    |  57 +++---
 lib/internal/http2/util.js                    |   8 +-
 src/async_wrap.h                              |   1 +
 src/env.h                                     |   1 +
 src/node_http2.cc                             | 185 ++++++++++++------
 src/node_http2.h                              |  91 +++++----
 src/node_http2_state.h                        |   1 +
 test/parallel/test-http2-session-settings.js  |   4 +-
 test/parallel/test-http2-too-many-settings.js |  76 +++----
 .../test-http2-util-update-options-buffer.js  |   8 +-
 test/sequential/test-async-wrap-getasyncid.js |   1 +
 11 files changed, 269 insertions(+), 164 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 252e2280317e7a..1fc0c6ce793c70 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -322,23 +322,14 @@ function onStreamRead(nread, buf, handle) {
 
 // Called when the remote peer settings have been updated.
 // Resets the cached settings.
-function onSettings(ack) {
+function onSettings() {
   const session = this[kOwner];
   if (session.destroyed)
     return;
   session[kUpdateTimer]();
-  let event = 'remoteSettings';
-  if (ack) {
-    debug(`Http2Session ${sessionName(session[kType])}: settings acknowledged`);
-    if (session[kState].pendingAck > 0)
-      session[kState].pendingAck--;
-    session[kLocalSettings] = undefined;
-    event = 'localSettings';
-  } else {
-    debug(`Http2Session ${sessionName(session[kType])}: new settings received`);
-    session[kRemoteSettings] = undefined;
-  }
-  process.nextTick(emit, session, event, session[event]);
+  debug(`Http2Session ${sessionName(session[kType])}: new settings received`);
+  session[kRemoteSettings] = undefined;
+  process.nextTick(emit, session, 'remoteSettings', session.remoteSettings);
 }
 
 // If the stream exists, an attempt will be made to emit an event
@@ -538,15 +529,32 @@ function onSessionInternalError(code) {
     this[kOwner].destroy(new NghttpError(code));
 }
 
+function settingsCallback(cb, ack, duration) {
+  this[kState].pendingAck--;
+  this[kLocalSettings] = undefined;
+  if (ack) {
+    debug(`Http2Session ${sessionName(this[kType])}: settings received`);
+    const settings = this.localSettings;
+    if (typeof cb === 'function')
+      cb(null, settings, duration);
+    this.emit('localSettings', settings);
+  } else {
+    debug(`Http2Session ${sessionName(this[kType])}: settings canceled`);
+    if (typeof cb === 'function')
+      cb(new errors.Error('ERR_HTTP2_SETTINGS_CANCEL'));
+  }
+}
+
 // Submits a SETTINGS frame to be sent to the remote peer.
-function submitSettings(settings) {
+function submitSettings(settings, callback) {
   if (this.destroyed)
     return;
   debug(`Http2Session ${sessionName(this[kType])}: submitting settings`);
   this[kUpdateTimer]();
-  this[kLocalSettings] = undefined;
   updateSettingsBuffer(settings);
-  this[kHandle].settings();
+  if (!this[kHandle].settings(settingsCallback.bind(this, callback))) {
+    this.destroy(new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK'));
+  }
 }
 
 // Submits a PRIORITY frame to be sent to the remote peer
@@ -781,7 +789,6 @@ class Http2Session extends EventEmitter {
       streams: new Map(),
       pendingStreams: new Set(),
       pendingAck: 0,
-      maxPendingAck: Math.max(1, (options.maxPendingAck | 0) || 10),
       writeQueueSize: 0
     };
 
@@ -948,21 +955,19 @@ class Http2Session extends EventEmitter {
   }
 
   // Submits a SETTINGS frame to be sent to the remote peer.
-  settings(settings) {
+  settings(settings, callback) {
     if (this.destroyed)
       throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
-
     assertIsObject(settings, 'settings');
     settings = validateSettings(settings);
-    const state = this[kState];
-    if (state.pendingAck === state.maxPendingAck) {
-      throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
-                             this[kState].pendingAck);
-    }
+
+    if (callback && typeof callback !== 'function')
+      throw new errors.TypeError('ERR_INVALID_CALLBACK');
     debug(`Http2Session ${sessionName(this[kType])}: sending settings`);
 
-    state.pendingAck++;
-    const settingsFn = submitSettings.bind(this, settings);
+    this[kState].pendingAck++;
+
+    const settingsFn = submitSettings.bind(this, settings, callback);
     if (this.connecting) {
       this.once('connect', settingsFn);
       return;
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index 1800dc5cff33ff..51a2094d43af56 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -174,7 +174,8 @@ const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3;
 const IDX_OPTIONS_PADDING_STRATEGY = 4;
 const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
 const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
-const IDX_OPTIONS_FLAGS = 7;
+const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
+const IDX_OPTIONS_FLAGS = 8;
 
 function updateOptionsBuffer(options) {
   var flags = 0;
@@ -213,6 +214,11 @@ function updateOptionsBuffer(options) {
     optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] =
       options.maxOutstandingPings;
   }
+  if (typeof options.maxOutstandingSettings === 'number') {
+    flags |= (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS);
+    optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS] =
+      Math.max(1, options.maxOutstandingSettings);
+  }
   optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
 }
 
diff --git a/src/async_wrap.h b/src/async_wrap.h
index bc826768d92736..8325d152ab09c4 100644
--- a/src/async_wrap.h
+++ b/src/async_wrap.h
@@ -44,6 +44,7 @@ namespace node {
   V(HTTP2SESSION)                                                             \
   V(HTTP2STREAM)                                                              \
   V(HTTP2PING)                                                                \
+  V(HTTP2SETTINGS)                                                            \
   V(HTTPPARSER)                                                               \
   V(JSSTREAM)                                                                 \
   V(PIPECONNECTWRAP)                                                          \
diff --git a/src/env.h b/src/env.h
index 0d231ba447da3e..a2e5872a35cbe4 100644
--- a/src/env.h
+++ b/src/env.h
@@ -315,6 +315,7 @@ class ModuleWrap;
   V(domains_stack_array, v8::Array)                                           \
   V(http2ping_constructor_template, v8::ObjectTemplate)                       \
   V(http2stream_constructor_template, v8::ObjectTemplate)                     \
+  V(http2settings_constructor_template, v8::ObjectTemplate)                   \
   V(inspector_console_api_object, v8::Object)                                 \
   V(module_load_list_array, v8::Array)                                        \
   V(pbkdf2_constructor_template, v8::ObjectTemplate)                          \
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 56ef79f8a5da03..ba2a8a655fdac7 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -153,22 +153,28 @@ Http2Options::Http2Options(Environment* env) {
   if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS)) {
     SetMaxOutstandingPings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS]);
   }
+
+  // The HTTP2 specification places no limits on the number of HTTP2
+  // SETTINGS frames that can be sent. In order to prevent PINGS from being
+  // abused as an attack vector, however, we place a strict upper limit
+  // on the number of unacknowledged SETTINGS that can be sent at any given
+  // time.
+  if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS)) {
+    SetMaxOutstandingSettings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS]);
+  }
 }
 
-// The Http2Settings class is used to configure a SETTINGS frame that is
-// to be sent to the connected peer. The settings are set using a TypedArray
-// that is shared with the JavaScript side.
-Http2Settings::Http2Settings(Environment* env) : env_(env) {
+void Http2Session::Http2Settings::Init() {
   entries_.AllocateSufficientStorage(IDX_SETTINGS_COUNT);
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
-      env->http2_state()->settings_buffer;
+      env()->http2_state()->settings_buffer;
   uint32_t flags = buffer[IDX_SETTINGS_COUNT];
 
   size_t n = 0;
 
   if (flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_HEADER_TABLE_SIZE];
-    DEBUG_HTTP2("Http2Settings: setting header table size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session, "setting header table size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE;
     entries_[n].value = val;
     n++;
@@ -176,7 +182,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
 
   if (flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) {
     uint32_t val = buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS];
-    DEBUG_HTTP2("Http2Settings: setting max concurrent streams: %d\n", val);
+    DEBUG_HTTP2SESSION2(session, "setting max concurrent streams: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS;
     entries_[n].value = val;
     n++;
@@ -184,7 +190,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
 
   if (flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_MAX_FRAME_SIZE];
-    DEBUG_HTTP2("Http2Settings: setting max frame size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session, "setting max frame size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE;
     entries_[n].value = val;
     n++;
@@ -192,7 +198,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
 
   if (flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE];
-    DEBUG_HTTP2("Http2Settings: setting initial window size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session, "setting initial window size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
     entries_[n].value = val;
     n++;
@@ -200,7 +206,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
 
   if (flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
-    DEBUG_HTTP2("Http2Settings: setting max header list size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session, "setting max header list size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE;
     entries_[n].value = val;
     n++;
@@ -208,7 +214,7 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
 
   if (flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) {
     uint32_t val = buffer[IDX_SETTINGS_ENABLE_PUSH];
-    DEBUG_HTTP2("Http2Settings: setting enable push: %d\n", val);
+    DEBUG_HTTP2SESSION2(session, "setting enable push: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
     entries_[n].value = val;
     n++;
@@ -217,13 +223,46 @@ Http2Settings::Http2Settings(Environment* env) : env_(env) {
   count_ = n;
 }
 
+Http2Session::Http2Settings::Http2Settings(
+    Environment* env)
+        : AsyncWrap(env,
+                    env->http2settings_constructor_template()
+                        ->NewInstance(env->context())
+                            .ToLocalChecked(),
+                    AsyncWrap::PROVIDER_HTTP2SETTINGS),
+          session_(nullptr),
+          startTime_(0) {
+  Init();
+}
+
+// The Http2Settings class is used to configure a SETTINGS frame that is
+// to be sent to the connected peer. The settings are set using a TypedArray
+// that is shared with the JavaScript side.
+Http2Session::Http2Settings::Http2Settings(
+    Http2Session* session)
+        : AsyncWrap(session->env(),
+                    session->env()->http2settings_constructor_template()
+                        ->NewInstance(session->env()->context())
+                            .ToLocalChecked(),
+                    AsyncWrap::PROVIDER_HTTP2SETTINGS),
+          session_(session),
+          startTime_(uv_hrtime()) {
+  Init();
+}
+
+Http2Session::Http2Settings::~Http2Settings() {
+  if (!object().IsEmpty())
+    ClearWrap(object());
+  persistent().Reset();
+  CHECK(persistent().IsEmpty());
+}
 
 // Generates a Buffer that contains the serialized payload of a SETTINGS
 // frame. This can be used, for instance, to create the Base64-encoded
 // content of an Http2-Settings header field.
-inline Local<Value> Http2Settings::Pack() {
+inline Local<Value> Http2Session::Http2Settings::Pack() {
   const size_t len = count_ * 6;
-  Local<Value> buf = Buffer::New(env_, len).ToLocalChecked();
+  Local<Value> buf = Buffer::New(env(), len).ToLocalChecked();
   ssize_t ret =
       nghttp2_pack_settings_payload(
         reinterpret_cast<uint8_t*>(Buffer::Data(buf)), len,
@@ -231,14 +270,14 @@ inline Local<Value> Http2Settings::Pack() {
   if (ret >= 0)
     return buf;
   else
-    return Undefined(env_->isolate());
+    return Undefined(env()->isolate());
 }
 
 // Updates the shared TypedArray with the current remote or local settings for
 // the session.
-inline void Http2Settings::Update(Environment* env,
-                                  Http2Session* session,
-                                  get_setting fn) {
+inline void Http2Session::Http2Settings::Update(Environment* env,
+                                                Http2Session* session,
+                                                get_setting fn) {
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
       env->http2_state()->settings_buffer;
   buffer[IDX_SETTINGS_HEADER_TABLE_SIZE] =
@@ -256,7 +295,7 @@ inline void Http2Settings::Update(Environment* env,
 }
 
 // Initializes the shared TypedArray with the default settings values.
-inline void Http2Settings::RefreshDefaults(Environment* env) {
+inline void Http2Session::Http2Settings::RefreshDefaults(Environment* env) {
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
       env->http2_state()->settings_buffer;
 
@@ -279,6 +318,24 @@ inline void Http2Settings::RefreshDefaults(Environment* env) {
 }
 
 
+void Http2Session::Http2Settings::Send() {
+  Http2Scope h2scope(session_);
+  CHECK_EQ(nghttp2_submit_settings(**session_, NGHTTP2_FLAG_NONE,
+                                   *entries_, length()), 0);
+}
+
+void Http2Session::Http2Settings::Done(bool ack) {
+  uint64_t end = uv_hrtime();
+  double duration = (end - startTime_) / 1e6;
+
+  Local<Value> argv[2] = {
+    Boolean::New(env()->isolate(), ack),
+    Number::New(env()->isolate(), duration)
+  };
+  MakeCallback(env()->ondone_string(), arraysize(argv), argv);
+  delete this;
+}
+
 // The Http2Priority class initializes an appropriate nghttp2_priority_spec
 // struct used when either creating a stream or updating its priority
 // settings.
@@ -417,6 +474,7 @@ Http2Session::Http2Session(Environment* env,
           : std::max(maxHeaderPairs, 1);    // minimum # of response headers
 
   max_outstanding_pings_ = opts.GetMaxOutstandingPings();
+  max_outstanding_settings_ = opts.GetMaxOutstandingSettings();
 
   padding_strategy_ = opts.GetPaddingStrategy();
 
@@ -554,22 +612,6 @@ inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
 }
 
 
-// Sends a SETTINGS frame to the connected peer. This has the side effect of
-// changing the settings state within the nghttp2_session, but those will
-// only be considered active once the connected peer acknowledges the SETTINGS
-// frame.
-// Note: This *must* send a SETTINGS frame even if niv == 0
-inline void Http2Session::Settings(const nghttp2_settings_entry iv[],
-                                   size_t niv) {
-  DEBUG_HTTP2SESSION2(this, "submitting %d settings", niv);
-  Http2Scope h2scope(this);
-  // This will fail either if the system is out of memory, or if the settings
-  // values are not within the appropriate range. We should be catching the
-  // latter before it gets this far so crash in either case.
-  CHECK_EQ(nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, iv, niv), 0);
-}
-
-
 // Write data received from the i/o stream to the underlying nghttp2_session.
 // On each call to nghttp2_session_mem_recv, nghttp2 will begin calling the
 // various callback functions. Each of these will typically result in a call
@@ -1044,15 +1086,17 @@ inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
 
 // Called by OnFrameReceived when a complete SETTINGS frame has been received.
 inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
-  Isolate* isolate = env()->isolate();
-  HandleScope scope(isolate);
-  Local<Context> context = env()->context();
-  Context::Scope context_scope(context);
-
   bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
-
-  Local<Value> argv[1] = { Boolean::New(isolate, ack) };
-  MakeCallback(env()->onsettings_string(), arraysize(argv), argv);
+  if (ack) {
+    // If this is an acknowledgement, we should have an Http2Settings
+    // object for it.
+    Http2Settings* settings = PopSettings();
+    if (settings != nullptr)
+      settings->Done(true);
+  } else {
+    // Otherwise, notify the session about a new settings
+    MakeCallback(env()->onsettings_string(), 0, nullptr);
+  }
 }
 
 // Callback used when data has been written to the stream.
@@ -1954,7 +1998,7 @@ void HttpErrorString(const FunctionCallbackInfo<Value>& args) {
 // output for an HTTP2-Settings header field.
 void PackSettings(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
-  Http2Settings settings(env);
+  Http2Session::Http2Settings settings(env);
   args.GetReturnValue().Set(settings.Pack());
 }
 
@@ -1963,7 +2007,7 @@ void PackSettings(const FunctionCallbackInfo<Value>& args) {
 // default values.
 void RefreshDefaultSettings(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
-  Http2Settings::RefreshDefaults(env);
+  Http2Session::Http2Settings::RefreshDefaults(env);
 }
 
 // Sets the next stream ID the Http2Session. If successful, returns true.
@@ -2061,17 +2105,6 @@ void Http2Session::Destroy(const FunctionCallbackInfo<Value>& args) {
   session->Close(code, socketDestroyed);
 }
 
-// Submits a SETTINGS frame for the Http2Session
-void Http2Session::Settings(const FunctionCallbackInfo<Value>& args) {
-  Http2Session* session;
-  ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
-  Environment* env = session->env();
-
-  Http2Settings settings(env);
-  session->Http2Session::Settings(*settings, settings.length());
-  DEBUG_HTTP2SESSION(session, "settings submitted");
-}
-
 // Submits a new request on the Http2Session and returns either an error code
 // or the Http2Stream object.
 void Http2Session::Request(const FunctionCallbackInfo<Value>& args) {
@@ -2373,6 +2406,26 @@ void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
   args.GetReturnValue().Set(true);
 }
 
+// Submits a SETTINGS frame for the Http2Session
+void Http2Session::Settings(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  Http2Session* session;
+  ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+  Http2Session::Http2Settings* settings = new Http2Settings(session);
+  Local<Object> obj = settings->object();
+  obj->Set(env->context(), env->ondone_string(), args[0]).FromJust();
+
+  if (!session->AddSettings(settings)) {
+    settings->Done(false);
+    return args.GetReturnValue().Set(false);
+  }
+
+  settings->Send();
+  args.GetReturnValue().Set(true);
+}
+
+
 Http2Session::Http2Ping* Http2Session::PopPing() {
   Http2Ping* ping = nullptr;
   if (!outstanding_pings_.empty()) {
@@ -2389,6 +2442,21 @@ bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
   return true;
 }
 
+Http2Session::Http2Settings* Http2Session::PopSettings() {
+  Http2Settings* settings = nullptr;
+  if (!outstanding_settings_.empty()) {
+    settings = outstanding_settings_.front();
+    outstanding_settings_.pop();
+  }
+  return settings;
+}
+
+bool Http2Session::AddSettings(Http2Session::Http2Settings* settings) {
+  if (outstanding_settings_.size() == max_outstanding_settings_)
+    return false;
+  outstanding_settings_.push(settings);
+  return true;
+}
 
 Http2Session::Http2Ping::Http2Ping(
     Http2Session* session)
@@ -2488,6 +2556,13 @@ void Initialize(Local<Object> target,
   pingt->SetInternalFieldCount(1);
   env->set_http2ping_constructor_template(pingt);
 
+  Local<FunctionTemplate> setting = FunctionTemplate::New(env->isolate());
+  setting->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Setting"));
+  AsyncWrap::AddWrapMethods(env, setting);
+  Local<ObjectTemplate> settingt = setting->InstanceTemplate();
+  settingt->SetInternalFieldCount(1);
+  env->set_http2settings_constructor_template(settingt);
+
   Local<FunctionTemplate> stream = FunctionTemplate::New(env->isolate());
   stream->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "Http2Stream"));
   env->SetProtoMethod(stream, "id", Http2Stream::GetID);
diff --git a/src/node_http2.h b/src/node_http2.h
index 8cccfb342db3e0..a207fc07432caa 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -76,6 +76,9 @@ void inline debug_vfprintf(const char* format, ...) {
 // option.
 #define DEFAULT_MAX_PINGS 10
 
+// Also strictly limit the number of outstanding SETTINGS frames a user sends
+#define DEFAULT_MAX_SETTINGS 10
+
 // These are the standard HTTP/2 defaults as specified by the RFC
 #define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
 #define DEFAULT_SETTINGS_ENABLE_PUSH 1
@@ -483,41 +486,20 @@ class Http2Options {
     return max_outstanding_pings_;
   }
 
+  void SetMaxOutstandingSettings(size_t max) {
+    max_outstanding_settings_ = max;
+  }
+
+  size_t GetMaxOutstandingSettings() {
+    return max_outstanding_settings_;
+  }
+
  private:
   nghttp2_option* options_;
   uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
   padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
   size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
-};
-
-// The Http2Settings class is used to parse the settings passed in for
-// an Http2Session, converting those into an array of nghttp2_settings_entry
-// structs.
-class Http2Settings {
- public:
-  explicit Http2Settings(Environment* env);
-
-  size_t length() const { return count_; }
-
-  nghttp2_settings_entry* operator*() {
-    return *entries_;
-  }
-
-  // Returns a Buffer instance with the serialized SETTINGS payload
-  inline Local<Value> Pack();
-
-  // Resets the default values in the settings buffer
-  static inline void RefreshDefaults(Environment* env);
-
-  // Update the local or remote settings for the given session
-  static inline void Update(Environment* env,
-                            Http2Session* session,
-                            get_setting fn);
-
- private:
-  Environment* env_;
-  size_t count_ = 0;
-  MaybeStackBuffer<nghttp2_settings_entry, IDX_SETTINGS_COUNT> entries_;
+  size_t max_outstanding_settings_ = DEFAULT_MAX_SETTINGS;
 };
 
 class Http2Priority {
@@ -792,6 +774,7 @@ class Http2Session : public AsyncWrap {
   ~Http2Session() override;
 
   class Http2Ping;
+  class Http2Settings;
 
   void Start();
   void Stop();
@@ -841,9 +824,6 @@ class Http2Session : public AsyncWrap {
   // Removes a stream instance from this session
   inline void RemoveStream(int32_t id);
 
-  // Submits a SETTINGS frame to the connected peer.
-  inline void Settings(const nghttp2_settings_entry iv[], size_t niv);
-
   // Write data to the session
   inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
 
@@ -896,6 +876,9 @@ class Http2Session : public AsyncWrap {
   Http2Ping* PopPing();
   bool AddPing(Http2Ping* ping);
 
+  Http2Settings* PopSettings();
+  bool AddSettings(Http2Settings* settings);
+
  private:
   // Frame Padding Strategies
   inline ssize_t OnMaxFrameSizePadding(size_t frameLength,
@@ -1026,6 +1009,9 @@ class Http2Session : public AsyncWrap {
   size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
   std::queue<Http2Ping*> outstanding_pings_;
 
+  size_t max_outstanding_settings_ = DEFAULT_MAX_SETTINGS;
+  std::queue<Http2Settings*> outstanding_settings_;
+
   std::vector<nghttp2_stream_write> outgoing_buffers_;
   std::vector<uint8_t> outgoing_storage_;
 
@@ -1050,6 +1036,45 @@ class Http2Session::Http2Ping : public AsyncWrap {
   uint64_t startTime_;
 };
 
+// The Http2Settings class is used to parse the settings passed in for
+// an Http2Session, converting those into an array of nghttp2_settings_entry
+// structs.
+class Http2Session::Http2Settings : public AsyncWrap {
+ public:
+  explicit Http2Settings(Environment* env);
+  explicit Http2Settings(Http2Session* session);
+  ~Http2Settings();
+
+  size_t self_size() const override { return sizeof(*this); }
+
+  void Send();
+  void Done(bool ack);
+
+  size_t length() const { return count_; }
+
+  nghttp2_settings_entry* operator*() {
+    return *entries_;
+  }
+
+  // Returns a Buffer instance with the serialized SETTINGS payload
+  inline Local<Value> Pack();
+
+  // Resets the default values in the settings buffer
+  static inline void RefreshDefaults(Environment* env);
+
+  // Update the local or remote settings for the given session
+  static inline void Update(Environment* env,
+                            Http2Session* session,
+                            get_setting fn);
+
+ private:
+  void Init();
+  Http2Session* session_;
+  uint64_t startTime_;
+  size_t count_ = 0;
+  MaybeStackBuffer<nghttp2_settings_entry, IDX_SETTINGS_COUNT> entries_;
+};
+
 class ExternalHeader :
     public String::ExternalOneByteStringResource {
  public:
diff --git a/src/node_http2_state.h b/src/node_http2_state.h
index a7ad23fb519886..ef8696ce60d8f8 100644
--- a/src/node_http2_state.h
+++ b/src/node_http2_state.h
@@ -49,6 +49,7 @@ namespace http2 {
     IDX_OPTIONS_PADDING_STRATEGY,
     IDX_OPTIONS_MAX_HEADER_LIST_PAIRS,
     IDX_OPTIONS_MAX_OUTSTANDING_PINGS,
+    IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS,
     IDX_OPTIONS_FLAGS
   };
 
diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js
index 239e2fa76c9c02..75fcc1942104ac 100644
--- a/test/parallel/test-http2-session-settings.js
+++ b/test/parallel/test-http2-session-settings.js
@@ -77,9 +77,7 @@ server.listen(
     // State will only be valid after connect event is emitted
     req.on('ready', common.mustCall(() => {
       assert.doesNotThrow(() => {
-        client.settings({
-          maxHeaderListSize: 1
-        });
+        client.settings({ maxHeaderListSize: 1 }, common.mustCall());
       });
 
       // Verify valid error ranges
diff --git a/test/parallel/test-http2-too-many-settings.js b/test/parallel/test-http2-too-many-settings.js
index c4f101e8224161..0302fe623da07c 100644
--- a/test/parallel/test-http2-too-many-settings.js
+++ b/test/parallel/test-http2-too-many-settings.js
@@ -1,7 +1,7 @@
 'use strict';
 
 // Tests that attempting to send too many non-acknowledged
-// settings frames will result in a throw.
+// settings frames will result in an error
 
 const common = require('../common');
 if (!common.hasCrypto)
@@ -9,53 +9,41 @@ if (!common.hasCrypto)
 const assert = require('assert');
 const h2 = require('http2');
 
-const maxPendingAck = 2;
-const server = h2.createServer({ maxPendingAck: maxPendingAck + 1 });
-
-let clients = 2;
+const maxOutstandingSettings = 2;
 
 function doTest(session) {
-  for (let n = 0; n < maxPendingAck; n++)
-    assert.doesNotThrow(() => session.settings({ enablePush: false }));
-  assert.throws(() => session.settings({ enablePush: false }),
-                common.expectsError({
-                  code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
-                  type: Error
-                }));
+  session.on('error', common.expectsError({
+    code: 'ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
+    type: Error
+  }));
+  for (let n = 0; n < maxOutstandingSettings; n++) {
+    session.settings({ enablePush: false });
+    assert.strictEqual(session.pendingSettingsAck, true);
+  }
 }
 
-server.on('stream', common.mustNotCall());
-
-server.once('session', common.mustCall((session) => doTest(session)));
-
-server.listen(0);
-
-const closeServer = common.mustCall(() => {
-  if (--clients === 0)
-    server.close();
-}, clients);
-
-server.on('listening', common.mustCall(() => {
-  const client = h2.connect(`http://localhost:${server.address().port}`,
-                            { maxPendingAck: maxPendingAck + 1 });
-  let remaining = maxPendingAck + 1;
-
-  client.on('close', closeServer);
-  client.on('localSettings', common.mustCall(() => {
-    if (--remaining <= 0) {
-      client.close();
-    }
-  }, maxPendingAck + 1));
-  client.on('connect', common.mustCall(() => doTest(client)));
-}));
+{
+  const server = h2.createServer({ maxOutstandingSettings });
+  server.on('stream', common.mustNotCall());
+  server.once('session', common.mustCall((session) => doTest(session)));
+
+  server.listen(0, common.mustCall(() => {
+    const client = h2.connect(`http://localhost:${server.address().port}`);
+    // On some operating systems, an ECONNRESET error may be emitted.
+    // On others it won't be. Do not make this a mustCall
+    client.on('error', () => {});
+    client.on('close', common.mustCall(() => server.close()));
+  }));
+}
 
-// Setting maxPendingAck to 0, defaults it to 1
-server.on('listening', common.mustCall(() => {
-  const client = h2.connect(`http://localhost:${server.address().port}`,
-                            { maxPendingAck: 0 });
+{
+  const server = h2.createServer();
+  server.on('stream', common.mustNotCall());
 
-  client.on('close', closeServer);
-  client.on('localSettings', common.mustCall(() => {
-    client.close();
+  server.listen(0, common.mustCall(() => {
+    const client = h2.connect(`http://localhost:${server.address().port}`,
+                              { maxOutstandingSettings });
+    client.on('connect', () => doTest(client));
+    client.on('close', () => server.close());
   }));
-}));
+}
diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js
index 14e4ba23d8fdca..5768ce0204dc8c 100644
--- a/test/parallel/test-http2-util-update-options-buffer.js
+++ b/test/parallel/test-http2-util-update-options-buffer.js
@@ -19,7 +19,8 @@ const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3;
 const IDX_OPTIONS_PADDING_STRATEGY = 4;
 const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
 const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
-const IDX_OPTIONS_FLAGS = 7;
+const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
+const IDX_OPTIONS_FLAGS = 8;
 
 {
   updateOptionsBuffer({
@@ -29,7 +30,8 @@ const IDX_OPTIONS_FLAGS = 7;
     peerMaxConcurrentStreams: 4,
     paddingStrategy: 5,
     maxHeaderListPairs: 6,
-    maxOutstandingPings: 7
+    maxOutstandingPings: 7,
+    maxOutstandingSettings: 8
   });
 
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -39,6 +41,7 @@ const IDX_OPTIONS_FLAGS = 7;
   strictEqual(optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY], 5);
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7);
+  strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS], 8);
 
   const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
 
@@ -49,6 +52,7 @@ const IDX_OPTIONS_FLAGS = 7;
   ok(flags & (1 << IDX_OPTIONS_PADDING_STRATEGY));
   ok(flags & (1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS));
   ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS));
+  ok(flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS));
 }
 
 {
diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js
index 9c4f7d58f662e0..c8c25725957824 100644
--- a/test/sequential/test-async-wrap-getasyncid.js
+++ b/test/sequential/test-async-wrap-getasyncid.js
@@ -26,6 +26,7 @@ const tmpdir = require('../common/tmpdir');
     delete providers.HTTP2SESSION;
     delete providers.HTTP2STREAM;
     delete providers.HTTP2PING;
+    delete providers.HTTP2SETTINGS;
 
     const obj_keys = Object.keys(providers);
     if (obj_keys.length > 0)

From 5036d0fdf500679ebe6691de5165e1a211467224 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Mon, 25 Dec 2017 12:21:58 +0100
Subject: [PATCH 16/77] http2: fix compiling with `--debug-http2`

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17863
Fixes: https://github.com/nodejs/node/issues/17840
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
---
 src/node_http2.cc | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index ba2a8a655fdac7..c86a1e54b66a30 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -174,7 +174,7 @@ void Http2Session::Http2Settings::Init() {
 
   if (flags & (1 << IDX_SETTINGS_HEADER_TABLE_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_HEADER_TABLE_SIZE];
-    DEBUG_HTTP2SESSION2(session, "setting header table size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session_, "setting header table size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_HEADER_TABLE_SIZE;
     entries_[n].value = val;
     n++;
@@ -182,7 +182,7 @@ void Http2Session::Http2Settings::Init() {
 
   if (flags & (1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS)) {
     uint32_t val = buffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS];
-    DEBUG_HTTP2SESSION2(session, "setting max concurrent streams: %d\n", val);
+    DEBUG_HTTP2SESSION2(session_, "setting max concurrent streams: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS;
     entries_[n].value = val;
     n++;
@@ -190,7 +190,7 @@ void Http2Session::Http2Settings::Init() {
 
   if (flags & (1 << IDX_SETTINGS_MAX_FRAME_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_MAX_FRAME_SIZE];
-    DEBUG_HTTP2SESSION2(session, "setting max frame size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session_, "setting max frame size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_FRAME_SIZE;
     entries_[n].value = val;
     n++;
@@ -198,7 +198,7 @@ void Http2Session::Http2Settings::Init() {
 
   if (flags & (1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE];
-    DEBUG_HTTP2SESSION2(session, "setting initial window size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session_, "setting initial window size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE;
     entries_[n].value = val;
     n++;
@@ -206,7 +206,7 @@ void Http2Session::Http2Settings::Init() {
 
   if (flags & (1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE)) {
     uint32_t val = buffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE];
-    DEBUG_HTTP2SESSION2(session, "setting max header list size: %d\n", val);
+    DEBUG_HTTP2SESSION2(session_, "setting max header list size: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE;
     entries_[n].value = val;
     n++;
@@ -214,7 +214,7 @@ void Http2Session::Http2Settings::Init() {
 
   if (flags & (1 << IDX_SETTINGS_ENABLE_PUSH)) {
     uint32_t val = buffer[IDX_SETTINGS_ENABLE_PUSH];
-    DEBUG_HTTP2SESSION2(session, "setting enable push: %d\n", val);
+    DEBUG_HTTP2SESSION2(session_, "setting enable push: %d\n", val);
     entries_[n].settings_id = NGHTTP2_SETTINGS_ENABLE_PUSH;
     entries_[n].value = val;
     n++;

From 3490359ac18f5fcf05b7bedcd15248ce6041c26d Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Mon, 25 Dec 2017 20:16:54 +0100
Subject: [PATCH 17/77] http2: keep session objects alive during Http2Scope

Keep a local handle as a reference to the JS `Http2Session`
object so that it will not be garbage collected
when inside an `Http2Scope`, because the presence of the
latter usually indicates that further actions on
the session object are expected.

Strictly speaking, storing the `session_handle_` as a
property on the scope object is not necessary, but
this is not very costly and makes the code more
obviously correct.

Fixes: https://github.com/nodejs/node/issues/17840

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17863
Fixes: https://github.com/nodejs/node/issues/17840
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
---
 src/node_http2.cc                          |  7 +++++++
 src/node_http2.h                           |  1 +
 test/parallel/test-http2-createwritereq.js | 11 +++++++++++
 3 files changed, 19 insertions(+)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index c86a1e54b66a30..34c96b04e8c026 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -71,6 +71,11 @@ Http2Scope::Http2Scope(Http2Session* session) {
   }
   session->flags_ |= SESSION_STATE_HAS_SCOPE;
   session_ = session;
+
+  // Always keep the session object alive for at least as long as
+  // this scope is active.
+  session_handle_ = session->object();
+  CHECK(!session_handle_.IsEmpty());
 }
 
 Http2Scope::~Http2Scope() {
@@ -512,6 +517,7 @@ void Http2Session::Unconsume() {
 }
 
 Http2Session::~Http2Session() {
+  CHECK_EQ(flags_ & SESSION_STATE_HAS_SCOPE, 0);
   if (!object().IsEmpty())
     ClearWrap(object());
   persistent().Reset();
@@ -1365,6 +1371,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
                                     void* ctx) {
   Http2Session* session = static_cast<Http2Session*>(ctx);
   Http2Scope h2scope(session);
+  CHECK_NE(session->stream_, nullptr);
   DEBUG_HTTP2SESSION2(session, "receiving %d bytes", nread);
   if (nread < 0) {
     uv_buf_t tmp_buf;
diff --git a/src/node_http2.h b/src/node_http2.h
index a207fc07432caa..fe8c3f4a5ee72f 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -444,6 +444,7 @@ class Http2Scope {
 
  private:
   Http2Session* session_ = nullptr;
+  Local<Object> session_handle_;
 };
 
 // The Http2Options class is used to parse the options object passed in to
diff --git a/test/parallel/test-http2-createwritereq.js b/test/parallel/test-http2-createwritereq.js
index 40d0ddc117c5ad..1d2b31676284d0 100644
--- a/test/parallel/test-http2-createwritereq.js
+++ b/test/parallel/test-http2-createwritereq.js
@@ -1,5 +1,7 @@
 'use strict';
 
+// Flags: --expose-gc
+
 const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
@@ -62,6 +64,15 @@ server.listen(0, common.mustCall(function() {
       }
     }));
 
+    // Ref: https://github.com/nodejs/node/issues/17840
+    const origDestroy = req.destroy;
+    req.destroy = function(...args) {
+      // Schedule a garbage collection event at the end of the current
+      // MakeCallback() run.
+      process.nextTick(global.gc);
+      return origDestroy.call(this, ...args);
+    };
+
     req.end();
   });
 }));

From 3bf3eb828d247677f7dc97b559c99dd33113c595 Mon Sep 17 00:00:00 2001
From: Kelvin Jin <kelvinjin@google.com>
Date: Mon, 11 Dec 2017 15:05:18 -0800
Subject: [PATCH 18/77] http2: implement ref() and unref() on client sessions

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17620
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 doc/api/http2.md                          | 153 +++++++++++++---------
 lib/internal/http2/core.js                |  12 ++
 lib/net.js                                |   8 +-
 test/parallel/test-http2-session-unref.js |  53 ++++++++
 4 files changed, 159 insertions(+), 67 deletions(-)
 create mode 100644 test/parallel/test-http2-session-unref.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 7cb43babc3b7dd..d9f44d2a43317a 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -413,78 +413,23 @@ session.ping(Buffer.from('abcdefgh'), (err, duration, payload) => {
 If the `payload` argument is not specified, the default payload will be the
 64-bit timestamp (little endian) marking the start of the `PING` duration.
 
-#### http2session.remoteSettings
+#### http2session.ref()
 <!-- YAML
-added: v8.4.0
+added: REPLACEME
 -->
 
-* Value: {[Settings Object][]}
+Calls [`ref()`][`net.Socket.prototype.ref`] on this `Http2Session`
+instance's underlying [`net.Socket`].
 
-A prototype-less object describing the current remote settings of this
-`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
-
-#### http2session.request(headers[, options])
+#### http2session.remoteSettings
 <!-- YAML
 added: v8.4.0
 -->
 
-* `headers` {[Headers Object][]}
-* `options` {Object}
-  * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should
-    be closed initially, such as when sending a `GET` request that should not
-    expect a payload body.
-  * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
-    the created stream is made the sole direct dependency of the parent, with
-    all other existing dependents made a dependent of the newly created stream.
-    **Default:** `false`
-  * `parent` {number} Specifies the numeric identifier of a stream the newly
-    created stream is dependent on.
-  * `weight` {number} Specifies the relative dependency of a stream in relation
-    to other streams with the same `parent`. The value is a number between `1`
-    and `256` (inclusive).
-  * `getTrailers` {Function} Callback function invoked to collect trailer
-    headers.
-
-* Returns: {ClientHttp2Stream}
-
-For HTTP/2 Client `Http2Session` instances only, the `http2session.request()`
-creates and returns an `Http2Stream` instance that can be used to send an
-HTTP/2 request to the connected server.
-
-This method is only available if `http2session.type` is equal to
-`http2.constants.NGHTTP2_SESSION_CLIENT`.
-
-```js
-const http2 = require('http2');
-const clientSession = http2.connect('https://localhost:1234');
-const {
-  HTTP2_HEADER_PATH,
-  HTTP2_HEADER_STATUS
-} = http2.constants;
-
-const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' });
-req.on('response', (headers) => {
-  console.log(headers[HTTP2_HEADER_STATUS]);
-  req.on('data', (chunk) => { /** .. **/ });
-  req.on('end', () => { /** .. **/ });
-});
-```
-
-When set, the `options.getTrailers()` function is called immediately after
-queuing the last chunk of payload data to be sent. The callback is passed a
-single object (with a `null` prototype) that the listener may used to specify
-the trailing header fields to send to the peer.
-
-*Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
-"pseudo-header" fields (e.g. `':method'`, `':path'`, etc). An `'error'` event
-will be emitted if the `getTrailers` callback attempts to set such header
-fields.
-
-The the `:method` and `:path` pseudoheaders are not specified within `headers`,
-they respectively default to:
+* Value: {[Settings Object][]}
 
-* `:method` = `'GET'`
-* `:path` = `/`
+A prototype-less object describing the current remote settings of this
+`Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
 
 #### http2session.setTimeout(msecs, callback)
 <!-- YAML
@@ -617,6 +562,82 @@ The `http2session.type` will be equal to
 server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a
 client.
 
+#### http2session.unref()
+<!-- YAML
+added: REPLACEME
+-->
+
+Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session`
+instance's underlying [`net.Socket`].
+
+### Class: ClientHttp2Session
+<!-- YAML
+added: v8.4.0
+-->
+
+#### clienthttp2session.request(headers[, options])
+<!-- YAML
+added: v8.4.0
+-->
+
+* `headers` {[Headers Object][]}
+* `options` {Object}
+  * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should
+    be closed initially, such as when sending a `GET` request that should not
+    expect a payload body.
+  * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
+    the created stream is made the sole direct dependency of the parent, with
+    all other existing dependents made a dependent of the newly created stream.
+    **Default:** `false`
+  * `parent` {number} Specifies the numeric identifier of a stream the newly
+    created stream is dependent on.
+  * `weight` {number} Specifies the relative dependency of a stream in relation
+    to other streams with the same `parent`. The value is a number between `1`
+    and `256` (inclusive).
+  * `getTrailers` {Function} Callback function invoked to collect trailer
+    headers.
+
+* Returns: {ClientHttp2Stream}
+
+For HTTP/2 Client `Http2Session` instances only, the `http2session.request()`
+creates and returns an `Http2Stream` instance that can be used to send an
+HTTP/2 request to the connected server.
+
+This method is only available if `http2session.type` is equal to
+`http2.constants.NGHTTP2_SESSION_CLIENT`.
+
+```js
+const http2 = require('http2');
+const clientSession = http2.connect('https://localhost:1234');
+const {
+  HTTP2_HEADER_PATH,
+  HTTP2_HEADER_STATUS
+} = http2.constants;
+
+const req = clientSession.request({ [HTTP2_HEADER_PATH]: '/' });
+req.on('response', (headers) => {
+  console.log(headers[HTTP2_HEADER_STATUS]);
+  req.on('data', (chunk) => { /** .. **/ });
+  req.on('end', () => { /** .. **/ });
+});
+```
+
+When set, the `options.getTrailers()` function is called immediately after
+queuing the last chunk of payload data to be sent. The callback is passed a
+single object (with a `null` prototype) that the listener may used to specify
+the trailing header fields to send to the peer.
+
+*Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
+"pseudo-header" fields (e.g. `':method'`, `':path'`, etc). An `'error'` event
+will be emitted if the `getTrailers` callback attempts to set such header
+fields.
+
+The `:method` and `:path` pseudoheaders are not specified within `headers`,
+they respectively default to:
+
+* `:method` = `'GET'`
+* `:path` = `/`
+
 ### Class: Http2Stream
 <!-- YAML
 added: v8.4.0
@@ -1688,9 +1709,9 @@ changes:
     [`Duplex`][] stream that is to be used as the connection for this session.
   * ...: Any [`net.connect()`][] or [`tls.connect()`][] options can be provided.
 * `listener` {Function}
-* Returns: {Http2Session}
+* Returns {ClientHttp2Session}
 
-Returns a HTTP/2 client `Http2Session` instance.
+Returns a `ClientHttp2Session` instance.
 
 ```js
 const http2 = require('http2');
@@ -2825,6 +2846,8 @@ if the stream is closed.
 [`http2.createServer()`]: #http2_http2_createserver_options_onrequesthandler
 [`http2stream.pushStream()`]: #http2_http2stream_pushstream_headers_options_callback
 [`net.Socket`]: net.html#net_class_net_socket
+[`net.Socket.prototype.ref`]: net.html#net_socket_ref
+[`net.Socket.prototype.unref`]: net.html#net_socket_unref
 [`net.connect()`]: net.html#net_net_connect
 [`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed
 [`response.end()`]: #http2_response_end_data_encoding_callback
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 1fc0c6ce793c70..3c84732169a07a 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1124,6 +1124,18 @@ class Http2Session extends EventEmitter {
 
     process.nextTick(emit, this, 'timeout');
   }
+
+  ref() {
+    if (this[kSocket]) {
+      this[kSocket].ref();
+    }
+  }
+
+  unref() {
+    if (this[kSocket]) {
+      this[kSocket].unref();
+    }
+  }
 }
 
 // ServerHttp2Session instances should never have to wait for the socket
diff --git a/lib/net.js b/lib/net.js
index 0f7586f9f748ac..53c91a640f5c1d 100644
--- a/lib/net.js
+++ b/lib/net.js
@@ -1125,7 +1125,9 @@ Socket.prototype.ref = function() {
     return this;
   }
 
-  this._handle.ref();
+  if (typeof this._handle.ref === 'function') {
+    this._handle.ref();
+  }
 
   return this;
 };
@@ -1137,7 +1139,9 @@ Socket.prototype.unref = function() {
     return this;
   }
 
-  this._handle.unref();
+  if (typeof this._handle.unref === 'function') {
+    this._handle.unref();
+  }
 
   return this;
 };
diff --git a/test/parallel/test-http2-session-unref.js b/test/parallel/test-http2-session-unref.js
new file mode 100644
index 00000000000000..e765352cdc615d
--- /dev/null
+++ b/test/parallel/test-http2-session-unref.js
@@ -0,0 +1,53 @@
+'use strict';
+// Flags: --expose-internals
+
+// Tests that calling unref() on Http2Session:
+// (1) Prevents it from keeping the process alive
+// (2) Doesn't crash
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const http2 = require('http2');
+const makeDuplexPair = require('../common/duplexpair');
+
+const server = http2.createServer();
+const { clientSide, serverSide } = makeDuplexPair();
+
+// 'session' event should be emitted 3 times:
+// - the vanilla client
+// - the destroyed client
+// - manual 'connection' event emission with generic Duplex stream
+server.on('session', common.mustCallAtLeast((session) => {
+  session.unref();
+}, 3));
+
+server.listen(0, common.mustCall(() => {
+  const port = server.address().port;
+
+  // unref new client
+  {
+    const client = http2.connect(`http://localhost:${port}`);
+    client.unref();
+  }
+
+  // unref destroyed client
+  {
+    const client = http2.connect(`http://localhost:${port}`);
+    client.destroy();
+    client.unref();
+  }
+
+  // unref destroyed client
+  {
+    const client = http2.connect(`http://localhost:${port}`, {
+      createConnection: common.mustCall(() => clientSide)
+    });
+    client.destroy();
+    client.unref();
+  }
+}));
+server.emit('connection', serverSide);
+server.unref();
+
+setTimeout(common.mustNotCall(() => {}), 1000).unref();

From e92be6a533aacc1eef9c670584e8b9401d405c44 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Tue, 19 Dec 2017 17:40:41 -0800
Subject: [PATCH 19/77] http2: perf_hooks integration

Collect and report basic timing information about `Http2Session`
and `Http2Stream` instances.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17906
Refs: https://github.com/nodejs/node/issues/17746
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/http2.md                       |  50 ++++++++++++
 lib/perf_hooks.js                      |   5 +-
 src/node_http2.cc                      | 104 ++++++++++++++++++++++++-
 src/node_http2.h                       |  85 ++++++++++++++++++++
 src/node_perf.cc                       |   9 +--
 src/node_perf.h                        |   6 +-
 src/node_perf_common.h                 |   3 +-
 test/parallel/test-http2-perf_hooks.js |  95 ++++++++++++++++++++++
 8 files changed, 344 insertions(+), 13 deletions(-)
 create mode 100644 test/parallel/test-http2-perf_hooks.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index d9f44d2a43317a..23bfac059c7321 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -2819,6 +2819,55 @@ given newly created [`Http2Stream`] on `Http2ServerRespose`.
 The callback will be called with an error with code `ERR_HTTP2_STREAM_CLOSED`
 if the stream is closed.
 
+## Collecting HTTP/2 Performance Metrics
+
+The [Performance Observer][] API can be used to collect basic performance
+metrics for each `Http2Session` and `Http2Stream` instance.
+
+```js
+const { PerformanceObserver } = require('perf_hooks');
+
+const obs = new PerformanceObserver((items) => {
+  const entry = items.getEntries()[0];
+  console.log(entry.entryType);  // prints 'http2'
+  if (entry.name === 'Http2Session') {
+    // entry contains statistics about the Http2Session
+  } else if (entry.name === 'Http2Stream') {
+    // entry contains statistics about the Http2Stream
+  }
+});
+obs.observe({ entryTypes: ['http2'] });
+```
+
+The `entryType` property of the `PerformanceEntry` will be equal to `'http2'`.
+
+The `name` property of the `PerformanceEntry` will be equal to either
+`'Http2Stream'` or `'Http2Session'`.
+
+If `name` is equal to `Http2Stream`, the `PerformanceEntry` will contain the
+following additional properties:
+
+* `timeToFirstByte` {number} The number of milliseconds elapsed between the
+  `PerformanceEntry` `startTime` and the reception of the first `DATA` frame.
+* `timeToFirstHeader` {number} The number of milliseconds elapsed between the
+  `PerformanceEntry` `startTime` and the reception of the first header.
+
+If `name` is equal to `Http2Session`, the `PerformanceEntry` will contain the
+following additional properties:
+
+* `pingRTT` {number} The number of milliseconds elapsed since the transmission
+  of a `PING` frame and the reception of its acknowledgement. Only present if
+  a `PING` frame has been sent on the `Http2Session`.
+* `streamCount` {number} The number of `Http2Stream` instances processed by
+  the `Http2Session`.
+* `streamAverageDuration` {number} The average duration (in milliseconds) for
+  all `Http2Stream` instances.
+* `framesReceived` {number} The number of HTTP/2 frames received by the
+  `Http2Session`.
+* `type` {string} Either `'server'` or `'client'` to identify the type of
+  `Http2Session`.
+
+
 [ALPN negotiation]: #http2_alpn_negotiation
 [Compatibility API]: #http2_compatibility_api
 [HTTP/1]: http.html
@@ -2826,6 +2875,7 @@ if the stream is closed.
 [HTTPS]: https.html
 [Headers Object]: #http2_headers_object
 [Http2Session and Sockets]: #http2_http2session_and_sockets
+[Performance Observer]: perf_hooks.html
 [Readable Stream]: stream.html#stream_class_stream_readable
 [Settings Object]: #http2_settings_object
 [Using options.selectPadding]: #http2_using_options_selectpadding
diff --git a/lib/perf_hooks.js b/lib/perf_hooks.js
index 4e7a0de7eb37be..7e1d085b525b44 100644
--- a/lib/perf_hooks.js
+++ b/lib/perf_hooks.js
@@ -18,6 +18,7 @@ const {
   NODE_PERFORMANCE_ENTRY_TYPE_MEASURE,
   NODE_PERFORMANCE_ENTRY_TYPE_GC,
   NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION,
+  NODE_PERFORMANCE_ENTRY_TYPE_HTTP2,
 
   NODE_PERFORMANCE_MILESTONE_NODE_START,
   NODE_PERFORMANCE_MILESTONE_V8_START,
@@ -61,7 +62,8 @@ const observerableTypes = [
   'mark',
   'measure',
   'gc',
-  'function'
+  'function',
+  'http2'
 ];
 
 let errors;
@@ -504,6 +506,7 @@ function mapTypes(i) {
     case 'measure': return NODE_PERFORMANCE_ENTRY_TYPE_MEASURE;
     case 'gc': return NODE_PERFORMANCE_ENTRY_TYPE_GC;
     case 'function': return NODE_PERFORMANCE_ENTRY_TYPE_FUNCTION;
+    case 'http2': return NODE_PERFORMANCE_ENTRY_TYPE_HTTP2;
   }
 }
 
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 34c96b04e8c026..442fa64b6ca981 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -3,6 +3,7 @@
 #include "node_buffer.h"
 #include "node_http2.h"
 #include "node_http2_state.h"
+#include "node_perf.h"
 
 #include <algorithm>
 
@@ -20,6 +21,7 @@ using v8::Uint32;
 using v8::Uint32Array;
 using v8::Undefined;
 
+using node::performance::PerformanceEntry;
 namespace http2 {
 
 namespace {
@@ -468,6 +470,7 @@ Http2Session::Http2Session(Environment* env,
     : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTP2SESSION),
       session_type_(type) {
   MakeWeak<Http2Session>(this);
+  statistics_.start_time = uv_hrtime();
 
   // Capture the configuration options for this session
   Http2Options opts(env);
@@ -527,6 +530,86 @@ Http2Session::~Http2Session() {
   nghttp2_session_del(session_);
 }
 
+inline bool HasHttp2Observer(Environment* env) {
+  uint32_t* observers = env->performance_state()->observers;
+  return observers[performance::NODE_PERFORMANCE_ENTRY_TYPE_HTTP2] != 0;
+}
+
+inline void Http2Stream::EmitStatistics() {
+  if (!HasHttp2Observer(env()))
+    return;
+  Http2StreamPerformanceEntry* entry =
+    new Http2StreamPerformanceEntry(env(), statistics_);
+  env()->SetImmediate([](Environment* env, void* data) {
+    Local<Context> context = env->context();
+    Http2StreamPerformanceEntry* entry =
+      static_cast<Http2StreamPerformanceEntry*>(data);
+    if (HasHttp2Observer(env)) {
+      Local<Object> obj = entry->ToObject();
+      v8::PropertyAttribute attr =
+          static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
+      obj->DefineOwnProperty(
+          context,
+          FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstByte"),
+          Number::New(env->isolate(),
+                      (entry->first_byte() - entry->startTimeNano()) / 1e6),
+          attr);
+      obj->DefineOwnProperty(
+          context,
+          FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstHeader"),
+          Number::New(env->isolate(),
+                      (entry->first_header() - entry->startTimeNano()) / 1e6),
+          attr);
+      entry->Notify(obj);
+    }
+    delete entry;
+  }, static_cast<void*>(entry));
+}
+
+inline void Http2Session::EmitStatistics() {
+  if (!HasHttp2Observer(env()))
+    return;
+  Http2SessionPerformanceEntry* entry =
+    new Http2SessionPerformanceEntry(env(), statistics_, TypeName());
+  env()->SetImmediate([](Environment* env, void* data) {
+    Local<Context> context = env->context();
+    Http2SessionPerformanceEntry* entry =
+      static_cast<Http2SessionPerformanceEntry*>(data);
+    if (HasHttp2Observer(env)) {
+      Local<Object> obj = entry->ToObject();
+      v8::PropertyAttribute attr =
+          static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
+      obj->DefineOwnProperty(
+          context,
+          FIXED_ONE_BYTE_STRING(env->isolate(), "type"),
+          String::NewFromUtf8(env->isolate(),
+                              entry->typeName(),
+                              v8::NewStringType::kInternalized)
+                                  .ToLocalChecked(), attr);
+      if (entry->ping_rtt() != 0) {
+        obj->DefineOwnProperty(
+            context,
+            FIXED_ONE_BYTE_STRING(env->isolate(), "pingRTT"),
+            Number::New(env->isolate(), entry->ping_rtt() / 1e6), attr);
+      }
+      obj->DefineOwnProperty(
+          context,
+          FIXED_ONE_BYTE_STRING(env->isolate(), "framesReceived"),
+          Integer::NewFromUnsigned(env->isolate(), entry->frame_count()), attr);
+      obj->DefineOwnProperty(
+          context,
+          FIXED_ONE_BYTE_STRING(env->isolate(), "streamCount"),
+          Integer::New(env->isolate(), entry->stream_count()), attr);
+      obj->DefineOwnProperty(
+          context,
+          FIXED_ONE_BYTE_STRING(env->isolate(), "streamAverageDuration"),
+          Number::New(env->isolate(), entry->stream_average_duration()), attr);
+      entry->Notify(obj);
+    }
+    delete entry;
+  }, static_cast<void*>(entry));
+}
+
 // Closes the session and frees the associated resources
 void Http2Session::Close(uint32_t code, bool socket_closed) {
   DEBUG_HTTP2SESSION(this, "closing session");
@@ -560,6 +643,9 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
       static_cast<Http2Session::Http2Ping*>(data)->Done(false);
     }, static_cast<void*>(ping));
   }
+
+  statistics_.end_time = uv_hrtime();
+  EmitStatistics();
 }
 
 // Locates an existing known stream by ID. nghttp2 has a similar method
@@ -571,6 +657,7 @@ inline Http2Stream* Http2Session::FindStream(int32_t id) {
 
 
 inline void Http2Session::AddStream(Http2Stream* stream) {
+  CHECK_GE(++statistics_.stream_count, 0);
   streams_[stream->id()] = stream;
 }
 
@@ -720,6 +807,7 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
                                         const nghttp2_frame* frame,
                                         void* user_data) {
   Http2Session* session = static_cast<Http2Session*>(user_data);
+  session->statistics_.frame_count++;
   DEBUG_HTTP2SESSION2(session, "complete frame received: type: %d",
                       frame->hd.type);
   switch (frame->hd.type) {
@@ -1447,6 +1535,7 @@ Http2Stream::Http2Stream(
                    id_(id),
                    current_headers_category_(category) {
   MakeWeak<Http2Stream>(this);
+  statistics_.start_time = uv_hrtime();
 
   // Limit the number of header pairs
   max_header_pairs_ = session->GetMaxHeaderPairs();
@@ -1530,6 +1619,8 @@ inline bool Http2Stream::HasDataChunks(bool ignore_eos) {
 // handles it's internal memory`.
 inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) {
   CHECK(!this->IsDestroyed());
+  if (this->statistics_.first_byte == 0)
+    this->statistics_.first_byte = uv_hrtime();
   if (flags_ & NGHTTP2_STREAM_FLAG_EOS)
     return;
   char* buf = nullptr;
@@ -1590,7 +1681,6 @@ inline void Http2Stream::Destroy() {
   // may still be some pending operations queued for this stream.
   env()->SetImmediate([](Environment* env, void* data) {
     Http2Stream* stream = static_cast<Http2Stream*>(data);
-
     // Free any remaining outgoing data chunks here. This should be done
     // here because it's possible for destroy to have been called while
     // we still have qeueued outbound writes.
@@ -1603,6 +1693,12 @@ inline void Http2Stream::Destroy() {
 
     delete stream;
   }, this, this->object());
+
+  statistics_.end_time = uv_hrtime();
+  session_->statistics_.stream_average_duration =
+      ((statistics_.end_time - statistics_.start_time) /
+          session_->statistics_.stream_count) / 1e6;
+  EmitStatistics();
 }
 
 
@@ -1815,6 +1911,8 @@ inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
                                    nghttp2_rcbuf* value,
                                    uint8_t flags) {
   CHECK(!this->IsDestroyed());
+  if (this->statistics_.first_header == 0)
+    this->statistics_.first_header = uv_hrtime();
   size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
   if (current_headers_.size() == max_header_pairs_ ||
       current_headers_length_ + length > max_header_length_) {
@@ -2493,8 +2591,8 @@ void Http2Session::Http2Ping::Send(uint8_t* payload) {
 }
 
 void Http2Session::Http2Ping::Done(bool ack, const uint8_t* payload) {
-  uint64_t end = uv_hrtime();
-  double duration = (end - startTime_) / 1e6;
+  session_->statistics_.ping_rtt = (uv_hrtime() - startTime_);
+  double duration = (session_->statistics_.ping_rtt - startTime_) / 1e6;
 
   Local<Value> buf = Undefined(env()->isolate());
   if (payload != nullptr) {
diff --git a/src/node_http2.h b/src/node_http2.h
index fe8c3f4a5ee72f..cdad6e16690c5d 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -5,6 +5,7 @@
 
 #include "nghttp2/nghttp2.h"
 #include "node_http2_state.h"
+#include "node_perf.h"
 #include "stream_base-inl.h"
 #include "string_bytes.h"
 
@@ -19,6 +20,8 @@ using v8::EscapableHandleScope;
 using v8::Isolate;
 using v8::MaybeLocal;
 
+using performance::PerformanceEntry;
+
 #ifdef NODE_DEBUG_HTTP2
 
 // Adapted from nghttp2 own debug printer
@@ -530,6 +533,8 @@ class Http2Stream : public AsyncWrap,
 
   Http2Session* session() { return session_; }
 
+  inline void EmitStatistics();
+
   inline bool HasDataChunks(bool ignore_eos = false);
 
   inline void AddChunk(const uint8_t* data, size_t len);
@@ -690,6 +695,15 @@ class Http2Stream : public AsyncWrap,
 
   class Provider;
 
+  struct Statistics {
+    uint64_t start_time;
+    uint64_t end_time;
+    uint64_t first_header;  // Time first header was received
+    uint64_t first_byte;    // Time first data frame byte was received
+  };
+
+  Statistics statistics_ = {};
+
  private:
   Http2Session* session_;                       // The Parent HTTP/2 Session
   int32_t id_;                                  // The Stream Identifier
@@ -777,6 +791,8 @@ class Http2Session : public AsyncWrap {
   class Http2Ping;
   class Http2Settings;
 
+  inline void EmitStatistics();
+
   void Start();
   void Stop();
   void Close(uint32_t code = NGHTTP2_NO_ERROR,
@@ -880,6 +896,17 @@ class Http2Session : public AsyncWrap {
   Http2Settings* PopSettings();
   bool AddSettings(Http2Settings* settings);
 
+  struct Statistics {
+    uint64_t start_time;
+    uint64_t end_time;
+    uint64_t ping_rtt;
+    uint32_t frame_count;
+    int32_t stream_count;
+    double stream_average_duration;
+  };
+
+  Statistics statistics_ = {};
+
  private:
   // Frame Padding Strategies
   inline ssize_t OnMaxFrameSizePadding(size_t frameLength,
@@ -1022,6 +1049,62 @@ class Http2Session : public AsyncWrap {
   friend class Http2Scope;
 };
 
+class Http2SessionPerformanceEntry : public PerformanceEntry {
+ public:
+  Http2SessionPerformanceEntry(
+      Environment* env,
+      const Http2Session::Statistics& stats,
+      const char* kind) :
+          PerformanceEntry(env, "Http2Session", "http2",
+                           stats.start_time,
+                           stats.end_time),
+          ping_rtt_(stats.ping_rtt),
+          frame_count_(stats.frame_count),
+          stream_count_(stats.stream_count),
+          stream_average_duration_(stats.stream_average_duration),
+          kind_(kind) { }
+
+  uint64_t ping_rtt() const { return ping_rtt_; }
+  uint32_t frame_count() const { return frame_count_; }
+  int32_t stream_count() const { return stream_count_; }
+  double stream_average_duration() const { return stream_average_duration_; }
+  const char* typeName() const { return kind_; }
+
+  void Notify(Local<Value> obj) {
+    PerformanceEntry::Notify(env(), kind(), obj);
+  }
+
+ private:
+  uint64_t ping_rtt_;
+  uint32_t frame_count_;
+  int32_t stream_count_;
+  double stream_average_duration_;
+  const char* kind_;
+};
+
+class Http2StreamPerformanceEntry : public PerformanceEntry {
+ public:
+  Http2StreamPerformanceEntry(
+      Environment* env,
+      const Http2Stream::Statistics& stats) :
+          PerformanceEntry(env, "Http2Stream", "http2",
+                           stats.start_time,
+                           stats.end_time),
+          first_header_(stats.first_header),
+          first_byte_(stats.first_byte) { }
+
+  uint64_t first_header() const { return first_header_; }
+  uint64_t first_byte() const { return first_byte_; }
+
+  void Notify(Local<Value> obj) {
+    PerformanceEntry::Notify(env(), kind(), obj);
+  }
+
+ private:
+  uint64_t first_header_;
+  uint64_t first_byte_;
+};
+
 class Http2Session::Http2Ping : public AsyncWrap {
  public:
   explicit Http2Ping(Http2Session* session);
@@ -1035,6 +1118,8 @@ class Http2Session::Http2Ping : public AsyncWrap {
  private:
   Http2Session* session_;
   uint64_t startTime_;
+
+  friend class Http2Session;
 };
 
 // The Http2Settings class is used to parse the settings passed in for
diff --git a/src/node_perf.cc b/src/node_perf.cc
index c13aea2317a110..d37f8ad002ec22 100644
--- a/src/node_perf.cc
+++ b/src/node_perf.cc
@@ -81,14 +81,13 @@ void PerformanceEntry::New(const FunctionCallbackInfo<Value>& args) {
 }
 
 // Pass the PerformanceEntry object to the PerformanceObservers
-inline void PerformanceEntry::Notify(Environment* env,
-                                     PerformanceEntryType type,
-                                     Local<Value> object) {
+void PerformanceEntry::Notify(Environment* env,
+                              PerformanceEntryType type,
+                              Local<Value> object) {
   Context::Scope scope(env->context());
   AliasedBuffer<uint32_t, v8::Uint32Array>& observers =
       env->performance_state()->observers;
-  if (observers != nullptr &&
-      type != NODE_PERFORMANCE_ENTRY_TYPE_INVALID &&
+  if (type != NODE_PERFORMANCE_ENTRY_TYPE_INVALID &&
       observers[type]) {
     node::MakeCallback(env->isolate(),
                        env->process_object(),
diff --git a/src/node_perf.h b/src/node_perf.h
index f67917066f2f57..6091c0752cd493 100644
--- a/src/node_perf.h
+++ b/src/node_perf.h
@@ -47,9 +47,9 @@ NODE_EXTERN inline void MarkPerformanceMilestone(
 
 class PerformanceEntry {
  public:
-  static inline void Notify(Environment* env,
-                            PerformanceEntryType type,
-                            Local<Value> object);
+  static void Notify(Environment* env,
+                     PerformanceEntryType type,
+                     Local<Value> object);
 
   static void New(const FunctionCallbackInfo<Value>& args);
 
diff --git a/src/node_perf_common.h b/src/node_perf_common.h
index 02c3cc2d6650f0..435a4cffe5a753 100644
--- a/src/node_perf_common.h
+++ b/src/node_perf_common.h
@@ -38,7 +38,8 @@ extern uint64_t performance_v8_start;
   V(MARK, "mark")                                                             \
   V(MEASURE, "measure")                                                       \
   V(GC, "gc")                                                                 \
-  V(FUNCTION, "function")
+  V(FUNCTION, "function")                                                     \
+  V(HTTP2, "http2")
 
 enum PerformanceMilestone {
 #define V(name, _) NODE_PERFORMANCE_MILESTONE_##name,
diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js
new file mode 100644
index 00000000000000..f2ef29cec25e06
--- /dev/null
+++ b/test/parallel/test-http2-perf_hooks.js
@@ -0,0 +1,95 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const h2 = require('http2');
+
+const { PerformanceObserver } = require('perf_hooks');
+
+const obs = new PerformanceObserver((items) => {
+  const entry = items.getEntries()[0];
+  assert.strictEqual(entry.entryType, 'http2');
+  assert.strictEqual(typeof entry.startTime, 'number');
+  assert.strictEqual(typeof entry.duration, 'number');
+  switch (entry.name) {
+    case 'Http2Session':
+      assert.strictEqual(typeof entry.pingRTT, 'number');
+      assert.strictEqual(typeof entry.streamAverageDuration, 'number');
+      assert.strictEqual(typeof entry.streamCount, 'number');
+      assert.strictEqual(typeof entry.framesReceived, 'number');
+      switch (entry.type) {
+        case 'server':
+          assert.strictEqual(entry.streamCount, 1);
+          assert.strictEqual(entry.framesReceived, 5);
+          break;
+        case 'client':
+          assert.strictEqual(entry.streamCount, 1);
+          assert.strictEqual(entry.framesReceived, 8);
+          break;
+        default:
+          assert.fail('invalid Http2Session type');
+      }
+      break;
+    case 'Http2Stream':
+      assert.strictEqual(typeof entry.timeToFirstByte, 'number');
+      assert.strictEqual(typeof entry.timeToFirstHeader, 'number');
+      break;
+    default:
+      assert.fail('invalid entry name');
+  }
+});
+obs.observe({ entryTypes: ['http2'] });
+
+const body =
+  '<html><head></head><body><h1>this is some data</h2></body></html>';
+
+const server = h2.createServer();
+
+// we use the lower-level API here
+server.on('stream', common.mustCall(onStream));
+
+function onStream(stream, headers, flags) {
+  assert.strictEqual(headers[':scheme'], 'http');
+  assert.ok(headers[':authority']);
+  assert.strictEqual(headers[':method'], 'GET');
+  assert.strictEqual(flags, 5);
+  stream.respond({
+    'content-type': 'text/html',
+    ':status': 200
+  });
+  stream.write(body.slice(0, 20));
+  stream.end(body.slice(20));
+}
+
+server.on('session', common.mustCall((session) => {
+  session.ping(common.mustCall());
+}));
+
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+  const client = h2.connect(`http://localhost:${server.address().port}`);
+
+  client.on('connect', common.mustCall(() => {
+    client.ping(common.mustCall());
+  }));
+
+  const req = client.request();
+
+  req.on('response', common.mustCall());
+
+  let data = '';
+  req.setEncoding('utf8');
+  req.on('data', (d) => data += d);
+  req.on('end', common.mustCall(() => {
+    assert.strictEqual(body, data);
+  }));
+  req.on('close', common.mustCall(() => {
+    client.close();
+    server.close();
+  }));
+
+}));

From 7767aaa89d0abebed1f458044fa450596dd380ac Mon Sep 17 00:00:00 2001
From: Kyle Farnung <kfarnung@microsoft.com>
Date: Mon, 22 Jan 2018 11:43:35 -0800
Subject: [PATCH 20/77] http2,perf_hooks: perf state using AliasedBuffer

This is the portion of be2cbcc that is not in dea44b9.

Update performance_state to use AliasedBuffer and update usage sites.

PR-URL: https://github.com/nodejs/node/pull/18300
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 src/node_http2.cc | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 442fa64b6ca981..21265bb15b91bd 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -531,7 +531,8 @@ Http2Session::~Http2Session() {
 }
 
 inline bool HasHttp2Observer(Environment* env) {
-  uint32_t* observers = env->performance_state()->observers;
+  AliasedBuffer<uint32_t, v8::Uint32Array>& observers =
+      env->performance_state()->observers;
   return observers[performance::NODE_PERFORMANCE_ENTRY_TYPE_HTTP2] != 0;
 }
 

From 936337ae06ba6678c1154efa075a14819c4198bf Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Tue, 21 Nov 2017 10:52:33 -0800
Subject: [PATCH 21/77] http2: strictly limit number on concurrent streams

Strictly limit the number of concurrent streams based on the
current setting of the MAX_CONCURRENT_STREAMS setting

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/16766
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Sebastiaan Deckers <sebdeckers83@gmail.com>
---
 src/node_http2.cc                            | 19 ++++++-
 src/node_http2.h                             |  2 +
 test/parallel/test-http2-too-many-streams.js | 60 ++++++++++++++++++++
 3 files changed, 80 insertions(+), 1 deletion(-)
 create mode 100644 test/parallel/test-http2-too-many-streams.js

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 21265bb15b91bd..8b9c35778ec496 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -656,6 +656,16 @@ inline Http2Stream* Http2Session::FindStream(int32_t id) {
   return s != streams_.end() ? s->second : nullptr;
 }
 
+inline bool Http2Session::CanAddStream() {
+  uint32_t maxConcurrentStreams =
+      nghttp2_session_get_local_settings(
+          session_, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
+  size_t maxSize =
+      std::min(streams_.max_size(), static_cast<size_t>(maxConcurrentStreams));
+  // We can add a new stream so long as we are less than the current
+  // maximum on concurrent streams
+  return streams_.size() < maxSize;
+}
 
 inline void Http2Session::AddStream(Http2Stream* stream) {
   CHECK_GE(++statistics_.stream_count, 0);
@@ -766,7 +776,14 @@ inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
 
   Http2Stream* stream = session->FindStream(id);
   if (stream == nullptr) {
-    new Http2Stream(session, id, frame->headers.cat);
+    if (session->CanAddStream()) {
+      new Http2Stream(session, id, frame->headers.cat);
+    } else {
+      // Too many concurrent streams being opened
+      nghttp2_submit_rst_stream(**session, NGHTTP2_FLAG_NONE, id,
+                                NGHTTP2_ENHANCE_YOUR_CALM);
+      return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
+    }
   } else {
     // If the stream has already been destroyed, ignore.
     if (stream->IsDestroyed())
diff --git a/src/node_http2.h b/src/node_http2.h
index cdad6e16690c5d..afba35959a192b 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -835,6 +835,8 @@ class Http2Session : public AsyncWrap {
   // Returns pointer to the stream, or nullptr if stream does not exist
   inline Http2Stream* FindStream(int32_t id);
 
+  inline bool CanAddStream();
+
   // Adds a stream instance to this session
   inline void AddStream(Http2Stream* stream);
 
diff --git a/test/parallel/test-http2-too-many-streams.js b/test/parallel/test-http2-too-many-streams.js
new file mode 100644
index 00000000000000..a4a67befa0f50a
--- /dev/null
+++ b/test/parallel/test-http2-too-many-streams.js
@@ -0,0 +1,60 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const Countdown = require('../common/countdown');
+const http2 = require('http2');
+const assert = require('assert');
+
+// Test that the maxConcurrentStreams setting is strictly enforced
+
+const server = http2.createServer({ settings: { maxConcurrentStreams: 1 } });
+
+let c = 0;
+
+server.on('stream', common.mustCall((stream) => {
+  // Because we only allow one open stream at a time,
+  // c should never be greater than 1.
+  assert.strictEqual(++c, 1);
+  stream.respond();
+  // Force some asynchronos stuff.
+  setImmediate(() => {
+    stream.end('ok');
+    assert.strictEqual(--c, 0);
+  });
+}, 3));
+
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+
+  const countdown = new Countdown(3, common.mustCall(() => {
+    server.close();
+    client.destroy();
+  }));
+
+  client.on('remoteSettings', common.mustCall(() => {
+    assert.strictEqual(client.remoteSettings.maxConcurrentStreams, 1);
+
+    {
+      const req = client.request();
+      req.resume();
+      req.on('close', () => {
+        countdown.dec();
+
+        setImmediate(() => {
+          const req = client.request();
+          req.resume();
+          req.on('close', () => countdown.dec());
+        });
+      });
+    }
+
+    {
+      const req = client.request();
+      req.resume();
+      req.on('close', () => countdown.dec());
+    }
+  }));
+}));

From 644bd5f56891e941e9b1b1a73735e0e57861aae2 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Fri, 29 Dec 2017 09:44:08 -0800
Subject: [PATCH 22/77] http2: add altsvc support

This commit also includes prerequisite error definitions
from c75f87c and 1698c8e.

Add support for sending and receiving ALTSVC frames.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17917
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/errors.md                  |  10 +++
 doc/api/http2.md                   |  94 +++++++++++++++++++++
 lib/internal/errors.js             |   6 ++
 lib/internal/http2/core.js         |  62 ++++++++++++++
 src/env.h                          |   1 +
 src/node_http2.cc                  |  76 +++++++++++++++++
 src/node_http2.h                   |   7 ++
 test/parallel/test-http2-altsvc.js | 126 +++++++++++++++++++++++++++++
 8 files changed, 382 insertions(+)
 create mode 100644 test/parallel/test-http2-altsvc.js

diff --git a/doc/api/errors.md b/doc/api/errors.md
index 9f487381309b96..12650d55f6349d 100755
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -643,6 +643,16 @@ that.
 
 Occurs with multiple attempts to shutdown an HTTP/2 session.
 
+<a id="ERR_HTTP2_ALTSVC_INVALID_ORIGIN"></a>
+### ERR_HTTP2_ALTSVC_INVALID_ORIGIN
+
+HTTP/2 ALTSVC frames require a valid origin.
+
+<a id="ERR_HTTP2_ALTSVC_LENGTH"></a>
+### ERR_HTTP2_ALTSVC_LENGTH
+
+HTTP/2 ALTSVC frames are limited to a maximum of 16,382 payload bytes.
+
 <a id="ERR_HTTP2_CONNECT_AUTHORITY"></a>
 ### ERR_HTTP2_CONNECT_AUTHORITY
 
diff --git a/doc/api/http2.md b/doc/api/http2.md
index 23bfac059c7321..138df5ee226438 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -570,11 +570,103 @@ added: REPLACEME
 Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session`
 instance's underlying [`net.Socket`].
 
+### Class: ServerHttp2Session
+<!-- YAML
+added: v8.4.0
+-->
+
+#### serverhttp2session.altsvc(alt, originOrStream)
+<!-- YAML
+added: REPLACEME
+-->
+
+* `alt` {string} A description of the alternative service configuration as
+  defined by [RFC 7838][].
+* `originOrStream` {number|string|URL|Object} Either a URL string specifying
+  the origin (or an Object with an `origin` property) or the numeric identifier
+  of an active `Http2Stream` as given by the `http2stream.id` property.
+
+Submits an `ALTSVC` frame (as defined by [RFC 7838][]) to the connected client.
+
+```js
+const http2 = require('http2');
+
+const server = http2.createServer();
+server.on('session', (session) => {
+  // Set altsvc for origin https://example.org:80
+  session.altsvc('h2=":8000"', 'https://example.org:80');
+});
+
+server.on('stream', (stream) => {
+  // Set altsvc for a specific stream
+  stream.session.altsvc('h2=":8000"', stream.id);
+});
+```
+
+Sending an `ALTSVC` frame with a specific stream ID indicates that the alternate
+service is associated with the origin of the given `Http2Stream`.
+
+The `alt` and origin string *must* contain only ASCII bytes and are
+strictly interpreted as a sequence of ASCII bytes. The special value `'clear'`
+may be passed to clear any previously set alternative service for a given
+domain.
+
+When a string is passed for the `originOrStream` argument, it will be parsed as
+a URL and the origin will be derived. For insetance, the origin for the
+HTTP URL `'https://example.org/foo/bar'` is the ASCII string
+`'https://example.org'`. An error will be thrown if either the given string
+cannot be parsed as a URL or if a valid origin cannot be derived.
+
+A `URL` object, or any object with an `origin` property, may be passed as
+`originOrStream`, in which case the value of the `origin` property will be
+used. The value of the `origin` property *must* be a properly serialized
+ASCII origin.
+
+#### Specifying alternative services
+
+The format of the `alt` parameter is strictly defined by [RFC 7838][] as an
+ASCII string containing a comma-delimited list of "alternative" protocols
+associated with a specific host and port.
+
+For example, the value `'h2="example.org:81"'` indicates that the HTTP/2
+protocol is available on the host `'example.org'` on TCP/IP port 81. The
+host and port *must* be contained within the quote (`"`) characters.
+
+Multiple alternatives may be specified, for instance: `'h2="example.org:81",
+h2=":82"'`
+
+The protocol identifier (`'h2'` in the examples) may be any valid
+[ALPN Protocol ID][].
+
+The syntax of these values is not validated by the Node.js implementation and
+are passed through as provided by the user or received from the peer.
+
 ### Class: ClientHttp2Session
 <!-- YAML
 added: v8.4.0
 -->
 
+#### Event: 'altsvc'
+<!-- YAML
+added: REPLACEME
+-->
+
+The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by
+the client. The event is emitted with the `ALTSVC` value, origin, and stream
+ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will
+be an empty string.
+
+```js
+const http2 = require('http2');
+const client = http2.connect('https://example.org');
+
+client.on('altsvc', (alt, origin, stream) => {
+  console.log(alt);
+  console.log(origin);
+  console.log(stream);
+});
+```
+
 #### clienthttp2session.request(headers[, options])
 <!-- YAML
 added: v8.4.0
@@ -2869,6 +2961,7 @@ following additional properties:
 
 
 [ALPN negotiation]: #http2_alpn_negotiation
+[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
 [Compatibility API]: #http2_compatibility_api
 [HTTP/1]: http.html
 [HTTP/2]: https://tools.ietf.org/html/rfc7540
@@ -2877,6 +2970,7 @@ following additional properties:
 [Http2Session and Sockets]: #http2_http2session_and_sockets
 [Performance Observer]: perf_hooks.html
 [Readable Stream]: stream.html#stream_class_stream_readable
+[RFC 7838]: https://tools.ietf.org/html/rfc7838
 [Settings Object]: #http2_settings_object
 [Using options.selectPadding]: #http2_using_options_selectpadding
 [Writable Stream]: stream.html#stream_writable_streams
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index c1a1f16213a043..fef210a9262169 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -152,6 +152,10 @@ E('ERR_ENCODING_NOT_SUPPORTED',
 E('ERR_FALSY_VALUE_REJECTION', 'Promise was rejected with falsy value');
 E('ERR_HTTP2_ALREADY_SHUTDOWN',
   'Http2Session is already shutdown or destroyed');
+E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
+  'HTTP/2 ALTSVC frames require a valid origin');
+E('ERR_HTTP2_ALTSVC_LENGTH',
+  'HTTP/2 ALTSVC frames are limited to 16382 bytes');
 E('ERR_HTTP2_CONNECT_AUTHORITY',
   ':authority header is required for CONNECT requests');
 E('ERR_HTTP2_CONNECT_PATH',
@@ -233,6 +237,7 @@ E('ERR_INVALID_ARRAY_LENGTH',
   });
 E('ERR_INVALID_ASYNC_ID', (type, id) => `Invalid ${type} value: ${id}`);
 E('ERR_INVALID_CALLBACK', 'callback must be a function');
+E('ERR_INVALID_CHAR', 'Invalid character in %s');
 E('ERR_INVALID_FD', (fd) => `"fd" must be a positive integer: ${fd}`);
 E('ERR_INVALID_FILE_URL_HOST', 'File URL host %s');
 E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s');
@@ -270,6 +275,7 @@ E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function');
 E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object');
 E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support');
 E('ERR_NO_ICU', '%s is not supported on Node.js compiled without ICU');
+E('ERR_OUT_OF_RANGE', 'The "%s" argument is out of range');
 E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s');
 E('ERR_REQUIRE_ESM', 'Must use import to load ES Module: %s');
 E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound');
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 3c84732169a07a..4bbc5dab459292 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -32,6 +32,9 @@ const kMaxFrameSize = (2 ** 24) - 1;
 const kMaxInt = (2 ** 32) - 1;
 const kMaxStreams = (2 ** 31) - 1;
 
+// eslint-disable-next-line no-control-regex
+const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/;
+
 const {
   assertIsObject,
   assertValidPseudoHeaderResponse,
@@ -362,6 +365,16 @@ function onFrameError(id, type, code) {
   process.nextTick(emit, emitter, 'frameError', type, code, id);
 }
 
+function onAltSvc(stream, origin, alt) {
+  const session = this[kOwner];
+  if (session.destroyed)
+    return;
+  debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` +
+        `stream: ${stream}, origin: ${origin}, alt: ${alt}`);
+  session[kUpdateTimer]();
+  process.nextTick(emit, session, 'altsvc', alt, origin, stream);
+}
+
 // Receiving a GOAWAY frame from the connected peer is a signal that no
 // new streams should be created. If the code === NGHTTP2_NO_ERROR, we
 // are going to send our our close, but allow existing frames to close
@@ -704,6 +717,7 @@ function setupHandle(socket, type, options) {
   handle.onheaders = onSessionHeaders;
   handle.onframeerror = onFrameError;
   handle.ongoawaydata = onGoawayData;
+  handle.onaltsvc = onAltSvc;
 
   if (typeof options.selectPadding === 'function')
     handle.ongetpadding = onSelectPadding(options.selectPadding);
@@ -1150,6 +1164,54 @@ class ServerHttp2Session extends Http2Session {
   get server() {
     return this[kServer];
   }
+
+  // Submits an altsvc frame to be sent to the client. `stream` is a
+  // numeric Stream ID. origin is a URL string that will be used to get
+  // the origin. alt is a string containing the altsvc details. No fancy
+  // API is provided for that.
+  altsvc(alt, originOrStream) {
+    if (this.destroyed)
+      throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
+
+    let stream = 0;
+    let origin;
+
+    if (typeof originOrStream === 'string') {
+      origin = (new URL(originOrStream)).origin;
+      if (origin === 'null')
+        throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
+    } else if (typeof originOrStream === 'number') {
+      if (originOrStream >>> 0 !== originOrStream || originOrStream === 0)
+        throw new errors.RangeError('ERR_OUT_OF_RANGE', 'originOrStream');
+      stream = originOrStream;
+    } else if (originOrStream !== undefined) {
+      // Allow origin to be passed a URL or object with origin property
+      if (originOrStream !== null && typeof originOrStream === 'object')
+        origin = originOrStream.origin;
+      // Note: if originOrStream is an object with an origin property other
+      // than a URL, then it is possible that origin will be malformed.
+      // We do not verify that here. Users who go that route need to
+      // ensure they are doing the right thing or the payload data will
+      // be invalid.
+      if (typeof origin !== 'string') {
+        throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'originOrStream',
+                                   ['string', 'number', 'URL', 'object']);
+      } else if (origin === 'null' || origin.length === 0) {
+        throw new errors.TypeError('ERR_HTTP2_ALTSVC_INVALID_ORIGIN');
+      }
+    }
+
+    if (typeof alt !== 'string')
+      throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'alt', 'string');
+    if (!kQuotedString.test(alt))
+      throw new errors.TypeError('ERR_INVALID_CHAR', 'alt');
+
+    // Max length permitted for ALTSVC
+    if ((alt.length + (origin !== undefined ? origin.length : 0)) > 16382)
+      throw new errors.TypeError('ERR_HTTP2_ALTSVC_LENGTH');
+
+    this[kHandle].altsvc(stream, origin || '', alt);
+  }
 }
 
 // ClientHttp2Session instances have to wait for the socket to connect after
diff --git a/src/env.h b/src/env.h
index a2e5872a35cbe4..78c088700b778f 100644
--- a/src/env.h
+++ b/src/env.h
@@ -199,6 +199,7 @@ class ModuleWrap;
   V(nsname_string, "nsname")                                                  \
   V(nexttick_string, "nextTick")                                              \
   V(ocsp_request_string, "OCSPRequest")                                       \
+  V(onaltsvc_string, "onaltsvc")                                              \
   V(onchange_string, "onchange")                                              \
   V(onclienthello_string, "onclienthello")                                    \
   V(oncomplete_string, "oncomplete")                                          \
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 8b9c35778ec496..858d0690a3e784 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -103,6 +103,11 @@ Http2Options::Http2Options(Environment* env) {
   // are required to buffer.
   nghttp2_option_set_no_auto_window_update(options_, 1);
 
+  // Enable built in support for ALTSVC frames. Once we add support for
+  // other non-built in extension frames, this will need to be handled
+  // a bit differently. For now, let's let nghttp2 take care of it.
+  nghttp2_option_set_builtin_recv_extension_type(options_, NGHTTP2_ALTSVC);
+
   AliasedBuffer<uint32_t, v8::Uint32Array>& buffer =
       env->http2_state()->options_buffer;
   uint32_t flags = buffer[IDX_OPTIONS_FLAGS];
@@ -848,6 +853,10 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
       break;
     case NGHTTP2_PING:
       session->HandlePingFrame(frame);
+      break;
+    case NGHTTP2_ALTSVC:
+      session->HandleAltSvcFrame(frame);
+      break;
     default:
       break;
   }
@@ -1186,6 +1195,34 @@ inline void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
   MakeCallback(env()->ongoawaydata_string(), arraysize(argv), argv);
 }
 
+// Called by OnFrameReceived when a complete ALTSVC frame has been received.
+inline void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
+  Isolate* isolate = env()->isolate();
+  HandleScope scope(isolate);
+  Local<Context> context = env()->context();
+  Context::Scope context_scope(context);
+
+  int32_t id = GetFrameID(frame);
+
+  nghttp2_extension ext = frame->ext;
+  nghttp2_ext_altsvc* altsvc = static_cast<nghttp2_ext_altsvc*>(ext.payload);
+  DEBUG_HTTP2SESSION(this, "handling altsvc frame");
+
+  Local<Value> argv[3] = {
+    Integer::New(isolate, id),
+    String::NewFromOneByte(isolate,
+                           altsvc->origin,
+                           v8::NewStringType::kNormal,
+                           altsvc->origin_len).ToLocalChecked(),
+    String::NewFromOneByte(isolate,
+                           altsvc->field_value,
+                           v8::NewStringType::kNormal,
+                           altsvc->field_value_len).ToLocalChecked(),
+  };
+
+  MakeCallback(env()->onaltsvc_string(), arraysize(argv), argv);
+}
+
 // Called by OnFrameReceived when a complete PING frame has been received.
 inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
   bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
@@ -2495,6 +2532,44 @@ void Http2Stream::RefreshState(const FunctionCallbackInfo<Value>& args) {
   }
 }
 
+void Http2Session::AltSvc(int32_t id,
+                          uint8_t* origin,
+                          size_t origin_len,
+                          uint8_t* value,
+                          size_t value_len) {
+  Http2Scope h2scope(this);
+  CHECK_EQ(nghttp2_submit_altsvc(session_, NGHTTP2_FLAG_NONE, id,
+                                 origin, origin_len, value, value_len), 0);
+}
+
+// Submits an AltSvc frame to the sent to the connected peer.
+void Http2Session::AltSvc(const FunctionCallbackInfo<Value>& args) {
+  Environment* env = Environment::GetCurrent(args);
+  Http2Session* session;
+  ASSIGN_OR_RETURN_UNWRAP(&session, args.Holder());
+
+  int32_t id = args[0]->Int32Value(env->context()).ToChecked();
+
+  // origin and value are both required to be ASCII, handle them as such.
+  Local<String> origin_str = args[1]->ToString(env->context()).ToLocalChecked();
+  Local<String> value_str = args[2]->ToString(env->context()).ToLocalChecked();
+
+  size_t origin_len = origin_str->Length();
+  size_t value_len = value_str->Length();
+
+  CHECK_LE(origin_len + value_len, 16382);  // Max permitted for ALTSVC
+  // Verify that origin len != 0 if stream id == 0, or
+  // that origin len == 0 if stream id != 0
+  CHECK((origin_len != 0 && id == 0) || (origin_len == 0 && id != 0));
+
+  MaybeStackBuffer<uint8_t> origin(origin_len);
+  MaybeStackBuffer<uint8_t> value(value_len);
+  origin_str->WriteOneByte(*origin);
+  value_str->WriteOneByte(*value);
+
+  session->AltSvc(id, *origin, origin_len, *value, value_len);
+}
+
 // Submits a PING frame to be sent to the connected peer.
 void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
   Environment* env = Environment::GetCurrent(args);
@@ -2712,6 +2787,7 @@ void Initialize(Local<Object> target,
   session->SetClassName(http2SessionClassName);
   session->InstanceTemplate()->SetInternalFieldCount(1);
   AsyncWrap::AddWrapMethods(env, session);
+  env->SetProtoMethod(session, "altsvc", Http2Session::AltSvc);
   env->SetProtoMethod(session, "ping", Http2Session::Ping);
   env->SetProtoMethod(session, "consume", Http2Session::Consume);
   env->SetProtoMethod(session, "destroy", Http2Session::Destroy);
diff --git a/src/node_http2.h b/src/node_http2.h
index afba35959a192b..cb44689e31b266 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -800,6 +800,11 @@ class Http2Session : public AsyncWrap {
   void Consume(Local<External> external);
   void Unconsume();
   void Goaway(uint32_t code, int32_t lastStreamID, uint8_t* data, size_t len);
+  void AltSvc(int32_t id,
+              uint8_t* origin,
+              size_t origin_len,
+              uint8_t* value,
+              size_t value_len);
 
   bool Ping(v8::Local<v8::Function> function);
 
@@ -879,6 +884,7 @@ class Http2Session : public AsyncWrap {
   static void UpdateChunksSent(const FunctionCallbackInfo<Value>& args);
   static void RefreshState(const FunctionCallbackInfo<Value>& args);
   static void Ping(const FunctionCallbackInfo<Value>& args);
+  static void AltSvc(const FunctionCallbackInfo<Value>& args);
 
   template <get_setting fn>
   static void RefreshSettings(const FunctionCallbackInfo<Value>& args);
@@ -923,6 +929,7 @@ class Http2Session : public AsyncWrap {
   inline void HandlePriorityFrame(const nghttp2_frame* frame);
   inline void HandleSettingsFrame(const nghttp2_frame* frame);
   inline void HandlePingFrame(const nghttp2_frame* frame);
+  inline void HandleAltSvcFrame(const nghttp2_frame* frame);
 
   // nghttp2 callbacks
   static inline int OnBeginHeadersCallback(
diff --git a/test/parallel/test-http2-altsvc.js b/test/parallel/test-http2-altsvc.js
new file mode 100644
index 00000000000000..9fd9a9fc278552
--- /dev/null
+++ b/test/parallel/test-http2-altsvc.js
@@ -0,0 +1,126 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const http2 = require('http2');
+const { URL } = require('url');
+const Countdown = require('../common/countdown');
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream) => {
+  stream.session.altsvc('h2=":8000"', stream.id);
+  stream.respond();
+  stream.end('ok');
+}));
+server.on('session', common.mustCall((session) => {
+  // Origin may be specified by string, URL object, or object with an
+  // origin property. For string and URL object, origin is guaranteed
+  // to be an ASCII serialized origin. For object with an origin
+  // property, it is up to the user to ensure proper serialization.
+  session.altsvc('h2=":8000"', 'https://example.org:8111/this');
+  session.altsvc('h2=":8000"', new URL('https://example.org:8111/this'));
+  session.altsvc('h2=":8000"', { origin: 'https://example.org:8111' });
+
+  // Won't error, but won't send anything because the stream does not exist
+  session.altsvc('h2=":8000"', 3);
+
+  // Will error because the numeric stream id is out of valid range
+  [0, -1, 1.1, 0xFFFFFFFF + 1, Infinity, -Infinity].forEach((i) => {
+    common.expectsError(
+      () => session.altsvc('h2=":8000"', i),
+      {
+        code: 'ERR_OUT_OF_RANGE',
+        type: RangeError
+      }
+    );
+  });
+
+  // First argument must be a string
+  [0, {}, [], null, Infinity].forEach((i) => {
+    common.expectsError(
+      () => session.altsvc(i),
+      {
+        code: 'ERR_INVALID_ARG_TYPE',
+        type: TypeError
+      }
+    );
+  });
+
+  ['\u0001', 'h2="\uff20"', '👀'].forEach((i) => {
+    common.expectsError(
+      () => session.altsvc(i),
+      {
+        code: 'ERR_INVALID_CHAR',
+        type: TypeError
+      }
+    );
+  });
+
+  [{}, [], true].forEach((i) => {
+    common.expectsError(
+      () => session.altsvc('clear', i),
+      {
+        code: 'ERR_INVALID_ARG_TYPE',
+        type: TypeError
+      }
+    );
+  });
+
+  [
+    'abc:',
+    new URL('abc:'),
+    { origin: 'null' },
+    { origin: '' }
+  ].forEach((i) => {
+    common.expectsError(
+      () => session.altsvc('h2=":8000', i),
+      {
+        code: 'ERR_HTTP2_ALTSVC_INVALID_ORIGIN',
+        type: TypeError
+      }
+    );
+  });
+
+  // arguments + origin are too long for an ALTSVC frame
+  common.expectsError(
+    () => {
+      session.altsvc('h2=":8000"',
+                     `http://example.${'a'.repeat(17000)}.org:8000`);
+    },
+    {
+      code: 'ERR_HTTP2_ALTSVC_LENGTH',
+      type: TypeError
+    }
+  );
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+
+  const countdown = new Countdown(4, () => {
+    client.close();
+    server.close();
+  });
+
+  client.on('altsvc', common.mustCall((alt, origin, stream) => {
+    assert.strictEqual(alt, 'h2=":8000"');
+    switch (stream) {
+      case 0:
+        assert.strictEqual(origin, 'https://example.org:8111');
+        break;
+      case 1:
+        assert.strictEqual(origin, '');
+        break;
+      default:
+        assert.fail('should not happen');
+    }
+    countdown.dec();
+  }, 4));
+
+  const req = client.request();
+  req.resume();
+  req.on('close', common.mustCall());
+}));

From 6466ac19208723c2575fabff0fa85fc5a6a3c9fa Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 1 Jan 2018 11:13:05 -0800
Subject: [PATCH 23/77] tls: set servername on client side too

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17935
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Sebastiaan Deckers <sebdeckers83@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
---
 lib/_tls_wrap.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js
index 598bfaa5ae8c3e..4bd8ecbf48437d 100644
--- a/lib/_tls_wrap.js
+++ b/lib/_tls_wrap.js
@@ -628,7 +628,7 @@ TLSSocket.prototype._finishInit = function() {
     this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol();
   }
 
-  if (process.features.tls_sni && this._tlsOptions.isServer) {
+  if (process.features.tls_sni) {
     this.servername = this._handle.getServername();
   }
 

From fc97666e3113ee218ebaea2f125f2a657d6b8756 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 1 Jan 2018 11:13:29 -0800
Subject: [PATCH 24/77] http2: add initial support for originSet

Add new properties to `Http2Session` to identify alpnProtocol,
and indicator about whether the session is TLS or not, and
initial support for origin set (preparinng for `ORIGIN` frame
support and the client-side `Pool` implementation.

The `originSet` is the set of origins for which an `Http2Session`
may be considered authoritative. Per the `ORIGIN` frame spec,
the originSet is only valid on TLS connections, so this is only
exposed when using a `TLSSocket`.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17935
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Sebastiaan Deckers <sebdeckers83@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
---
 doc/api/http2.md                              | 35 +++++++++++
 lib/internal/http2/core.js                    | 58 ++++++++++++++++++-
 ...test-http2-create-client-secure-session.js | 19 ++++++
 .../test-http2-create-client-session.js       |  8 ++-
 4 files changed, 118 insertions(+), 2 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 138df5ee226438..572543e0826386 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -283,6 +283,18 @@ session.setTimeout(2000);
 session.on('timeout', () => { /** .. **/ });
 ```
 
+#### http2session.alpnProtocol
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {string|undefined}
+
+Value will be `undefined` if the `Http2Session` is not yet connected to a
+socket, `h2c` if the `Http2Session` is not connected to a `TLSSocket`, or
+will return the value of the connected `TLSSocket`'s own `alpnProtocol`
+property.
+
 #### http2session.close([callback])
 <!-- YAML
 added: REPLACEME
@@ -340,6 +352,18 @@ added: v8.4.0
 Will be `true` if this `Http2Session` instance has been destroyed and must no
 longer be used, otherwise `false`.
 
+#### http2session.encrypted
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {boolean|undefined}
+
+Value is `undefined` if the `Http2Session` session socket has not yet been
+connected, `true` if the `Http2Session` is connected with a `TLSSocket`,
+and `false` if the `Http2Session` is connected to any other kind of socket
+or stream.
+
 #### http2session.goaway([code, [lastStreamID, [opaqueData]]])
 <!-- YAML
 added: REPLACEME
@@ -363,6 +387,17 @@ added: v8.4.0
 A prototype-less object describing the current local settings of this
 `Http2Session`. The local settings are local to *this* `Http2Session` instance.
 
+#### http2session.originSet
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {string[]|undefined}
+
+If the `Http2Session` is connected to a `TLSSocket`, the `originSet` property
+will return an Array of origins for which the `Http2Session` may be
+considered authoritative.
+
 #### http2session.pendingSettingsAck
 <!-- YAML
 added: v8.4.0
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 4bbc5dab459292..204aa2bdbc3069 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -68,7 +68,9 @@ const TLSServer = tls.Server;
 
 const kInspect = require('internal/util').customInspectSymbol;
 
+const kAlpnProtocol = Symbol('alpnProtocol');
 const kAuthority = Symbol('authority');
+const kEncrypted = Symbol('encrypted');
 const kHandle = Symbol('handle');
 const kID = Symbol('id');
 const kInit = Symbol('init');
@@ -729,6 +731,17 @@ function setupHandle(socket, type, options) {
 
   this[kHandle] = handle;
 
+  if (socket.encrypted) {
+    this[kAlpnProtocol] = socket.alpnProtocol;
+    this[kEncrypted] = true;
+  } else {
+    // 'h2c' is the protocol identifier for HTTP/2 over plain-text. We use
+    // it here to identify any session that is not explicitly using an
+    // encrypted socket.
+    this[kAlpnProtocol] = 'h2c';
+    this[kEncrypted] = false;
+  }
+
   const settings = typeof options.settings === 'object' ?
     options.settings : {};
 
@@ -803,9 +816,12 @@ class Http2Session extends EventEmitter {
       streams: new Map(),
       pendingStreams: new Set(),
       pendingAck: 0,
-      writeQueueSize: 0
+      writeQueueSize: 0,
+      originSet: undefined
     };
 
+    this[kEncrypted] = undefined;
+    this[kAlpnProtocol] = undefined;
     this[kType] = type;
     this[kProxySocket] = null;
     this[kSocket] = socket;
@@ -830,6 +846,46 @@ class Http2Session extends EventEmitter {
     debug(`Http2Session ${sessionName(type)}: created`);
   }
 
+  // Returns undefined if the socket is not yet connected, true if the
+  // socket is a TLSSocket, and false if it is not.
+  get encrypted() {
+    return this[kEncrypted];
+  }
+
+  // Returns undefined if the socket is not yet connected, `h2` if the
+  // socket is a TLSSocket and the alpnProtocol is `h2`, or `h2c` if the
+  // socket is not a TLSSocket.
+  get alpnProtocol() {
+    return this[kAlpnProtocol];
+  }
+
+  // TODO(jasnell): originSet is being added in preparation for ORIGIN frame
+  // support. At the current time, the ORIGIN frame specification is awaiting
+  // publication as an RFC and is awaiting implementation in nghttp2. Once
+  // added, an ORIGIN frame will add to the origins included in the origin
+  // set. 421 responses will remove origins from the set.
+  get originSet() {
+    if (!this.encrypted || this.destroyed)
+      return undefined;
+
+    let originSet = this[kState].originSet;
+    if (originSet === undefined) {
+      const socket = this[kSocket];
+      this[kState].originSet = originSet = new Set();
+      if (socket.servername != null) {
+        let originString = `https://${socket.servername}`;
+        if (socket.remotePort != null)
+          originString += `:${socket.remotePort}`;
+        // We have to ensure that it is a properly serialized
+        // ASCII origin string. The socket.servername might not
+        // be properly ASCII encoded.
+        originSet.add((new URL(originString)).origin);
+      }
+    }
+
+    return Array.from(originSet);
+  }
+
   // True if the Http2Session is still waiting for the socket to connect
   get connecting() {
     return (this[kState].flags & SESSION_FLAGS_READY) === 0;
diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js
index 811ef772d5903a..6120a58602065d 100644
--- a/test/parallel/test-http2-create-client-secure-session.js
+++ b/test/parallel/test-http2-create-client-secure-session.js
@@ -19,6 +19,14 @@ function loadKey(keyname) {
 
 function onStream(stream, headers) {
   const socket = stream.session[kSocket];
+
+  assert(stream.session.encrypted);
+  assert(stream.session.alpnProtocol, 'h2');
+  const originSet = stream.session.originSet;
+  assert(Array.isArray(originSet));
+  assert.strictEqual(originSet[0],
+                     `https://${socket.servername}:${socket.remotePort}`);
+
   assert(headers[':authority'].startsWith(socket.servername));
   stream.respond({ 'content-type': 'application/json' });
   stream.end(JSON.stringify({
@@ -39,6 +47,17 @@ function verifySecureSession(key, cert, ca, opts) {
     assert.strictEqual(client.socket.listenerCount('secureConnect'), 1);
     const req = client.request();
 
+    client.on('connect', common.mustCall(() => {
+      assert(client.encrypted);
+      assert.strictEqual(client.alpnProtocol, 'h2');
+      const originSet = client.originSet;
+      assert(Array.isArray(originSet));
+      assert.strictEqual(originSet.length, 1);
+      assert.strictEqual(
+        originSet[0],
+        `https://${opts.servername || 'localhost'}:${server.address().port}`);
+    }));
+
     req.on('response', common.mustCall((headers) => {
       assert.strictEqual(headers[':status'], 200);
       assert.strictEqual(headers['content-type'], 'application/json');
diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js
index c5e9468f9bd15b..b5be6bc8581452 100644
--- a/test/parallel/test-http2-create-client-session.js
+++ b/test/parallel/test-http2-create-client-session.js
@@ -34,10 +34,16 @@ server.listen(0);
 server.on('listening', common.mustCall(() => {
 
   const client = h2.connect(`http://localhost:${server.address().port}`);
-  client.setMaxListeners(100);
+  client.setMaxListeners(101);
 
   client.on('goaway', console.log);
 
+  client.on('connect', common.mustCall(() => {
+    assert(!client.encrypted);
+    assert(!client.originSet);
+    assert.strictEqual(client.alpnProtocol, 'h2c');
+  }));
+
   const countdown = new Countdown(count, () => {
     client.close();
     server.close();

From 08f599b1011dda6886cc865fd1967112c60c34ca Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 1 Jan 2018 13:06:46 -0800
Subject: [PATCH 25/77] http2: add aligned padding strategy

Add a padding strategy option that makes a best attempt to ensure
that total frame length for DATA and HEADERS frames are aligned
on multiples of 8-bytes.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17938
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/http2.md                            | 21 +++++++
 src/node_http2.cc                           | 41 +++++++++++--
 src/node_http2.h                            |  4 ++
 test/parallel/test-http2-padding-aligned.js | 68 +++++++++++++++++++++
 4 files changed, 129 insertions(+), 5 deletions(-)
 create mode 100644 test/parallel/test-http2-padding-aligned.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 572543e0826386..fac3c35210a7d2 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1671,6 +1671,13 @@ changes:
      * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
        provided `options.selectPadding` callback is to be used to determine the
        amount of padding.
+     * `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
+       enough padding to ensure that the total frame length, including the
+       9-byte header, is a multiple of 8. For each frame, however, there is a
+       maxmimum allowed number of padding bytes that is determined by current
+       flow control state and settings. If this maximum is less than the
+       calculated amount needed to ensure alignment, the maximum will be used
+       and the total frame length will *not* necessarily be aligned at 8 bytes.
   * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
     streams for the remote peer as if a SETTINGS frame had been received. Will
     be overridden if the remote peer sets its own value for.
@@ -1742,6 +1749,13 @@ changes:
      * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
        provided `options.selectPadding` callback is to be used to determine the
        amount of padding.
+     * `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
+       enough padding to ensure that the total frame length, including the
+       9-byte header, is a multiple of 8. For each frame, however, there is a
+       maxmimum allowed number of padding bytes that is determined by current
+       flow control state and settings. If this maximum is less than the
+       calculated amount needed to ensure alignment, the maximum will be used
+       and the total frame length will *not* necessarily be aligned at 8 bytes.
   * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
     streams for the remote peer as if a SETTINGS frame had been received. Will
     be overridden if the remote peer sets its own value for
@@ -1822,6 +1836,13 @@ changes:
      * `http2.constants.PADDING_STRATEGY_CALLBACK` - Specifies that the user
        provided `options.selectPadding` callback is to be used to determine the
        amount of padding.
+     * `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
+       enough padding to ensure that the total frame length, including the
+       9-byte header, is a multiple of 8. For each frame, however, there is a
+       maxmimum allowed number of padding bytes that is determined by current
+       flow control state and settings. If this maximum is less than the
+       calculated amount needed to ensure alignment, the maximum will be used
+       and the total frame length will *not* necessarily be aligned at 8 bytes.
   * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
     streams for the remote peer as if a SETTINGS frame had been received. Will
     be overridden if the remote peer sets its own value for
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 858d0690a3e784..740072e92d6f1a 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -492,8 +492,7 @@ Http2Session::Http2Session(Environment* env,
   padding_strategy_ = opts.GetPaddingStrategy();
 
   bool hasGetPaddingCallback =
-      padding_strategy_ == PADDING_STRATEGY_MAX ||
-      padding_strategy_ == PADDING_STRATEGY_CALLBACK;
+      padding_strategy_ != PADDING_STRATEGY_NONE;
 
   nghttp2_session_callbacks* callbacks
       = callback_struct_saved[hasGetPaddingCallback ? 1 : 0].callbacks;
@@ -682,6 +681,25 @@ inline void Http2Session::RemoveStream(int32_t id) {
   streams_.erase(id);
 }
 
+// Used as one of the Padding Strategy functions. Will attempt to ensure
+// that the total frame size, including header bytes, are 8-byte aligned.
+// If maxPayloadLen is smaller than the number of bytes necessary to align,
+// will return maxPayloadLen instead.
+inline ssize_t Http2Session::OnDWordAlignedPadding(size_t frameLen,
+                                                   size_t maxPayloadLen) {
+  size_t r = (frameLen + 9) % 8;
+  if (r == 0) return frameLen;  // If already a multiple of 8, return.
+
+  size_t pad = frameLen + (8 - r);
+
+  // If maxPayloadLen happens to be less than the calculated pad length,
+  // use the max instead, even tho this means the frame will not be
+  // aligned.
+  pad = std::min(maxPayloadLen, pad);
+  DEBUG_HTTP2SESSION2(this, "using frame size padding: %d", pad);
+  return pad;
+}
+
 // Used as one of the Padding Strategy functions. Uses the maximum amount
 // of padding allowed for the current frame.
 inline ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
@@ -988,9 +1006,21 @@ inline ssize_t Http2Session::OnSelectPadding(nghttp2_session* handle,
   Http2Session* session = static_cast<Http2Session*>(user_data);
   ssize_t padding = frame->hd.length;
 
-  return session->padding_strategy_ == PADDING_STRATEGY_MAX
-    ? session->OnMaxFrameSizePadding(padding, maxPayloadLen)
-    : session->OnCallbackPadding(padding, maxPayloadLen);
+  switch (session->padding_strategy_) {
+    case PADDING_STRATEGY_NONE:
+      // Fall-through
+      break;
+    case PADDING_STRATEGY_MAX:
+      padding = session->OnMaxFrameSizePadding(padding, maxPayloadLen);
+      break;
+    case PADDING_STRATEGY_ALIGNED:
+      padding = session->OnDWordAlignedPadding(padding, maxPayloadLen);
+      break;
+    case PADDING_STRATEGY_CALLBACK:
+      padding = session->OnCallbackPadding(padding, maxPayloadLen);
+      break;
+  }
+  return padding;
 }
 
 #define BAD_PEER_MESSAGE "Remote peer returned unexpected data while we "     \
@@ -2873,6 +2903,7 @@ void Initialize(Local<Object> target,
   NODE_DEFINE_CONSTANT(constants, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE);
 
   NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_NONE);
+  NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_ALIGNED);
   NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_MAX);
   NODE_DEFINE_CONSTANT(constants, PADDING_STRATEGY_CALLBACK);
 
diff --git a/src/node_http2.h b/src/node_http2.h
index cb44689e31b266..e154b2fc79a6b1 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -358,6 +358,8 @@ HTTP_STATUS_CODES(V)
 enum padding_strategy_type {
   // No padding strategy. This is the default.
   PADDING_STRATEGY_NONE,
+  // Attempts to ensure that the frame is 8-byte aligned
+  PADDING_STRATEGY_ALIGNED,
   // Padding will ensure all data frames are maxFrameSize
   PADDING_STRATEGY_MAX,
   // Padding will be determined via a JS callback. Note that this can be
@@ -917,6 +919,8 @@ class Http2Session : public AsyncWrap {
 
  private:
   // Frame Padding Strategies
+  inline ssize_t OnDWordAlignedPadding(size_t frameLength,
+                                       size_t maxPayloadLen);
   inline ssize_t OnMaxFrameSizePadding(size_t frameLength,
                                        size_t maxPayloadLen);
   inline ssize_t OnCallbackPadding(size_t frame,
diff --git a/test/parallel/test-http2-padding-aligned.js b/test/parallel/test-http2-padding-aligned.js
new file mode 100644
index 00000000000000..183eaef7389360
--- /dev/null
+++ b/test/parallel/test-http2-padding-aligned.js
@@ -0,0 +1,68 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const { PADDING_STRATEGY_ALIGNED } = http2.constants;
+const makeDuplexPair = require('../common/duplexpair');
+
+{
+  const testData = '<h1>Hello World.</h1>';
+  const server = http2.createServer({
+    paddingStrategy: PADDING_STRATEGY_ALIGNED
+  });
+  server.on('stream', common.mustCall((stream, headers) => {
+    stream.respond({
+      'content-type': 'text/html',
+      ':status': 200
+    });
+    stream.end(testData);
+  }));
+
+  const { clientSide, serverSide } = makeDuplexPair();
+
+  // The lengths of the expected writes... note that this is highly
+  // sensitive to how the internals are implemented.
+  const serverLengths = [24, 9, 9, 32];
+  const clientLengths = [9, 9, 48, 9, 1, 21, 1, 16];
+
+  // Adjust for the 24-byte preamble and two 9-byte settings frames, and
+  // the result must be equally divisible by 8
+  assert.strictEqual(
+    (serverLengths.reduce((i, n) => i + n) - 24 - 9 - 9) % 8, 0);
+
+  // Adjust for two 9-byte settings frames, and the result must be equally
+  // divisible by 8
+  assert.strictEqual(
+    (clientLengths.reduce((i, n) => i + n) - 9 - 9) % 8, 0);
+
+  serverSide.on('data', common.mustCall((chunk) => {
+    assert.strictEqual(chunk.length, serverLengths.shift());
+  }, serverLengths.length));
+  clientSide.on('data', common.mustCall((chunk) => {
+    assert.strictEqual(chunk.length, clientLengths.shift());
+  }, clientLengths.length));
+
+  server.emit('connection', serverSide);
+
+  const client = http2.connect('http://localhost:80', {
+    paddingStrategy: PADDING_STRATEGY_ALIGNED,
+    createConnection: common.mustCall(() => clientSide)
+  });
+
+  const req = client.request({ ':path': '/a' });
+
+  req.on('response', common.mustCall());
+
+  req.setEncoding('utf8');
+  req.on('data', common.mustCall((data) => {
+    assert.strictEqual(data, testData);
+  }));
+  req.on('close', common.mustCall(() => {
+    clientSide.destroy();
+    clientSide.end();
+  }));
+  req.end();
+}

From 3601bc4660e75b131f2b36b6bd78f0f50e8f966b Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 1 Jan 2018 17:51:34 -0800
Subject: [PATCH 26/77] http2: properly handle already closed stream error

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17942
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 src/node_http2.cc                             |  27 ++++
 src/node_http2.h                              |   5 +
 test/common/http2.js                          | 129 ++++++++++++++++++
 .../test-http2-misbehaving-multiplex.js       |  59 ++++++++
 4 files changed, 220 insertions(+)
 create mode 100644 test/common/http2.js
 create mode 100644 test/parallel/test-http2-misbehaving-multiplex.js

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 740072e92d6f1a..4a144b9e56cc8d 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -457,6 +457,8 @@ Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
     callbacks, OnNghttpError);
   nghttp2_session_callbacks_set_send_data_callback(
     callbacks, OnSendData);
+  nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(
+    callbacks, OnInvalidFrame);
 
   if (kHasGetPaddingCallback) {
     nghttp2_session_callbacks_set_select_padding_callback(
@@ -881,6 +883,31 @@ inline int Http2Session::OnFrameReceive(nghttp2_session* handle,
   return 0;
 }
 
+inline int Http2Session::OnInvalidFrame(nghttp2_session* handle,
+                                        const nghttp2_frame *frame,
+                                        int lib_error_code,
+                                        void* user_data) {
+  Http2Session* session = static_cast<Http2Session*>(user_data);
+
+  DEBUG_HTTP2SESSION2(session, "invalid frame received, code: %d",
+                      lib_error_code);
+
+  // If the error is fatal or if error code is ERR_STREAM_CLOSED... emit error
+  if (nghttp2_is_fatal(lib_error_code) ||
+      lib_error_code == NGHTTP2_ERR_STREAM_CLOSED) {
+    Environment* env = session->env();
+    Isolate* isolate = env->isolate();
+    HandleScope scope(isolate);
+    Local<Context> context = env->context();
+    Context::Scope context_scope(context);
+
+    Local<Value> argv[1] = {
+      Integer::New(isolate, lib_error_code),
+    };
+    session->MakeCallback(env->error_string(), arraysize(argv), argv);
+  }
+  return 0;
+}
 
 // If nghttp2 is unable to send a queued up frame, it will call this callback
 // to let us know. If the failure occurred because we are in the process of
diff --git a/src/node_http2.h b/src/node_http2.h
index e154b2fc79a6b1..ae35314c1ad380 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -992,6 +992,11 @@ class Http2Session : public AsyncWrap {
       size_t length,
       nghttp2_data_source* source,
       void* user_data);
+  static inline int OnInvalidFrame(
+      nghttp2_session* session,
+      const nghttp2_frame *frame,
+      int lib_error_code,
+      void* user_data);
 
 
   static inline ssize_t OnStreamReadFD(
diff --git a/test/common/http2.js b/test/common/http2.js
new file mode 100644
index 00000000000000..44f5f9191e6e33
--- /dev/null
+++ b/test/common/http2.js
@@ -0,0 +1,129 @@
+/* eslint-disable required-modules */
+'use strict';
+
+// An HTTP/2 testing tool used to create mock frames for direct testing
+// of HTTP/2 endpoints.
+
+const kFrameData = Symbol('frame-data');
+const FLAG_EOS = 0x1;
+const FLAG_ACK = 0x1;
+const FLAG_EOH = 0x4;
+const FLAG_PADDED = 0x8;
+const PADDING = Buffer.alloc(255);
+
+const kClientMagic = Buffer.from('505249202a20485454502f322' +
+                                 'e300d0a0d0a534d0d0a0d0a', 'hex');
+
+const kFakeRequestHeaders = Buffer.from('828684410f7777772e65' +
+                                        '78616d706c652e636f6d', 'hex');
+
+
+const kFakeResponseHeaders = Buffer.from('4803333032580770726976617465611d' +
+                                         '4d6f6e2c203231204f63742032303133' +
+                                         '2032303a31333a323120474d546e1768' +
+                                         '747470733a2f2f7777772e6578616d70' +
+                                         '6c652e636f6d', 'hex');
+
+function isUint32(val) {
+  return val >>> 0 === val;
+}
+
+function isUint24(val) {
+  return val >>> 0 === val && val <= 0xFFFFFF;
+}
+
+function isUint8(val) {
+  return val >>> 0 === val && val <= 0xFF;
+}
+
+function write32BE(array, pos, val) {
+  if (!isUint32(val))
+    throw new RangeError('val is not a 32-bit number');
+  array[pos++] = (val >> 24) & 0xff;
+  array[pos++] = (val >> 16) & 0xff;
+  array[pos++] = (val >> 8) & 0xff;
+  array[pos++] = val & 0xff;
+}
+
+function write24BE(array, pos, val) {
+  if (!isUint24(val))
+    throw new RangeError('val is not a 24-bit number');
+  array[pos++] = (val >> 16) & 0xff;
+  array[pos++] = (val >> 8) & 0xff;
+  array[pos++] = val & 0xff;
+}
+
+function write8(array, pos, val) {
+  if (!isUint8(val))
+    throw new RangeError('val is not an 8-bit number');
+  array[pos] = val;
+}
+
+class Frame {
+  constructor(length, type, flags, id) {
+    this[kFrameData] = Buffer.alloc(9);
+    write24BE(this[kFrameData], 0, length);
+    write8(this[kFrameData], 3, type);
+    write8(this[kFrameData], 4, flags);
+    write32BE(this[kFrameData], 5, id);
+  }
+
+  get data() {
+    return this[kFrameData];
+  }
+}
+
+class SettingsFrame extends Frame {
+  constructor(ack = false) {
+    let flags = 0;
+    if (ack)
+      flags |= FLAG_ACK;
+    super(0, 4, flags, 0);
+  }
+}
+
+class DataFrame extends Frame {
+  constructor(id, payload, padlen = 0, final = false) {
+    let len = payload.length;
+    let flags = 0;
+    if (final) flags |= FLAG_EOS;
+    const buffers = [payload];
+    if (padlen > 0) {
+      buffers.unshift(Buffer.from([padlen]));
+      buffers.push(PADDING.slice(0, padlen));
+      len += padlen + 1;
+      flags |= FLAG_PADDED;
+    }
+    super(len, 0, flags, id);
+    buffers.unshift(this[kFrameData]);
+    this[kFrameData] = Buffer.concat(buffers);
+  }
+}
+
+class HeadersFrame extends Frame {
+  constructor(id, payload, padlen = 0, final = false) {
+    let len = payload.length;
+    let flags = FLAG_EOH;
+    if (final) flags |= FLAG_EOS;
+    const buffers = [payload];
+    if (padlen > 0) {
+      buffers.unshift(Buffer.from([padlen]));
+      buffers.push(PADDING.slice(0, padlen));
+      len += padlen + 1;
+      flags |= FLAG_PADDED;
+    }
+    super(len, 1, flags, id);
+    buffers.unshift(this[kFrameData]);
+    this[kFrameData] = Buffer.concat(buffers);
+  }
+}
+
+module.exports = {
+  Frame,
+  DataFrame,
+  HeadersFrame,
+  SettingsFrame,
+  kFakeRequestHeaders,
+  kFakeResponseHeaders,
+  kClientMagic
+};
diff --git a/test/parallel/test-http2-misbehaving-multiplex.js b/test/parallel/test-http2-misbehaving-multiplex.js
new file mode 100644
index 00000000000000..7d5a7a2f552d49
--- /dev/null
+++ b/test/parallel/test-http2-misbehaving-multiplex.js
@@ -0,0 +1,59 @@
+'use strict';
+
+const common = require('../common');
+
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const h2 = require('http2');
+const net = require('net');
+const h2test = require('../common/http2');
+let client;
+
+const server = h2.createServer();
+server.on('stream', common.mustCall((stream) => {
+  stream.respond();
+  stream.end('ok');
+}, 2));
+server.on('session', common.mustCall((session) => {
+  session.on('error', common.expectsError({
+    code: 'ERR_HTTP2_ERROR',
+    type: Error,
+    message: 'Stream was already closed or invalid'
+  }));
+}));
+
+const settings = new h2test.SettingsFrame();
+const settingsAck = new h2test.SettingsFrame(true);
+const head1 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true);
+const head2 = new h2test.HeadersFrame(3, h2test.kFakeRequestHeaders, 0, true);
+const head3 = new h2test.HeadersFrame(1, h2test.kFakeRequestHeaders, 0, true);
+const head4 = new h2test.HeadersFrame(5, h2test.kFakeRequestHeaders, 0, true);
+
+server.listen(0, () => {
+  client = net.connect(server.address().port, () => {
+    client.write(h2test.kClientMagic, () => {
+      client.write(settings.data, () => {
+        client.write(settingsAck.data);
+        // This will make it ok.
+        client.write(head1.data, () => {
+          // This will make it ok.
+          client.write(head2.data, () => {
+            // This will cause an error to occur because the client is
+            // attempting to reuse an already closed stream. This must
+            // cause the server session to be torn down.
+            client.write(head3.data, () => {
+              // This won't ever make it to the server
+              client.write(head4.data);
+            });
+          });
+        });
+      });
+    });
+  });
+
+  // An error may or may not be emitted on the client side, we don't care
+  // either way if it is, but we don't want to die if it is.
+  client.on('error', () => {});
+  client.on('close', common.mustCall(() => server.close()));
+});

From 21c0a8ca96ccf44c3196cd3432bd35f6d39113ed Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Tue, 2 Jan 2018 10:58:03 -0800
Subject: [PATCH 27/77] doc: add docs for common/http2.js utility

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17942
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 test/common/README.md | 138 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 138 insertions(+)

diff --git a/test/common/README.md b/test/common/README.md
index 030b3e0366b24a..e4bf2887ca3a71 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -13,6 +13,7 @@ This directory contains modules used to test the Node.js implementation.
 * [Internet module](#internet-module)
 * [tmpdir module](#tmpdir-module)
 * [WPT module](#wpt-module)
+* [HTTP2 module](#http2-module)
 
 ## Benchmark Module
 
@@ -559,6 +560,143 @@ Node.js
 implementation with tests from
 [W3C Web Platform Tests](https://github.com/w3c/web-platform-tests).
 
+## HTTP/2 Module
+
+The http2.js module provides a handful of utilities for creating mock HTTP/2
+frames for testing of HTTP/2 endpoints
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+const http2 = require('../common/http2');
+```
+
+### Class: Frame
+
+The `http2.Frame` is a base class that creates a `Buffer` containing a
+serialized HTTP/2 frame header.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+// length is a 24-bit unsigned integer
+// type is an 8-bit unsigned integer identifying the frame type
+// flags is an 8-bit unsigned integer containing the flag bits
+// id is the 32-bit stream identifier, if any.
+const frame = new http2.Frame(length, type, flags, id);
+
+// Write the frame data to a socket
+socket.write(frame.data);
+```
+
+The serialized `Buffer` may be retrieved using the `frame.data` property.
+
+### Class: DataFrame extends Frame
+
+The `http2.DataFrame` is a subclass of `http2.Frame` that serializes a `DATA`
+frame.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+// id is the 32-bit stream identifier
+// payload is a Buffer containing the DATA payload
+// padlen is an 8-bit integer giving the number of padding bytes to include
+// final is a boolean indicating whether the End-of-stream flag should be set,
+// defaults to false.
+const data = new http2.DataFrame(id, payload, padlen, final);
+
+socket.write(frame.data);
+```
+
+### Class: HeadersFrame
+
+The `http2.HeadersFrame` is a subclass of `http2.Frame` that serializes a
+`HEADERS` frame.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+// id is the 32-bit stream identifier
+// payload is a Buffer containing the HEADERS payload (see either
+// http2.kFakeRequestHeaders or http2.kFakeResponseHeaders).
+// padlen is an 8-bit integer giving the number of padding bytes to include
+// final is a boolean indicating whether the End-of-stream flag should be set,
+// defaults to false.
+const data = new http2.HeadersFrame(id, http2.kFakeRequestHeaders,
+                                    padlen, final);
+
+socket.write(frame.data);
+```
+
+### Class: SettingsFrame
+
+The `http2.SettingsFrame` is a subclass of `http2.Frame` that serializes an
+empty `SETTINGS` frame.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+// ack is a boolean indicating whether or not to set the ACK flag.
+const frame = new http2.SettingsFrame(ack);
+
+socket.write(frame.data);
+```
+
+### http2.kFakeRequestHeaders
+
+Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2
+request headers to be used as the payload of a `http2.HeadersFrame`.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+const frame = new http2.HeadersFrame(1, http2.kFakeRequestHeaders, 0, true);
+
+socket.write(frame.data);
+```
+
+### http2.kFakeResponseHeaders
+
+Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2
+response headers to be used as the payload a `http2.HeadersFrame`.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+const frame = new http2.HeadersFrame(1, http2.kFakeResponseHeaders, 0, true);
+
+socket.write(frame.data);
+```
+
+### http2.kClientMagic
+
+Set to a `Buffer` containing the preamble bytes an HTTP/2 client must send
+upon initial establishment of a connection.
+
+<!-- eslint-disable strict -->
+<!-- eslint-disable required-modules -->
+<!-- eslint-disable no-unused-vars -->
+<!-- eslint-disable no-undef -->
+```js
+socket.write(http2.kClientMagic);
+```
+
+
 [&lt;Array>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
 [&lt;ArrayBufferView&#91;&#93;>]: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView
 [&lt;Boolean>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type

From 098d0527d44d6d83e5e976f8632dd92f20871c1f Mon Sep 17 00:00:00 2001
From: cjihrig <cjihrig@gmail.com>
Date: Tue, 2 Jan 2018 13:00:12 -0500
Subject: [PATCH 28/77] src: silence http2 -Wunused-result warnings

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17954
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 src/node_http2.cc | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 4a144b9e56cc8d..0c458ca33a3778 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -560,13 +560,13 @@ inline void Http2Stream::EmitStatistics() {
           FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstByte"),
           Number::New(env->isolate(),
                       (entry->first_byte() - entry->startTimeNano()) / 1e6),
-          attr);
+          attr).FromJust();
       obj->DefineOwnProperty(
           context,
           FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstHeader"),
           Number::New(env->isolate(),
                       (entry->first_header() - entry->startTimeNano()) / 1e6),
-          attr);
+          attr).FromJust();
       entry->Notify(obj);
     }
     delete entry;
@@ -592,25 +592,29 @@ inline void Http2Session::EmitStatistics() {
           String::NewFromUtf8(env->isolate(),
                               entry->typeName(),
                               v8::NewStringType::kInternalized)
-                                  .ToLocalChecked(), attr);
+                                  .ToLocalChecked(), attr).FromJust();
       if (entry->ping_rtt() != 0) {
         obj->DefineOwnProperty(
             context,
             FIXED_ONE_BYTE_STRING(env->isolate(), "pingRTT"),
-            Number::New(env->isolate(), entry->ping_rtt() / 1e6), attr);
+            Number::New(env->isolate(), entry->ping_rtt() / 1e6),
+            attr).FromJust();
       }
       obj->DefineOwnProperty(
           context,
           FIXED_ONE_BYTE_STRING(env->isolate(), "framesReceived"),
-          Integer::NewFromUnsigned(env->isolate(), entry->frame_count()), attr);
+          Integer::NewFromUnsigned(env->isolate(), entry->frame_count()),
+          attr).FromJust();
       obj->DefineOwnProperty(
           context,
           FIXED_ONE_BYTE_STRING(env->isolate(), "streamCount"),
-          Integer::New(env->isolate(), entry->stream_count()), attr);
+          Integer::New(env->isolate(), entry->stream_count()),
+          attr).FromJust();
       obj->DefineOwnProperty(
           context,
           FIXED_ONE_BYTE_STRING(env->isolate(), "streamAverageDuration"),
-          Number::New(env->isolate(), entry->stream_average_duration()), attr);
+          Number::New(env->isolate(), entry->stream_average_duration()),
+          attr).FromJust();
       entry->Notify(obj);
     }
     delete entry;

From 3a5e97fb0ff597901419b0eeca9d98d2e826e68e Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Wed, 3 Jan 2018 11:15:57 -0800
Subject: [PATCH 29/77] http2: implement maxSessionMemory

The maxSessionMemory is a cap for the amount of memory an
Http2Session is permitted to consume. If exceeded, new
`Http2Stream` sessions will be rejected with an
`ENHANCE_YOUR_CALM` error and existing `Http2Stream`
instances that are still receiving headers will be
terminated with an `ENHANCE_YOUR_CALM` error.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17967
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/http2.md                              | 27 +++++++++
 lib/internal/http2/util.js                    |  8 ++-
 src/node_http2.cc                             | 55 +++++++++++++++----
 src/node_http2.h                              | 45 ++++++++++++++-
 src/node_http2_state.h                        |  1 +
 .../test-http2-util-update-options-buffer.js  |  7 ++-
 .../test-http2-max-session-memory.js          | 44 +++++++++++++++
 7 files changed, 172 insertions(+), 15 deletions(-)
 create mode 100644 test/sequential/test-http2-max-session-memory.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index fac3c35210a7d2..17c1ae018f9e4b 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1652,6 +1652,15 @@ changes:
 * `options` {Object}
   * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
     for deflating header fields. **Default:** `4Kib`
+  * `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
+    is permitted to use. The value is expressed in terms of number of megabytes,
+    e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
+    `10`. This is a credit based limit, existing `Http2Stream`s may cause this
+    limit to be exceeded, but new `Http2Stream` instances will be rejected
+    while this limit is exceeded. The current number of `Http2Stream` sessions,
+    the current memory use of the header compression tables, current data
+    queued to be sent, and unacknowledged PING and SETTINGS frames are all
+    counted towards the current limit.
   * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
     **Default:** `128`. The minimum value is `4`.
   * `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1730,6 +1739,15 @@ changes:
     `false`. See the [`'unknownProtocol'`][] event. See [ALPN negotiation][].
   * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
     for deflating header fields. **Default:** `4Kib`
+  * `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
+    is permitted to use. The value is expressed in terms of number of megabytes,
+    e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
+    `10`. This is a credit based limit, existing `Http2Stream`s may cause this
+    limit to be exceeded, but new `Http2Stream` instances will be rejected
+    while this limit is exceeded. The current number of `Http2Stream` sessions,
+    the current memory use of the header compression tables, current data
+    queued to be sent, and unacknowledged PING and SETTINGS frames are all
+    counted towards the current limit.
   * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
     **Default:** `128`. The minimum value is `4`.
   * `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1813,6 +1831,15 @@ changes:
 * `options` {Object}
   * `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
     for deflating header fields. **Default:** `4Kib`
+  * `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
+    is permitted to use. The value is expressed in terms of number of megabytes,
+    e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
+    `10`. This is a credit based limit, existing `Http2Stream`s may cause this
+    limit to be exceeded, but new `Http2Stream` instances will be rejected
+    while this limit is exceeded. The current number of `Http2Stream` sessions,
+    the current memory use of the header compression tables, current data
+    queued to be sent, and unacknowledged PING and SETTINGS frames are all
+    counted towards the current limit.
   * `maxHeaderListPairs` {number} Sets the maximum number of header entries.
     **Default:** `128`. The minimum value is `1`.
   * `maxOutstandingPings` {number} Sets the maximum number of outstanding,
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index 51a2094d43af56..ef48b83d783af0 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -175,7 +175,8 @@ const IDX_OPTIONS_PADDING_STRATEGY = 4;
 const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
 const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
 const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
-const IDX_OPTIONS_FLAGS = 8;
+const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
+const IDX_OPTIONS_FLAGS = 9;
 
 function updateOptionsBuffer(options) {
   var flags = 0;
@@ -219,6 +220,11 @@ function updateOptionsBuffer(options) {
     optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS] =
       Math.max(1, options.maxOutstandingSettings);
   }
+  if (typeof options.maxSessionMemory === 'number') {
+    flags |= (1 << IDX_OPTIONS_MAX_SESSION_MEMORY);
+    optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY] =
+      Math.max(1, options.maxSessionMemory);
+  }
   optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
 }
 
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 0c458ca33a3778..2ead727b116b35 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -174,6 +174,18 @@ Http2Options::Http2Options(Environment* env) {
   if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS)) {
     SetMaxOutstandingSettings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS]);
   }
+
+  // The HTTP2 specification places no limits on the amount of memory
+  // that a session can consume. In order to prevent abuse, we place a
+  // cap on the amount of memory a session can consume at any given time.
+  // this is a credit based system. Existing streams may cause the limit
+  // to be temporarily exceeded but once over the limit, new streams cannot
+  // created.
+  // Important: The maxSessionMemory option in javascript is expressed in
+  //            terms of MB increments (i.e. the value 1 == 1 MB)
+  if (flags & (1 << IDX_OPTIONS_MAX_SESSION_MEMORY)) {
+    SetMaxSessionMemory(buffer[IDX_OPTIONS_MAX_SESSION_MEMORY] * 1e6);
+  }
 }
 
 void Http2Session::Http2Settings::Init() {
@@ -482,11 +494,13 @@ Http2Session::Http2Session(Environment* env,
   // Capture the configuration options for this session
   Http2Options opts(env);
 
-  int32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
+  max_session_memory_ = opts.GetMaxSessionMemory();
+
+  uint32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
   max_header_pairs_ =
       type == NGHTTP2_SESSION_SERVER
-          ? std::max(maxHeaderPairs, 4)     // minimum # of request headers
-          : std::max(maxHeaderPairs, 1);    // minimum # of response headers
+          ? std::max(maxHeaderPairs, 4U)     // minimum # of request headers
+          : std::max(maxHeaderPairs, 1U);    // minimum # of response headers
 
   max_outstanding_pings_ = opts.GetMaxOutstandingPings();
   max_outstanding_settings_ = opts.GetMaxOutstandingSettings();
@@ -673,18 +687,21 @@ inline bool Http2Session::CanAddStream() {
   size_t maxSize =
       std::min(streams_.max_size(), static_cast<size_t>(maxConcurrentStreams));
   // We can add a new stream so long as we are less than the current
-  // maximum on concurrent streams
-  return streams_.size() < maxSize;
+  // maximum on concurrent streams and there's enough available memory
+  return streams_.size() < maxSize &&
+         IsAvailableSessionMemory(sizeof(Http2Stream));
 }
 
 inline void Http2Session::AddStream(Http2Stream* stream) {
   CHECK_GE(++statistics_.stream_count, 0);
   streams_[stream->id()] = stream;
+  IncrementCurrentSessionMemory(stream->self_size());
 }
 
 
-inline void Http2Session::RemoveStream(int32_t id) {
-  streams_.erase(id);
+inline void Http2Session::RemoveStream(Http2Stream* stream) {
+  streams_.erase(stream->id());
+  DecrementCurrentSessionMemory(stream->self_size());
 }
 
 // Used as one of the Padding Strategy functions. Will attempt to ensure
@@ -1678,7 +1695,7 @@ Http2Stream::Http2Stream(
 
 Http2Stream::~Http2Stream() {
   if (session_ != nullptr) {
-    session_->RemoveStream(id_);
+    session_->RemoveStream(this);
     session_ = nullptr;
   }
 
@@ -2008,7 +2025,7 @@ inline int Http2Stream::DoWrite(WriteWrap* req_wrap,
       i == nbufs - 1 ? req_wrap : nullptr,
       bufs[i]
     });
-    available_outbound_length_ += bufs[i].len;
+    IncrementAvailableOutboundLength(bufs[i].len);
   }
   CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM);
   return 0;
@@ -2030,7 +2047,10 @@ inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
   if (this->statistics_.first_header == 0)
     this->statistics_.first_header = uv_hrtime();
   size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
-  if (current_headers_.size() == max_header_pairs_ ||
+  // A header can only be added if we have not exceeded the maximum number
+  // of headers and the session has memory available for it.
+  if (!session_->IsAvailableSessionMemory(length) ||
+      current_headers_.size() == max_header_pairs_ ||
       current_headers_length_ + length > max_header_length_) {
     return false;
   }
@@ -2174,7 +2194,7 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
       // Just return the length, let Http2Session::OnSendData take care of
       // actually taking the buffers out of the queue.
       *flags |= NGHTTP2_DATA_FLAG_NO_COPY;
-      stream->available_outbound_length_ -= amount;
+      stream->DecrementAvailableOutboundLength(amount);
     }
   }
 
@@ -2197,6 +2217,15 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
   return amount;
 }
 
+inline void Http2Stream::IncrementAvailableOutboundLength(size_t amount) {
+  available_outbound_length_ += amount;
+  session_->IncrementCurrentSessionMemory(amount);
+}
+
+inline void Http2Stream::DecrementAvailableOutboundLength(size_t amount) {
+  available_outbound_length_ -= amount;
+  session_->DecrementCurrentSessionMemory(amount);
+}
 
 
 // Implementation of the JavaScript API
@@ -2690,6 +2719,7 @@ Http2Session::Http2Ping* Http2Session::PopPing() {
   if (!outstanding_pings_.empty()) {
     ping = outstanding_pings_.front();
     outstanding_pings_.pop();
+    DecrementCurrentSessionMemory(ping->self_size());
   }
   return ping;
 }
@@ -2698,6 +2728,7 @@ bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
   if (outstanding_pings_.size() == max_outstanding_pings_)
     return false;
   outstanding_pings_.push(ping);
+  IncrementCurrentSessionMemory(ping->self_size());
   return true;
 }
 
@@ -2706,6 +2737,7 @@ Http2Session::Http2Settings* Http2Session::PopSettings() {
   if (!outstanding_settings_.empty()) {
     settings = outstanding_settings_.front();
     outstanding_settings_.pop();
+    DecrementCurrentSessionMemory(settings->self_size());
   }
   return settings;
 }
@@ -2714,6 +2746,7 @@ bool Http2Session::AddSettings(Http2Session::Http2Settings* settings) {
   if (outstanding_settings_.size() == max_outstanding_settings_)
     return false;
   outstanding_settings_.push(settings);
+  IncrementCurrentSessionMemory(settings->self_size());
   return true;
 }
 
diff --git a/src/node_http2.h b/src/node_http2.h
index ae35314c1ad380..5b5f7e5f52e084 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -82,6 +82,9 @@ void inline debug_vfprintf(const char* format, ...) {
 // Also strictly limit the number of outstanding SETTINGS frames a user sends
 #define DEFAULT_MAX_SETTINGS 10
 
+// Default maximum total memory cap for Http2Session.
+#define DEFAULT_MAX_SESSION_MEMORY 1e7;
+
 // These are the standard HTTP/2 defaults as specified by the RFC
 #define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
 #define DEFAULT_SETTINGS_ENABLE_PUSH 1
@@ -500,8 +503,17 @@ class Http2Options {
     return max_outstanding_settings_;
   }
 
+  void SetMaxSessionMemory(uint64_t max) {
+    max_session_memory_ = max;
+  }
+
+  uint64_t GetMaxSessionMemory() {
+    return max_session_memory_;
+  }
+
  private:
   nghttp2_option* options_;
+  uint64_t max_session_memory_ = DEFAULT_MAX_SESSION_MEMORY;
   uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
   padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
   size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
@@ -628,6 +640,9 @@ class Http2Stream : public AsyncWrap,
   // Returns the stream identifier for this stream
   inline int32_t id() const { return id_; }
 
+  inline void IncrementAvailableOutboundLength(size_t amount);
+  inline void DecrementAvailableOutboundLength(size_t amount);
+
   inline bool AddHeader(nghttp2_rcbuf* name,
                         nghttp2_rcbuf* value,
                         uint8_t flags);
@@ -848,7 +863,7 @@ class Http2Session : public AsyncWrap {
   inline void AddStream(Http2Stream* stream);
 
   // Removes a stream instance from this session
-  inline void RemoveStream(int32_t id);
+  inline void RemoveStream(Http2Stream* stream);
 
   // Write data to the session
   inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
@@ -906,6 +921,30 @@ class Http2Session : public AsyncWrap {
   Http2Settings* PopSettings();
   bool AddSettings(Http2Settings* settings);
 
+  void IncrementCurrentSessionMemory(uint64_t amount) {
+    current_session_memory_ += amount;
+  }
+
+  void DecrementCurrentSessionMemory(uint64_t amount) {
+    current_session_memory_ -= amount;
+  }
+
+  // Returns the current session memory including the current size of both
+  // the inflate and deflate hpack headers, the current outbound storage
+  // queue, and pending writes.
+  uint64_t GetCurrentSessionMemory() {
+    uint64_t total = current_session_memory_ + sizeof(Http2Session);
+    total += nghttp2_session_get_hd_deflate_dynamic_table_size(session_);
+    total += nghttp2_session_get_hd_inflate_dynamic_table_size(session_);
+    total += outgoing_storage_.size();
+    return total;
+  }
+
+  // Return true if current_session_memory + amount is less than the max
+  bool IsAvailableSessionMemory(uint64_t amount) {
+    return GetCurrentSessionMemory() + amount <= max_session_memory_;
+  }
+
   struct Statistics {
     uint64_t start_time;
     uint64_t end_time;
@@ -1035,6 +1074,10 @@ class Http2Session : public AsyncWrap {
   // The maximum number of header pairs permitted for streams on this session
   uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
 
+  // The maximum amount of memory allocated for this session
+  uint64_t max_session_memory_ = DEFAULT_MAX_SESSION_MEMORY;
+  uint64_t current_session_memory_ = 0;
+
   // The collection of active Http2Streams associated with this session
   std::unordered_map<int32_t, Http2Stream*> streams_;
 
diff --git a/src/node_http2_state.h b/src/node_http2_state.h
index ef8696ce60d8f8..af0740c994e765 100644
--- a/src/node_http2_state.h
+++ b/src/node_http2_state.h
@@ -50,6 +50,7 @@ namespace http2 {
     IDX_OPTIONS_MAX_HEADER_LIST_PAIRS,
     IDX_OPTIONS_MAX_OUTSTANDING_PINGS,
     IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS,
+    IDX_OPTIONS_MAX_SESSION_MEMORY,
     IDX_OPTIONS_FLAGS
   };
 
diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js
index 5768ce0204dc8c..6ab8bcff02866e 100644
--- a/test/parallel/test-http2-util-update-options-buffer.js
+++ b/test/parallel/test-http2-util-update-options-buffer.js
@@ -20,7 +20,8 @@ const IDX_OPTIONS_PADDING_STRATEGY = 4;
 const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
 const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
 const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
-const IDX_OPTIONS_FLAGS = 8;
+const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
+const IDX_OPTIONS_FLAGS = 9;
 
 {
   updateOptionsBuffer({
@@ -31,7 +32,8 @@ const IDX_OPTIONS_FLAGS = 8;
     paddingStrategy: 5,
     maxHeaderListPairs: 6,
     maxOutstandingPings: 7,
-    maxOutstandingSettings: 8
+    maxOutstandingSettings: 8,
+    maxSessionMemory: 9
   });
 
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -42,6 +44,7 @@ const IDX_OPTIONS_FLAGS = 8;
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7);
   strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS], 8);
+  strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY], 9);
 
   const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
 
diff --git a/test/sequential/test-http2-max-session-memory.js b/test/sequential/test-http2-max-session-memory.js
new file mode 100644
index 00000000000000..e16000d1261ab0
--- /dev/null
+++ b/test/sequential/test-http2-max-session-memory.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const http2 = require('http2');
+
+// Test that maxSessionMemory Caps work
+
+const largeBuffer = Buffer.alloc(1e6);
+
+const server = http2.createServer({ maxSessionMemory: 1 });
+
+server.on('stream', common.mustCall((stream) => {
+  stream.respond();
+  stream.end(largeBuffer);
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+
+  {
+    const req = client.request();
+
+    req.on('response', () => {
+      // This one should be rejected because the server is over budget
+      // on the current memory allocation
+      const req = client.request();
+      req.on('error', common.expectsError({
+        code: 'ERR_HTTP2_STREAM_ERROR',
+        type: Error,
+        message: 'Stream closed with error code 11'
+      }));
+      req.on('close', common.mustCall(() => {
+        server.close();
+        client.destroy();
+      }));
+    });
+
+    req.resume();
+    req.on('close', common.mustCall());
+  }
+}));

From b3d09deed741e7ae0fd65d28a718f9418ecc94a6 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Wed, 3 Jan 2018 12:02:46 -0800
Subject: [PATCH 30/77] http2: verify that a dependency cycle may exist

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17968
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 test/parallel/test-http2-priority-cycle-.js | 69 +++++++++++++++++++++
 1 file changed, 69 insertions(+)
 create mode 100644 test/parallel/test-http2-priority-cycle-.js

diff --git a/test/parallel/test-http2-priority-cycle-.js b/test/parallel/test-http2-priority-cycle-.js
new file mode 100644
index 00000000000000..af0d66d8343cbf
--- /dev/null
+++ b/test/parallel/test-http2-priority-cycle-.js
@@ -0,0 +1,69 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const Countdown = require('../common/countdown');
+
+const server = http2.createServer();
+const largeBuffer = Buffer.alloc(1e4);
+
+// Verify that a dependency cycle may exist, but that it doesn't crash anything
+
+server.on('stream', common.mustCall((stream) => {
+  stream.respond();
+  setImmediate(() => {
+    stream.end(largeBuffer);
+  });
+}, 3));
+server.on('session', common.mustCall((session) => {
+  session.on('priority', (id, parent, weight, exclusive) => {
+    assert.strictEqual(weight, 16);
+    assert.strictEqual(exclusive, false);
+    switch (id) {
+      case 1:
+        assert.strictEqual(parent, 5);
+        break;
+      case 3:
+        assert.strictEqual(parent, 1);
+        break;
+      case 5:
+        assert.strictEqual(parent, 3);
+        break;
+      default:
+        assert.fail('should not happen');
+    }
+  });
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+
+  const countdown = new Countdown(3, () => {
+    client.close();
+    server.close();
+  });
+
+  {
+    const req = client.request();
+    req.priority({ parent: 5 });
+    req.resume();
+    req.on('close', () => countdown.dec());
+  }
+
+  {
+    const req = client.request();
+    req.priority({ parent: 1 });
+    req.resume();
+    req.on('close', () => countdown.dec());
+  }
+
+  {
+    const req = client.request();
+    req.priority({ parent: 3 });
+    req.resume();
+    req.on('close', () => countdown.dec());
+  }
+}));

From 26c8b3c21c828fd2befc4d45f0a407e9d07bf27a Mon Sep 17 00:00:00 2001
From: Rich Trott <rtrott@gmail.com>
Date: Wed, 3 Jan 2018 16:39:46 -0800
Subject: [PATCH 31/77] doc: grammar fixes in http2.md
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17972
Reviewed-By: Weijia Wang <starkwang@126.com>
Reviewed-By: Jon Moss <me@jonathanmoss.me>
Reviewed-By: Evan Lucas <evanlucas@me.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
---
 doc/api/http2.md | 56 ++++++++++++++++++++++++------------------------
 1 file changed, 28 insertions(+), 28 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 17c1ae018f9e4b..b0b692c492fe64 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -166,7 +166,7 @@ When invoked, the handler function will receive three arguments:
 
 If the `'frameError'` event is associated with a stream, the stream will be
 closed and destroyed immediately following the `'frameError'` event. If the
-event is not associated with a stream, the `Http2Session` will be shutdown
+event is not associated with a stream, the `Http2Session` will be shut down
 immediately following the `'frameError'` event.
 
 #### Event: 'goaway'
@@ -183,7 +183,7 @@ the handler function will receive three arguments:
 * `opaqueData` {Buffer} If additional opaque data was included in the GOAWAY
   frame, a `Buffer` instance will be passed containing that data.
 
-*Note*: The `Http2Session` instance will be shutdown automatically when the
+*Note*: The `Http2Session` instance will be shut down automatically when the
 `'goaway'` event is emitted.
 
 #### Event: 'localSettings'
@@ -499,7 +499,7 @@ added: v8.4.0
   has been completed.
 * Returns: {undefined}
 
-Attempts to shutdown this `Http2Session` using HTTP/2 defined procedures.
+Attempts to shut down this `Http2Session` using HTTP/2 defined procedures.
 If specified, the given `callback` function will be invoked once the shutdown
 process has completed.
 
@@ -647,7 +647,7 @@ may be passed to clear any previously set alternative service for a given
 domain.
 
 When a string is passed for the `originOrStream` argument, it will be parsed as
-a URL and the origin will be derived. For insetance, the origin for the
+a URL and the origin will be derived. For instance, the origin for the
 HTTP URL `'https://example.org/foo/bar'` is the ASCII string
 `'https://example.org'`. An error will be thrown if either the given string
 cannot be parsed as a URL or if a valid origin cannot be derived.
@@ -751,15 +751,15 @@ req.on('response', (headers) => {
 
 When set, the `options.getTrailers()` function is called immediately after
 queuing the last chunk of payload data to be sent. The callback is passed a
-single object (with a `null` prototype) that the listener may used to specify
+single object (with a `null` prototype) that the listener may use to specify
 the trailing header fields to send to the peer.
 
 *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
-"pseudo-header" fields (e.g. `':method'`, `':path'`, etc). An `'error'` event
+pseudo-header fields (e.g. `':method'`, `':path'`, etc). An `'error'` event
 will be emitted if the `getTrailers` callback attempts to set such header
 fields.
 
-The `:method` and `:path` pseudoheaders are not specified within `headers`,
+The `:method` and `:path` pseudo-headers are not specified within `headers`,
 they respectively default to:
 
 * `:method` = `'GET'`
@@ -786,7 +786,7 @@ On the client, `Http2Stream` instances are created and returned when either the
 `'push'` event.
 
 *Note*: The `Http2Stream` class is a base for the [`ServerHttp2Stream`][] and
-[`ClientHttp2Stream`][] classes, each of which are used specifically by either
+[`ClientHttp2Stream`][] classes, each of which is used specifically by either
 the Server or Client side, respectively.
 
 All `Http2Stream` instances are [`Duplex`][] streams. The `Writable` side of the
@@ -810,7 +810,7 @@ On the client side, instances of [`ClientHttp2Stream`][] are created when the
 `http2session.request()` may not be immediately ready for use if the parent
 `Http2Session` has not yet been fully established. In such cases, operations
 called on the `Http2Stream` will be buffered until the `'ready'` event is
-emitted. User code should rarely, if ever, have need to handle the `'ready'`
+emitted. User code should rarely, if ever, need to handle the `'ready'`
 event directly. The ready status of an `Http2Stream` can be determined by
 checking the value of `http2stream.id`. If the value is `undefined`, the stream
 is not yet ready for use.
@@ -1067,7 +1067,7 @@ added: v8.4.0
 -->
 
 The `'headers'` event is emitted when an additional block of headers is received
-for a stream, such as when a block of `1xx` informational headers are received.
+for a stream, such as when a block of `1xx` informational headers is received.
 The listener callback is passed the [Headers Object][] and flags associated with
 the headers.
 
@@ -1217,7 +1217,7 @@ server.on('stream', (stream) => {
 
 When set, the `options.getTrailers()` function is called immediately after
 queuing the last chunk of payload data to be sent. The callback is passed a
-single object (with a `null` prototype) that the listener may used to specify
+single object (with a `null` prototype) that the listener may use to specify
 the trailing header fields to send to the peer.
 
 ```js
@@ -1234,7 +1234,7 @@ server.on('stream', (stream) => {
 ```
 
 *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
-"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
+pseudo-header fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
 will be emitted if the `getTrailers` callback attempts to set such header
 fields.
 
@@ -1291,7 +1291,7 @@ requests.
 
 When set, the `options.getTrailers()` function is called immediately after
 queuing the last chunk of payload data to be sent. The callback is passed a
-single object (with a `null` prototype) that the listener may used to specify
+single object (with a `null` prototype) that the listener may use to specify
 the trailing header fields to send to the peer.
 
 ```js
@@ -1318,7 +1318,7 @@ server.on('close', () => fs.closeSync(fd));
 ```
 
 *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
-"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
+pseudo-header fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
 will be emitted if the `getTrailers` callback attempts to set such header
 fields.
 
@@ -1350,7 +1350,7 @@ of the given file:
 
 If an error occurs while attempting to read the file data, the `Http2Stream`
 will be closed using an `RST_STREAM` frame using the standard `INTERNAL_ERROR`
-code. If the `onError` callback is defined it will be called, otherwise
+code. If the `onError` callback is defined, then it will be called. Otherwise
 the stream will be destroyed.
 
 Example using a file path:
@@ -1410,7 +1410,7 @@ default behavior is to destroy the stream.
 
 When set, the `options.getTrailers()` function is called immediately after
 queuing the last chunk of payload data to be sent. The callback is passed a
-single object (with a `null` prototype) that the listener may used to specify
+single object (with a `null` prototype) that the listener may use to specify
 the trailing header fields to send to the peer.
 
 ```js
@@ -1427,7 +1427,7 @@ server.on('stream', (stream) => {
 ```
 
 *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2
-"pseudo-header" fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
+pseudo-header fields (e.g. `':status'`, `':path'`, etc). An `'error'` event
 will be emitted if the `getTrailers` callback attempts to set such header
 fields.
 
@@ -1497,7 +1497,7 @@ an `Http2Session` object associated with the `Http2Server`.
 added: v8.5.0
 -->
 
-If an `ServerHttp2Stream` emits an `'error'` event, it will be forwarded here.
+If a `ServerHttp2Stream` emits an `'error'` event, it will be forwarded here.
 The stream will already be destroyed when this event is triggered.
 
 #### Event: 'stream'
@@ -1683,7 +1683,7 @@ changes:
      * `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
        enough padding to ensure that the total frame length, including the
        9-byte header, is a multiple of 8. For each frame, however, there is a
-       maxmimum allowed number of padding bytes that is determined by current
+       maximum allowed number of padding bytes that is determined by current
        flow control state and settings. If this maximum is less than the
        calculated amount needed to ensure alignment, the maximum will be used
        and the total frame length will *not* necessarily be aligned at 8 bytes.
@@ -1770,7 +1770,7 @@ changes:
      * `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
        enough padding to ensure that the total frame length, including the
        9-byte header, is a multiple of 8. For each frame, however, there is a
-       maxmimum allowed number of padding bytes that is determined by current
+       maximum allowed number of padding bytes that is determined by current
        flow control state and settings. If this maximum is less than the
        calculated amount needed to ensure alignment, the maximum will be used
        and the total frame length will *not* necessarily be aligned at 8 bytes.
@@ -1866,7 +1866,7 @@ changes:
      * `http2.constants.PADDING_STRATEGY_ALIGNED` - Will *attempt* to apply
        enough padding to ensure that the total frame length, including the
        9-byte header, is a multiple of 8. For each frame, however, there is a
-       maxmimum allowed number of padding bytes that is determined by current
+       maximum allowed number of padding bytes that is determined by current
        flow control state and settings. If this maximum is less than the
        calculated amount needed to ensure alignment, the maximum will be used
        and the total frame length will *not* necessarily be aligned at 8 bytes.
@@ -2208,8 +2208,8 @@ req.end('Jane');
 
 The Compatibility API has the goal of providing a similar developer experience
 of HTTP/1 when using HTTP/2, making it possible to develop applications
-that supports both [HTTP/1][] and HTTP/2. This API targets only the
-**public API** of the [HTTP/1][], however many modules uses internal
+that support both [HTTP/1][] and HTTP/2. This API targets only the
+**public API** of the [HTTP/1][]. However many modules use internal
 methods or state, and those _are not supported_ as it is a completely
 different implementation.
 
@@ -2237,7 +2237,7 @@ the status message for HTTP codes is ignored.
 
 ### ALPN negotiation
 
-ALPN negotiation allows to support both [HTTPS][] and HTTP/2 over
+ALPN negotiation allows supporting both [HTTPS][] and HTTP/2 over
 the same socket. The `req` and `res` objects can be either HTTP/1 or
 HTTP/2, and an application **must** restrict itself to the public API of
 [HTTP/1][], and detect if it is possible to use the more advanced
@@ -2279,7 +2279,7 @@ added: v8.4.0
 
 A `Http2ServerRequest` object is created by [`http2.Server`][] or
 [`http2.SecureServer`][] and passed as the first argument to the
-[`'request'`][] event. It may be used to access a request status, headers and
+[`'request'`][] event. It may be used to access a request status, headers, and
 data.
 
 It implements the [Readable Stream][] interface, as well as the
@@ -2340,7 +2340,7 @@ console.log(request.headers);
 
 See [Headers Object][].
 
-*Note*: In HTTP/2, the request path, host name, protocol, and method are
+*Note*: In HTTP/2, the request path, hostname, protocol, and method are
 represented as special headers prefixed with the `:` character (e.g. `':path'`).
 These special headers will be included in the `request.headers` object. Care
 must be taken not to inadvertently modify these special headers or errors may
@@ -2373,7 +2373,7 @@ added: v8.4.0
 
 * {string}
 
-The request method as a string. Read only. Example:
+The request method as a string. Read-only. Example:
 `'GET'`, `'DELETE'`.
 
 #### request.rawHeaders
@@ -3031,7 +3031,7 @@ If `name` is equal to `Http2Session`, the `PerformanceEntry` will contain the
 following additional properties:
 
 * `pingRTT` {number} The number of milliseconds elapsed since the transmission
-  of a `PING` frame and the reception of its acknowledgement. Only present if
+  of a `PING` frame and the reception of its acknowledgment. Only present if
   a `PING` frame has been sent on the `Http2Session`.
 * `streamCount` {number} The number of `Http2Stream` instances processed by
   the `Http2Session`.

From 0954785b2d20df585b5ba4dcba908089775f1d15 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Wed, 3 Jan 2018 13:34:45 -0800
Subject: [PATCH 32/77] http2: verify flood error and unsolicited frames

* verify protections against ping and settings flooding
* Strictly handle and verify handling of unsolicited ping and
  settings frame acks.

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17969
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 src/node_http2.cc                             | 40 ++++++++++++-
 test/common/http2.js                          | 10 ++++
 .../test-http2-ping-unsolicited-ack.js        | 43 ++++++++++++++
 .../test-http2-settings-unsolicited-ack.js    | 50 +++++++++++++++++
 test/sequential/sequential.status             |  2 +
 test/sequential/test-http2-ping-flood.js      | 56 +++++++++++++++++++
 test/sequential/test-http2-settings-flood.js  | 53 ++++++++++++++++++
 7 files changed, 252 insertions(+), 2 deletions(-)
 create mode 100644 test/parallel/test-http2-ping-unsolicited-ack.js
 create mode 100644 test/parallel/test-http2-settings-unsolicited-ack.js
 create mode 100644 test/sequential/test-http2-ping-flood.js
 create mode 100644 test/sequential/test-http2-settings-flood.js

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 2ead727b116b35..844421fa35c1c3 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -1306,8 +1306,24 @@ inline void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
   bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
   if (ack) {
     Http2Ping* ping = PopPing();
-    if (ping != nullptr)
+    if (ping != nullptr) {
       ping->Done(true, frame->ping.opaque_data);
+    } else {
+      // PING Ack is unsolicited. Treat as a connection error. The HTTP/2
+      // spec does not require this, but there is no legitimate reason to
+      // receive an unsolicited PING ack on a connection. Either the peer
+      // is buggy or malicious, and we're not going to tolerate such
+      // nonsense.
+      Isolate* isolate = env()->isolate();
+      HandleScope scope(isolate);
+      Local<Context> context = env()->context();
+      Context::Scope context_scope(context);
+
+      Local<Value> argv[1] = {
+        Integer::New(isolate, NGHTTP2_ERR_PROTO),
+      };
+      MakeCallback(env()->error_string(), arraysize(argv), argv);
+    }
   }
 }
 
@@ -1318,8 +1334,28 @@ inline void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
     // If this is an acknowledgement, we should have an Http2Settings
     // object for it.
     Http2Settings* settings = PopSettings();
-    if (settings != nullptr)
+    if (settings != nullptr) {
       settings->Done(true);
+    } else {
+      // SETTINGS Ack is unsolicited. Treat as a connection error. The HTTP/2
+      // spec does not require this, but there is no legitimate reason to
+      // receive an unsolicited SETTINGS ack on a connection. Either the peer
+      // is buggy or malicious, and we're not going to tolerate such
+      // nonsense.
+      // Note that nghttp2 currently prevents this from happening for SETTINGS
+      // frames, so this block is purely defensive just in case that behavior
+      // changes. Specifically, unlike unsolicited PING acks, unsolicited
+      // SETTINGS acks should *never* make it this far.
+      Isolate* isolate = env()->isolate();
+      HandleScope scope(isolate);
+      Local<Context> context = env()->context();
+      Context::Scope context_scope(context);
+
+      Local<Value> argv[1] = {
+        Integer::New(isolate, NGHTTP2_ERR_PROTO),
+      };
+      MakeCallback(env()->error_string(), arraysize(argv), argv);
+    }
   } else {
     // Otherwise, notify the session about a new settings
     MakeCallback(env()->onsettings_string(), 0, nullptr);
diff --git a/test/common/http2.js b/test/common/http2.js
index 44f5f9191e6e33..1d4c269fffd5b5 100644
--- a/test/common/http2.js
+++ b/test/common/http2.js
@@ -118,11 +118,21 @@ class HeadersFrame extends Frame {
   }
 }
 
+class PingFrame extends Frame {
+  constructor(ack = false) {
+    const buffers = [Buffer.alloc(8)];
+    super(8, 6, ack ? 1 : 0, 0);
+    buffers.unshift(this[kFrameData]);
+    this[kFrameData] = Buffer.concat(buffers);
+  }
+}
+
 module.exports = {
   Frame,
   DataFrame,
   HeadersFrame,
   SettingsFrame,
+  PingFrame,
   kFakeRequestHeaders,
   kFakeResponseHeaders,
   kClientMagic
diff --git a/test/parallel/test-http2-ping-unsolicited-ack.js b/test/parallel/test-http2-ping-unsolicited-ack.js
new file mode 100644
index 00000000000000..5a3a261cb098b1
--- /dev/null
+++ b/test/parallel/test-http2-ping-unsolicited-ack.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const http2 = require('http2');
+const net = require('net');
+const http2util = require('../common/http2');
+
+// Test that ping flooding causes the session to be torn down
+
+const kSettings = new http2util.SettingsFrame();
+const kPingAck = new http2util.PingFrame(true);
+
+const server = http2.createServer();
+
+server.on('stream', common.mustNotCall());
+server.on('session', common.mustCall((session) => {
+  session.on('error', common.expectsError({
+    code: 'ERR_HTTP2_ERROR',
+    message: 'Protocol error'
+  }));
+  session.on('close', common.mustCall(() => server.close()));
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = net.connect(server.address().port);
+
+  client.on('connect', common.mustCall(() => {
+    client.write(http2util.kClientMagic, () => {
+      client.write(kSettings.data);
+      // Send an unsolicited ping ack
+      client.write(kPingAck.data);
+    });
+  }));
+
+  // An error event may or may not be emitted, depending on operating system
+  // and timing. We do not really care if one is emitted here or not, as the
+  // error on the server side is what we are testing for. Do not make this
+  // a common.mustCall() and there's no need to check the error details.
+  client.on('error', () => {});
+}));
diff --git a/test/parallel/test-http2-settings-unsolicited-ack.js b/test/parallel/test-http2-settings-unsolicited-ack.js
new file mode 100644
index 00000000000000..fa63e9ee3f6425
--- /dev/null
+++ b/test/parallel/test-http2-settings-unsolicited-ack.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const http2 = require('http2');
+const net = require('net');
+const http2util = require('../common/http2');
+const Countdown = require('../common/countdown');
+
+// Test that an unsolicited settings ack is ignored.
+
+const kSettings = new http2util.SettingsFrame();
+const kSettingsAck = new http2util.SettingsFrame(true);
+
+const server = http2.createServer();
+let client;
+
+const countdown = new Countdown(3, () => {
+  client.destroy();
+  server.close();
+});
+
+server.on('stream', common.mustNotCall());
+server.on('session', common.mustCall((session) => {
+  session.on('remoteSettings', common.mustCall(() => countdown.dec()));
+}));
+
+server.listen(0, common.mustCall(() => {
+  client = net.connect(server.address().port);
+
+  // Ensures that the clients settings frames are not sent until the
+  // servers are received, so that the first ack is actually expected.
+  client.once('data', (chunk) => {
+    // The very first chunk of data we get from the server should
+    // be a settings frame.
+    assert.deepStrictEqual(chunk.slice(0, 9), kSettings.data);
+    // The first ack is expected.
+    client.write(kSettingsAck.data, () => countdown.dec());
+    // The second one is not and will be ignored.
+    client.write(kSettingsAck.data, () => countdown.dec());
+  });
+
+  client.on('connect', common.mustCall(() => {
+    client.write(http2util.kClientMagic);
+    client.write(kSettings.data);
+  }));
+}));
diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status
index 656eb80c4db4ee..5e39392e658a79 100644
--- a/test/sequential/sequential.status
+++ b/test/sequential/sequential.status
@@ -12,6 +12,8 @@ test-inspector-bindings               : PASS, FLAKY
 test-inspector-debug-end              : PASS, FLAKY
 test-inspector-async-hook-setup-at-signal:  PASS, FLAKY
 test-inspector-stop-profile-after-done: PASS, FLAKY
+test-http2-ping-flood                 : PASS, FLAKY
+test-http2-settings-flood             : PASS, FLAKY
 
 [$system==linux]
 
diff --git a/test/sequential/test-http2-ping-flood.js b/test/sequential/test-http2-ping-flood.js
new file mode 100644
index 00000000000000..5b47d51be9c5a8
--- /dev/null
+++ b/test/sequential/test-http2-ping-flood.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const http2 = require('http2');
+const net = require('net');
+const http2util = require('../common/http2');
+
+// Test that ping flooding causes the session to be torn down
+
+const kSettings = new http2util.SettingsFrame();
+const kPing = new http2util.PingFrame();
+
+const server = http2.createServer();
+
+server.on('stream', common.mustNotCall());
+server.on('session', common.mustCall((session) => {
+  session.on('error', common.expectsError({
+    code: 'ERR_HTTP2_ERROR',
+    message:
+      'Flooding was detected in this HTTP/2 session, and it must be closed'
+  }));
+  session.on('close', common.mustCall(() => {
+    server.close();
+  }));
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = net.connect(server.address().port);
+
+  // nghttp2 uses a limit of 10000 items in it's outbound queue.
+  // If this number is exceeded, a flooding error is raised. Set
+  // this lim higher to account for the ones that nghttp2 is
+  // successfully able to respond to.
+  // TODO(jasnell): Unfortunately, this test is inherently flaky because
+  // it is entirely dependent on how quickly the server is able to handle
+  // the inbound frames and whether those just happen to overflow nghttp2's
+  // outbound queue. The threshold at which the flood error occurs can vary
+  // from one system to another, and from one test run to another.
+  client.on('connect', common.mustCall(() => {
+    client.write(http2util.kClientMagic, () => {
+      client.write(kSettings.data, () => {
+        for (let n = 0; n < 35000; n++)
+          client.write(kPing.data);
+      });
+    });
+  }));
+
+  // An error event may or may not be emitted, depending on operating system
+  // and timing. We do not really care if one is emitted here or not, as the
+  // error on the server side is what we are testing for. Do not make this
+  // a common.mustCall() and there's no need to check the error details.
+  client.on('error', () => {});
+}));
diff --git a/test/sequential/test-http2-settings-flood.js b/test/sequential/test-http2-settings-flood.js
new file mode 100644
index 00000000000000..bad4cec9a8d509
--- /dev/null
+++ b/test/sequential/test-http2-settings-flood.js
@@ -0,0 +1,53 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const http2 = require('http2');
+const net = require('net');
+const http2util = require('../common/http2');
+
+// Test that settings flooding causes the session to be torn down
+
+const kSettings = new http2util.SettingsFrame();
+
+const server = http2.createServer();
+
+server.on('stream', common.mustNotCall());
+server.on('session', common.mustCall((session) => {
+  session.on('error', common.expectsError({
+    code: 'ERR_HTTP2_ERROR',
+    message:
+      'Flooding was detected in this HTTP/2 session, and it must be closed'
+  }));
+  session.on('close', common.mustCall(() => {
+    server.close();
+  }));
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = net.connect(server.address().port);
+
+  // nghttp2 uses a limit of 10000 items in it's outbound queue.
+  // If this number is exceeded, a flooding error is raised. Set
+  // this lim higher to account for the ones that nghttp2 is
+  // successfully able to respond to.
+  // TODO(jasnell): Unfortunately, this test is inherently flaky because
+  // it is entirely dependent on how quickly the server is able to handle
+  // the inbound frames and whether those just happen to overflow nghttp2's
+  // outbound queue. The threshold at which the flood error occurs can vary
+  // from one system to another, and from one test run to another.
+  client.on('connect', common.mustCall(() => {
+    client.write(http2util.kClientMagic, () => {
+      for (let n = 0; n < 35000; n++)
+        client.write(kSettings.data);
+    });
+  }));
+
+  // An error event may or may not be emitted, depending on operating system
+  // and timing. We do not really care if one is emitted here or not, as the
+  // error on the server side is what we are testing for. Do not make this
+  // a common.mustCall() and there's no need to check the error details.
+  client.on('error', () => {});
+}));

From 5fe065bbf9fa8b1d5febd11e0b322c8f9e6c8623 Mon Sep 17 00:00:00 2001
From: sreepurnajasti <sreepurna.jasti@gmail.com>
Date: Fri, 29 Dec 2017 10:25:31 +0530
Subject: [PATCH 33/77] doc: correct spelling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Backport-PR-URL: https://github.com/nodejs/node/pull/18050
PR-URL: https://github.com/nodejs/node/pull/17911
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Gibson Fahnestock <gibfahn@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
Reviewed-By: Weijia Wang <starkwang@126.com>
---
 doc/api/errors.md | 15 ++++++++++++---
 doc/api/http2.md  | 16 ++++++++--------
 2 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/doc/api/errors.md b/doc/api/errors.md
index 12650d55f6349d..a540de4c1bce5c 100755
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -770,7 +770,7 @@ An operation was performed on a stream that had already been destroyed.
 ### ERR_HTTP2_MAX_PENDING_SETTINGS_ACK
 
 Whenever an HTTP/2 `SETTINGS` frame is sent to a connected peer, the peer is
-required to send an acknowledgement that it has received and applied the new
+required to send an acknowledgment that it has received and applied the new
 `SETTINGS`. By default, a maximum number of unacknowledged `SETTINGS` frames may
 be sent at any given time. This error code is used when that limit has been
 reached.
@@ -796,7 +796,7 @@ forbidden.
 <a id="ERR_HTTP2_PING_CANCEL"></a>
 ### ERR_HTTP2_PING_CANCEL
 
-An HTTP/2 ping was cancelled.
+An HTTP/2 ping was canceled.
 
 <a id="ERR_HTTP2_PING_LENGTH"></a>
 ### ERR_HTTP2_PING_LENGTH
@@ -1028,7 +1028,16 @@ Used when an [ES6 module][] cannot be resolved.
 
 > Stability: 1 - Experimental
 
-Used when a failure occurs resolving imports in an [ES6 module][].
+Used when a failure occurred resolving imports in an [ES6 module][].
+
+<a id="ERR_MULTIPLE_CALLBACK"></a>
+### ERR_MULTIPLE_CALLBACK
+
+A callback was called more than once.
+
+*Note*: A callback is almost always meant to only be called once as the query
+can either be fulfilled or rejected but not both at the same time. The latter
+would be possible by calling a callback more than once.
 
 <a id="ERR_NAPI_CONS_FUNCTION"></a>
 ### ERR_NAPI_CONS_FUNCTION
diff --git a/doc/api/http2.md b/doc/api/http2.md
index b0b692c492fe64..8b3e33723b4c7d 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -191,7 +191,7 @@ the handler function will receive three arguments:
 added: v8.4.0
 -->
 
-The `'localSettings'` event is emitted when an acknowledgement SETTINGS frame
+The `'localSettings'` event is emitted when an acknowledgment SETTINGS frame
 has been received. When invoked, the handler function will receive a copy of
 the local settings.
 
@@ -339,7 +339,7 @@ Once destroyed, the `Http2Session` will emit the `'close'` event. If `error`
 is not undefined, an `'error'` event will be emitted immediately after the
 `'close'` event.
 
-If there are any remaining open `Http2Streams` associatd with the
+If there are any remaining open `Http2Streams` associated with the
 `Http2Session`, those will also be destroyed.
 
 #### http2session.destroyed
@@ -406,7 +406,7 @@ added: v8.4.0
 * Value: {boolean}
 
 Indicates whether or not the `Http2Session` is currently waiting for an
-acknowledgement for a sent SETTINGS frame. Will be `true` after calling the
+acknowledgment for a sent SETTINGS frame. Will be `true` after calling the
 `http2session.settings()` method. Will be `false` once all sent SETTINGS
 frames have been acknowledged.
 
@@ -428,12 +428,12 @@ The maximum number of outstanding (unacknowledged) pings is determined by the
 
 If provided, the `payload` must be a `Buffer`, `TypedArray`, or `DataView`
 containing 8 bytes of data that will be transmitted with the `PING` and
-returned with the ping acknowledgement.
+returned with the ping acknowledgment.
 
 The callback will be invoked with three arguments: an error argument that will
 be `null` if the `PING` was successfully acknowledged, a `duration` argument
 that reports the number of milliseconds elapsed since the ping was sent and the
-acknowledgement was received, and a `Buffer` containing the 8-byte `PING`
+acknowledgment was received, and a `Buffer` containing the 8-byte `PING`
 payload.
 
 ```js
@@ -581,8 +581,8 @@ while the session is waiting for the remote peer to acknowledge the new
 settings.
 
 *Note*: The new settings will not become effective until the SETTINGS
-acknowledgement is received and the `'localSettings'` event is emitted. It
-is possible to send multiple SETTINGS frames while acknowledgement is still
+acknowledgment is received and the `'localSettings'` event is emitted. It
+is possible to send multiple SETTINGS frames while acknowledgment is still
 pending.
 
 #### http2session.type
@@ -885,7 +885,7 @@ added: v8.4.0
 -->
 
 The `'timeout'` event is emitted after no activity is received for this
-`'Http2Stream'` within the number of millseconds set using
+`'Http2Stream'` within the number of milliseconds set using
 `http2stream.setTimeout()`.
 
 #### Event: 'trailers'

From ea211576e67683ef9c43d76d2a231ca9cd84761a Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Thu, 4 Jan 2018 02:18:31 +0200
Subject: [PATCH 34/77] doc: fix code nits in common/README

1. Sync comments and code
2. Fix typos

PR-URL: https://github.com/nodejs/node/pull/17971
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Khaidi Chu <i@2333.moe>
Reviewed-By: Jon Moss <me@jonathanmoss.me>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 test/common/README.md | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/test/common/README.md b/test/common/README.md
index e4bf2887ca3a71..e7ab9dec0bada0 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -610,7 +610,7 @@ frame.
 // padlen is an 8-bit integer giving the number of padding bytes to include
 // final is a boolean indicating whether the End-of-stream flag should be set,
 // defaults to false.
-const data = new http2.DataFrame(id, payload, padlen, final);
+const frame = new http2.DataFrame(id, payload, padlen, final);
 
 socket.write(frame.data);
 ```
@@ -631,8 +631,7 @@ The `http2.HeadersFrame` is a subclass of `http2.Frame` that serializes a
 // padlen is an 8-bit integer giving the number of padding bytes to include
 // final is a boolean indicating whether the End-of-stream flag should be set,
 // defaults to false.
-const data = new http2.HeadersFrame(id, http2.kFakeRequestHeaders,
-                                    padlen, final);
+const frame = new http2.HeadersFrame(id, payload, padlen, final);
 
 socket.write(frame.data);
 ```

From b768f6fcb4079518b06039a4e16ef979e63e777a Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Thu, 4 Jan 2018 02:37:39 +0200
Subject: [PATCH 35/77] doc: re-alphabetise sections in common/README.md

PR-URL: https://github.com/nodejs/node/pull/17971
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Khaidi Chu <i@2333.moe>
Reviewed-By: Jon Moss <me@jonathanmoss.me>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 test/common/README.md | 101 ++++++++++++++++++------------------------
 1 file changed, 44 insertions(+), 57 deletions(-)

diff --git a/test/common/README.md b/test/common/README.md
index e7ab9dec0bada0..17ded3526bdf7f 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -10,10 +10,10 @@ This directory contains modules used to test the Node.js implementation.
 * [DNS module](#dns-module)
 * [Duplex pair helper](#duplex-pair-helper)
 * [Fixtures module](#fixtures-module)
+* [HTTP2 module](#http2-module)
 * [Internet module](#internet-module)
 * [tmpdir module](#tmpdir-module)
 * [WPT module](#wpt-module)
-* [HTTP2 module](#http2-module)
 
 ## Benchmark Module
 
@@ -504,62 +504,6 @@ Returns the result of
 Returns the result of
 `fs.readFileSync(path.join(fixtures.fixturesDir, 'keys', arg), 'enc')`.
 
-## Internet Module
-
-The `common/internet` module provides utilities for working with
-internet-related tests.
-
-### internet.addresses
-
-* [&lt;Object>]
-  * `INET_HOST` [&lt;String>] A generic host that has registered common
-    DNS records, supports both IPv4 and IPv6, and provides basic HTTP/HTTPS
-    services
-  * `INET4_HOST` [&lt;String>] A host that provides IPv4 services
-  * `INET6_HOST` [&lt;String>] A host that provides IPv6 services
-  * `INET4_IP` [&lt;String>] An accessible IPv4 IP, defaults to the
-    Google Public DNS IPv4 address
-  * `INET6_IP` [&lt;String>] An accessible IPv6 IP, defaults to the
-    Google Public DNS IPv6 address
-  * `INVALID_HOST` [&lt;String>] An invalid host that cannot be resolved
-  * `MX_HOST` [&lt;String>] A host with MX records registered
-  * `SRV_HOST` [&lt;String>] A host with SRV records registered
-  * `PTR_HOST` [&lt;String>] A host with PTR records registered
-  * `NAPTR_HOST` [&lt;String>] A host with NAPTR records registered
-  * `SOA_HOST` [&lt;String>] A host with SOA records registered
-  * `CNAME_HOST` [&lt;String>] A host with CNAME records registered
-  * `NS_HOST` [&lt;String>] A host with NS records registered
-  * `TXT_HOST` [&lt;String>] A host with TXT records registered
-  * `DNS4_SERVER` [&lt;String>] An accessible IPv4 DNS server
-  * `DNS6_SERVER` [&lt;String>] An accessible IPv6 DNS server
-
-A set of addresses for internet-related tests. All properties are configurable
-via `NODE_TEST_*` environment variables. For example, to configure
-`internet.addresses.INET_HOST`, set the environment
-vairable `NODE_TEST_INET_HOST` to a specified host.
-
-## tmpdir Module
-
-The `tmpdir` module supports the use of a temporary directory for testing.
-
-### path
-* [&lt;String>]
-
-The realpath of the testing temporary directory.
-
-### refresh()
-
-Deletes and recreates the testing temporary directory.
-
-## WPT Module
-
-The wpt.js module is a port of parts of
-[W3C testharness.js](https://github.com/w3c/testharness.js) for testing the
-Node.js
-[WHATWG URL API](https://nodejs.org/api/url.html#url_the_whatwg_url_api)
-implementation with tests from
-[W3C Web Platform Tests](https://github.com/w3c/web-platform-tests).
-
 ## HTTP/2 Module
 
 The http2.js module provides a handful of utilities for creating mock HTTP/2
@@ -695,6 +639,49 @@ upon initial establishment of a connection.
 socket.write(http2.kClientMagic);
 ```
 
+## Internet Module
+
+The `common/internet` module provides utilities for working with
+internet-related tests.
+
+### internet.addresses
+
+* [&lt;Object>]
+  * `INET_HOST` [&lt;String>] A generic host that has registered common
+    DNS records, supports both IPv4 and IPv6, and provides basic HTTP/HTTPS
+    services
+  * `INET4_HOST` [&lt;String>] A host that provides IPv4 services
+  * `INET6_HOST` [&lt;String>] A host that provides IPv6 services
+  * `INET4_IP` [&lt;String>] An accessible IPv4 IP, defaults to the
+    Google Public DNS IPv4 address
+  * `INET6_IP` [&lt;String>] An accessible IPv6 IP, defaults to the
+    Google Public DNS IPv6 address
+  * `INVALID_HOST` [&lt;String>] An invalid host that cannot be resolved
+  * `MX_HOST` [&lt;String>] A host with MX records registered
+  * `SRV_HOST` [&lt;String>] A host with SRV records registered
+  * `PTR_HOST` [&lt;String>] A host with PTR records registered
+  * `NAPTR_HOST` [&lt;String>] A host with NAPTR records registered
+  * `SOA_HOST` [&lt;String>] A host with SOA records registered
+  * `CNAME_HOST` [&lt;String>] A host with CNAME records registered
+  * `NS_HOST` [&lt;String>] A host with NS records registered
+  * `TXT_HOST` [&lt;String>] A host with TXT records registered
+  * `DNS4_SERVER` [&lt;String>] An accessible IPv4 DNS server
+  * `DNS6_SERVER` [&lt;String>] An accessible IPv6 DNS server
+
+A set of addresses for internet-related tests. All properties are configurable
+via `NODE_TEST_*` environment variables. For example, to configure
+`internet.addresses.INET_HOST`, set the environment
+vairable `NODE_TEST_INET_HOST` to a specified host.
+
+## WPT Module
+
+The wpt.js module is a port of parts of
+[W3C testharness.js](https://github.com/w3c/testharness.js) for testing the
+Node.js
+[WHATWG URL API](https://nodejs.org/api/url.html#url_the_whatwg_url_api)
+implementation with tests from
+[W3C Web Platform Tests](https://github.com/w3c/web-platform-tests).
+
 
 [&lt;Array>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
 [&lt;ArrayBufferView&#91;&#93;>]: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView

From 4eb9cca056f3e014a5df249a580a1a51c4eb9a66 Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Thu, 4 Jan 2018 02:08:49 +0200
Subject: [PATCH 36/77] doc: compact eslint directives in common/README

PR-URL: https://github.com/nodejs/node/pull/17971
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Khaidi Chu <i@2333.moe>
Reviewed-By: Jon Moss <me@jonathanmoss.me>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 test/common/README.md | 40 ++++++++--------------------------------
 1 file changed, 8 insertions(+), 32 deletions(-)

diff --git a/test/common/README.md b/test/common/README.md
index 17ded3526bdf7f..fad4467775ac32 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -509,10 +509,7 @@ Returns the result of
 The http2.js module provides a handful of utilities for creating mock HTTP/2
 frames for testing of HTTP/2 endpoints
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 const http2 = require('../common/http2');
 ```
@@ -522,10 +519,7 @@ const http2 = require('../common/http2');
 The `http2.Frame` is a base class that creates a `Buffer` containing a
 serialized HTTP/2 frame header.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 // length is a 24-bit unsigned integer
 // type is an 8-bit unsigned integer identifying the frame type
@@ -544,10 +538,7 @@ The serialized `Buffer` may be retrieved using the `frame.data` property.
 The `http2.DataFrame` is a subclass of `http2.Frame` that serializes a `DATA`
 frame.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 // id is the 32-bit stream identifier
 // payload is a Buffer containing the DATA payload
@@ -564,10 +555,7 @@ socket.write(frame.data);
 The `http2.HeadersFrame` is a subclass of `http2.Frame` that serializes a
 `HEADERS` frame.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 // id is the 32-bit stream identifier
 // payload is a Buffer containing the HEADERS payload (see either
@@ -585,10 +573,7 @@ socket.write(frame.data);
 The `http2.SettingsFrame` is a subclass of `http2.Frame` that serializes an
 empty `SETTINGS` frame.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 // ack is a boolean indicating whether or not to set the ACK flag.
 const frame = new http2.SettingsFrame(ack);
@@ -601,10 +586,7 @@ socket.write(frame.data);
 Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2
 request headers to be used as the payload of a `http2.HeadersFrame`.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 const frame = new http2.HeadersFrame(1, http2.kFakeRequestHeaders, 0, true);
 
@@ -616,10 +598,7 @@ socket.write(frame.data);
 Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2
 response headers to be used as the payload a `http2.HeadersFrame`.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 const frame = new http2.HeadersFrame(1, http2.kFakeResponseHeaders, 0, true);
 
@@ -631,10 +610,7 @@ socket.write(frame.data);
 Set to a `Buffer` containing the preamble bytes an HTTP/2 client must send
 upon initial establishment of a connection.
 
-<!-- eslint-disable strict -->
-<!-- eslint-disable required-modules -->
-<!-- eslint-disable no-unused-vars -->
-<!-- eslint-disable no-undef -->
+<!-- eslint-disable no-undef, no-unused-vars, required-modules, strict -->
 ```js
 socket.write(http2.kClientMagic);
 ```

From 39a0d35df96f6aa385360f25c6f34e4eb7d39527 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Sat, 6 Jan 2018 11:50:58 -0800
Subject: [PATCH 37/77] http2: use aliased buffer for perf stats, add stats

Add an aliased buffer for session and stream statistics,
add a few more metrics

PR-URL: https://github.com/nodejs/node/pull/18020
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
---
 doc/api/http2.md                       |  20 ++++-
 lib/perf_hooks.js                      |  68 ++++++++++++++
 src/node_http2.cc                      | 120 ++++++++++++++-----------
 src/node_http2.h                       |  50 +++++++++--
 src/node_http2_state.h                 |  37 ++++++++
 test/parallel/test-http2-perf_hooks.js |  11 ++-
 6 files changed, 240 insertions(+), 66 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 8b3e33723b4c7d..2706f4cff3f5e2 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -3022,23 +3022,35 @@ The `name` property of the `PerformanceEntry` will be equal to either
 If `name` is equal to `Http2Stream`, the `PerformanceEntry` will contain the
 following additional properties:
 
+* `bytesRead` {number} The number of DATA frame bytes received for this
+  `Http2Stream`.
+* `bytesWritten` {number} The number of DATA frame bytes sent for this
+  `Http2Stream`.
+* `id` {number} The identifier of the associated `Http2Stream`
 * `timeToFirstByte` {number} The number of milliseconds elapsed between the
   `PerformanceEntry` `startTime` and the reception of the first `DATA` frame.
+* `timeToFirstByteSent` {number} The number of milliseconds elapsed between
+  the `PerformanceEntry` `startTime` and sending of the first `DATA` frame.
 * `timeToFirstHeader` {number} The number of milliseconds elapsed between the
   `PerformanceEntry` `startTime` and the reception of the first header.
 
 If `name` is equal to `Http2Session`, the `PerformanceEntry` will contain the
 following additional properties:
 
+* `bytesRead` {number} The number of bytes received for this `Http2Session`.
+* `bytesWritten` {number} The number of bytes sent for this `Http2Session`.
+* `framesReceived` {number} The number of HTTP/2 frames received by the
+  `Http2Session`.
+* `framesSent` {number} The number of HTTP/2 frames sent by the `Http2Session`.
+* `maxConcurrentStreams` {number} The maximum number of streams concurrently
+  open during the lifetime of the `Http2Session`.
 * `pingRTT` {number} The number of milliseconds elapsed since the transmission
   of a `PING` frame and the reception of its acknowledgment. Only present if
   a `PING` frame has been sent on the `Http2Session`.
-* `streamCount` {number} The number of `Http2Stream` instances processed by
-  the `Http2Session`.
 * `streamAverageDuration` {number} The average duration (in milliseconds) for
   all `Http2Stream` instances.
-* `framesReceived` {number} The number of HTTP/2 frames received by the
-  `Http2Session`.
+* `streamCount` {number} The number of `Http2Stream` instances processed by
+  the `Http2Session`.
 * `type` {string} Either `'server'` or `'client'` to identify the type of
   `Http2Session`.
 
diff --git a/lib/perf_hooks.js b/lib/perf_hooks.js
index 7e1d085b525b44..4a05f7ccba7bc8 100644
--- a/lib/perf_hooks.js
+++ b/lib/perf_hooks.js
@@ -66,6 +66,70 @@ const observerableTypes = [
   'http2'
 ];
 
+const IDX_STREAM_STATS_ID = 0;
+const IDX_STREAM_STATS_TIMETOFIRSTBYTE = 1;
+const IDX_STREAM_STATS_TIMETOFIRSTHEADER = 2;
+const IDX_STREAM_STATS_TIMETOFIRSTBYTESENT = 3;
+const IDX_STREAM_STATS_SENTBYTES = 4;
+const IDX_STREAM_STATS_RECEIVEDBYTES = 5;
+
+const IDX_SESSION_STATS_TYPE = 0;
+const IDX_SESSION_STATS_PINGRTT = 1;
+const IDX_SESSION_STATS_FRAMESRECEIVED = 2;
+const IDX_SESSION_STATS_FRAMESSENT = 3;
+const IDX_SESSION_STATS_STREAMCOUNT = 4;
+const IDX_SESSION_STATS_STREAMAVERAGEDURATION = 5;
+const IDX_SESSION_STATS_DATA_SENT = 6;
+const IDX_SESSION_STATS_DATA_RECEIVED = 7;
+const IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS = 8;
+
+let sessionStats;
+let streamStats;
+
+function collectHttp2Stats(entry) {
+  switch (entry.name) {
+    case 'Http2Stream':
+      if (streamStats === undefined)
+        streamStats = process.binding('http2').streamStats;
+      entry.id =
+        streamStats[IDX_STREAM_STATS_ID] >>> 0;
+      entry.timeToFirstByte =
+        streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTE];
+      entry.timeToFirstHeader =
+        streamStats[IDX_STREAM_STATS_TIMETOFIRSTHEADER];
+      entry.timeToFirstByteSent =
+        streamStats[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT];
+      entry.bytesWritten =
+        streamStats[IDX_STREAM_STATS_SENTBYTES];
+      entry.bytesRead =
+        streamStats[IDX_STREAM_STATS_RECEIVEDBYTES];
+      break;
+    case 'Http2Session':
+      if (sessionStats === undefined)
+        sessionStats = process.binding('http2').sessionStats;
+      entry.type =
+        sessionStats[IDX_SESSION_STATS_TYPE] >>> 0 === 0 ? 'server' : 'client';
+      entry.pingRTT =
+        sessionStats[IDX_SESSION_STATS_PINGRTT];
+      entry.framesReceived =
+        sessionStats[IDX_SESSION_STATS_FRAMESRECEIVED];
+      entry.framesSent =
+        sessionStats[IDX_SESSION_STATS_FRAMESSENT];
+      entry.streamCount =
+        sessionStats[IDX_SESSION_STATS_STREAMCOUNT];
+      entry.streamAverageDuration =
+        sessionStats[IDX_SESSION_STATS_STREAMAVERAGEDURATION];
+      entry.bytesWritten =
+        sessionStats[IDX_SESSION_STATS_DATA_SENT];
+      entry.bytesRead =
+        sessionStats[IDX_SESSION_STATS_DATA_RECEIVED];
+      entry.maxConcurrentStreams =
+        sessionStats[IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS];
+      break;
+  }
+}
+
+
 let errors;
 function lazyErrors() {
   if (errors === undefined)
@@ -467,6 +531,10 @@ function doNotify() {
 // Set up the callback used to receive PerformanceObserver notifications
 function observersCallback(entry) {
   const type = mapTypes(entry.entryType);
+
+  if (type === NODE_PERFORMANCE_ENTRY_TYPE_HTTP2)
+    collectHttp2Stats(entry);
+
   performance[kInsertEntry](entry);
   const list = getObserversList(type);
 
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 844421fa35c1c3..2bcf7ccfa0554e 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -471,6 +471,8 @@ Http2Session::Callbacks::Callbacks(bool kHasGetPaddingCallback) {
     callbacks, OnSendData);
   nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(
     callbacks, OnInvalidFrame);
+  nghttp2_session_callbacks_set_on_frame_send_callback(
+    callbacks, OnFrameSent);
 
   if (kHasGetPaddingCallback) {
     nghttp2_session_callbacks_set_select_padding_callback(
@@ -560,28 +562,35 @@ inline void Http2Stream::EmitStatistics() {
   if (!HasHttp2Observer(env()))
     return;
   Http2StreamPerformanceEntry* entry =
-    new Http2StreamPerformanceEntry(env(), statistics_);
+    new Http2StreamPerformanceEntry(env(), id_, statistics_);
   env()->SetImmediate([](Environment* env, void* data) {
-    Local<Context> context = env->context();
     Http2StreamPerformanceEntry* entry =
       static_cast<Http2StreamPerformanceEntry*>(data);
     if (HasHttp2Observer(env)) {
-      Local<Object> obj = entry->ToObject();
-      v8::PropertyAttribute attr =
-          static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
-      obj->DefineOwnProperty(
-          context,
-          FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstByte"),
-          Number::New(env->isolate(),
-                      (entry->first_byte() - entry->startTimeNano()) / 1e6),
-          attr).FromJust();
-      obj->DefineOwnProperty(
-          context,
-          FIXED_ONE_BYTE_STRING(env->isolate(), "timeToFirstHeader"),
-          Number::New(env->isolate(),
-                      (entry->first_header() - entry->startTimeNano()) / 1e6),
-          attr).FromJust();
-      entry->Notify(obj);
+      AliasedBuffer<double, v8::Float64Array>& buffer =
+          env->http2_state()->stream_stats_buffer;
+      buffer[IDX_STREAM_STATS_ID] = entry->id();
+      if (entry->first_byte() != 0) {
+        buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTE] =
+            (entry->first_byte() - entry->startTimeNano()) / 1e6;
+      } else {
+        buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTE] = 0;
+      }
+      if (entry->first_header() != 0) {
+        buffer[IDX_STREAM_STATS_TIMETOFIRSTHEADER] =
+            (entry->first_header() - entry->startTimeNano()) / 1e6;
+      } else {
+        buffer[IDX_STREAM_STATS_TIMETOFIRSTHEADER] = 0;
+      }
+      if (entry->first_byte_sent() != 0) {
+        buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT] =
+            (entry->first_byte_sent() - entry->startTimeNano()) / 1e6;
+      } else {
+        buffer[IDX_STREAM_STATS_TIMETOFIRSTBYTESENT] = 0;
+      }
+      buffer[IDX_STREAM_STATS_SENTBYTES] = entry->sent_bytes();
+      buffer[IDX_STREAM_STATS_RECEIVEDBYTES] = entry->received_bytes();
+      entry->Notify(entry->ToObject());
     }
     delete entry;
   }, static_cast<void*>(entry));
@@ -591,45 +600,25 @@ inline void Http2Session::EmitStatistics() {
   if (!HasHttp2Observer(env()))
     return;
   Http2SessionPerformanceEntry* entry =
-    new Http2SessionPerformanceEntry(env(), statistics_, TypeName());
+    new Http2SessionPerformanceEntry(env(), statistics_, session_type_);
   env()->SetImmediate([](Environment* env, void* data) {
-    Local<Context> context = env->context();
     Http2SessionPerformanceEntry* entry =
       static_cast<Http2SessionPerformanceEntry*>(data);
     if (HasHttp2Observer(env)) {
-      Local<Object> obj = entry->ToObject();
-      v8::PropertyAttribute attr =
-          static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete);
-      obj->DefineOwnProperty(
-          context,
-          FIXED_ONE_BYTE_STRING(env->isolate(), "type"),
-          String::NewFromUtf8(env->isolate(),
-                              entry->typeName(),
-                              v8::NewStringType::kInternalized)
-                                  .ToLocalChecked(), attr).FromJust();
-      if (entry->ping_rtt() != 0) {
-        obj->DefineOwnProperty(
-            context,
-            FIXED_ONE_BYTE_STRING(env->isolate(), "pingRTT"),
-            Number::New(env->isolate(), entry->ping_rtt() / 1e6),
-            attr).FromJust();
-      }
-      obj->DefineOwnProperty(
-          context,
-          FIXED_ONE_BYTE_STRING(env->isolate(), "framesReceived"),
-          Integer::NewFromUnsigned(env->isolate(), entry->frame_count()),
-          attr).FromJust();
-      obj->DefineOwnProperty(
-          context,
-          FIXED_ONE_BYTE_STRING(env->isolate(), "streamCount"),
-          Integer::New(env->isolate(), entry->stream_count()),
-          attr).FromJust();
-      obj->DefineOwnProperty(
-          context,
-          FIXED_ONE_BYTE_STRING(env->isolate(), "streamAverageDuration"),
-          Number::New(env->isolate(), entry->stream_average_duration()),
-          attr).FromJust();
-      entry->Notify(obj);
+      AliasedBuffer<double, v8::Float64Array>& buffer =
+          env->http2_state()->session_stats_buffer;
+      buffer[IDX_SESSION_STATS_TYPE] = entry->type();
+      buffer[IDX_SESSION_STATS_PINGRTT] = entry->ping_rtt() / 1e6;
+      buffer[IDX_SESSION_STATS_FRAMESRECEIVED] = entry->frame_count();
+      buffer[IDX_SESSION_STATS_FRAMESSENT] = entry->frame_sent();
+      buffer[IDX_SESSION_STATS_STREAMCOUNT] = entry->stream_count();
+      buffer[IDX_SESSION_STATS_STREAMAVERAGEDURATION] =
+          entry->stream_average_duration();
+      buffer[IDX_SESSION_STATS_DATA_SENT] = entry->data_sent();
+      buffer[IDX_SESSION_STATS_DATA_RECEIVED] = entry->data_received();
+      buffer[IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS] =
+          entry->max_concurrent_streams();
+      entry->Notify(entry->ToObject());
     }
     delete entry;
   }, static_cast<void*>(entry));
@@ -695,6 +684,9 @@ inline bool Http2Session::CanAddStream() {
 inline void Http2Session::AddStream(Http2Stream* stream) {
   CHECK_GE(++statistics_.stream_count, 0);
   streams_[stream->id()] = stream;
+  size_t size = streams_.size();
+  if (size > statistics_.max_concurrent_streams)
+    statistics_.max_concurrent_streams = size;
   IncrementCurrentSessionMemory(stream->self_size());
 }
 
@@ -963,6 +955,14 @@ inline int Http2Session::OnFrameNotSent(nghttp2_session* handle,
   return 0;
 }
 
+inline int Http2Session::OnFrameSent(nghttp2_session* handle,
+                                     const nghttp2_frame* frame,
+                                     void* user_data) {
+  Http2Session* session = static_cast<Http2Session*>(user_data);
+  session->statistics_.frame_sent += 1;
+  return 0;
+}
+
 // Called by nghttp2 when a stream closes.
 inline int Http2Session::OnStreamClose(nghttp2_session* handle,
                                        int32_t id,
@@ -1040,6 +1040,7 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
     // If the stream has been destroyed, ignore this chunk
     if (stream->IsDestroyed())
       return 0;
+    stream->statistics_.received_bytes += len;
     stream->AddChunk(data, len);
   }
   return 0;
@@ -1494,6 +1495,7 @@ void Http2Session::SendPendingData() {
   size_t offset = 0;
   size_t i = 0;
   for (const nghttp2_stream_write& write : outgoing_buffers_) {
+    statistics_.data_sent += write.buf.len;
     if (write.buf.base == nullptr) {
       bufs[i++] = uv_buf_init(
           reinterpret_cast<char*>(outgoing_storage_.data() + offset),
@@ -1643,6 +1645,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
   if (bufs->len > 0) {
     // Only pass data on if nread > 0
     uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) };
+    session->statistics_.data_received += nread;
     ssize_t ret = session->Write(buf, 1);
 
     // Note: if ssize_t is not defined (e.g. on Win32), nghttp2 will typedef
@@ -2142,6 +2145,8 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
                                           void* user_data) {
   Http2Session* session = static_cast<Http2Session*>(user_data);
   Http2Stream* stream = session->FindStream(id);
+  if (stream->statistics_.first_byte_sent == 0)
+    stream->statistics_.first_byte_sent = uv_hrtime();
 
   DEBUG_HTTP2SESSION2(session, "reading outbound file data for stream %d", id);
   CHECK_EQ(id, stream->id());
@@ -2192,6 +2197,7 @@ ssize_t Http2Stream::Provider::FD::OnRead(nghttp2_session* handle,
       return NGHTTP2_ERR_CALLBACK_FAILURE;
   }
 
+  stream->statistics_.sent_bytes += numchars;
   return numchars;
 }
 
@@ -2217,6 +2223,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
   Http2Session* session = static_cast<Http2Session*>(user_data);
   DEBUG_HTTP2SESSION2(session, "reading outbound data for stream %d", id);
   Http2Stream* stream = GetStream(session, id, source);
+  if (stream->statistics_.first_byte_sent == 0)
+    stream->statistics_.first_byte_sent = uv_hrtime();
   CHECK_EQ(id, stream->id());
 
   size_t amount = 0;          // amount of data being sent in this data frame.
@@ -2250,6 +2258,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
     if (session->IsDestroyed())
       return NGHTTP2_ERR_CALLBACK_FAILURE;
   }
+
+  stream->statistics_.sent_bytes += amount;
   return amount;
 }
 
@@ -2863,6 +2873,10 @@ void Initialize(Local<Object> target,
     "settingsBuffer", state->settings_buffer.GetJSArray());
   SET_STATE_TYPEDARRAY(
     "optionsBuffer", state->options_buffer.GetJSArray());
+  SET_STATE_TYPEDARRAY(
+    "streamStats", state->stream_stats_buffer.GetJSArray());
+  SET_STATE_TYPEDARRAY(
+    "sessionStats", state->session_stats_buffer.GetJSArray());
 #undef SET_STATE_TYPEDARRAY
 
   env->set_http2_state(std::move(state));
diff --git a/src/node_http2.h b/src/node_http2.h
index 5b5f7e5f52e084..a7938a5f800854 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -715,8 +715,11 @@ class Http2Stream : public AsyncWrap,
   struct Statistics {
     uint64_t start_time;
     uint64_t end_time;
-    uint64_t first_header;  // Time first header was received
-    uint64_t first_byte;    // Time first data frame byte was received
+    uint64_t first_header;     // Time first header was received
+    uint64_t first_byte;       // Time first DATA frame byte was received
+    uint64_t first_byte_sent;  // Time first DATA frame byte was sent
+    uint64_t sent_bytes;
+    uint64_t received_bytes;
   };
 
   Statistics statistics_ = {};
@@ -949,8 +952,12 @@ class Http2Session : public AsyncWrap {
     uint64_t start_time;
     uint64_t end_time;
     uint64_t ping_rtt;
+    uint64_t data_sent;
+    uint64_t data_received;
     uint32_t frame_count;
+    uint32_t frame_sent;
     int32_t stream_count;
+    size_t max_concurrent_streams;
     double stream_average_duration;
   };
 
@@ -995,6 +1002,10 @@ class Http2Session : public AsyncWrap {
       const nghttp2_frame* frame,
       int error_code,
       void* user_data);
+  static inline int OnFrameSent(
+      nghttp2_session* session,
+      const nghttp2_frame* frame,
+      void* user_data);
   static inline int OnStreamClose(
       nghttp2_session* session,
       int32_t id,
@@ -1115,21 +1126,29 @@ class Http2SessionPerformanceEntry : public PerformanceEntry {
   Http2SessionPerformanceEntry(
       Environment* env,
       const Http2Session::Statistics& stats,
-      const char* kind) :
+      nghttp2_session_type type) :
           PerformanceEntry(env, "Http2Session", "http2",
                            stats.start_time,
                            stats.end_time),
           ping_rtt_(stats.ping_rtt),
+          data_sent_(stats.data_sent),
+          data_received_(stats.data_received),
           frame_count_(stats.frame_count),
+          frame_sent_(stats.frame_sent),
           stream_count_(stats.stream_count),
+          max_concurrent_streams_(stats.max_concurrent_streams),
           stream_average_duration_(stats.stream_average_duration),
-          kind_(kind) { }
+          session_type_(type) { }
 
   uint64_t ping_rtt() const { return ping_rtt_; }
+  uint64_t data_sent() const { return data_sent_; }
+  uint64_t data_received() const { return data_received_; }
   uint32_t frame_count() const { return frame_count_; }
+  uint32_t frame_sent() const { return frame_sent_; }
   int32_t stream_count() const { return stream_count_; }
+  size_t max_concurrent_streams() const { return max_concurrent_streams_; }
   double stream_average_duration() const { return stream_average_duration_; }
-  const char* typeName() const { return kind_; }
+  nghttp2_session_type type() const { return session_type_; }
 
   void Notify(Local<Value> obj) {
     PerformanceEntry::Notify(env(), kind(), obj);
@@ -1137,33 +1156,50 @@ class Http2SessionPerformanceEntry : public PerformanceEntry {
 
  private:
   uint64_t ping_rtt_;
+  uint64_t data_sent_;
+  uint64_t data_received_;
   uint32_t frame_count_;
+  uint32_t frame_sent_;
   int32_t stream_count_;
+  size_t max_concurrent_streams_;
   double stream_average_duration_;
-  const char* kind_;
+  nghttp2_session_type session_type_;
 };
 
 class Http2StreamPerformanceEntry : public PerformanceEntry {
  public:
   Http2StreamPerformanceEntry(
       Environment* env,
+      int32_t id,
       const Http2Stream::Statistics& stats) :
           PerformanceEntry(env, "Http2Stream", "http2",
                            stats.start_time,
                            stats.end_time),
+          id_(id),
           first_header_(stats.first_header),
-          first_byte_(stats.first_byte) { }
+          first_byte_(stats.first_byte),
+          first_byte_sent_(stats.first_byte_sent),
+          sent_bytes_(stats.sent_bytes),
+          received_bytes_(stats.received_bytes) { }
 
+  int32_t id() const { return id_; }
   uint64_t first_header() const { return first_header_; }
   uint64_t first_byte() const { return first_byte_; }
+  uint64_t first_byte_sent() const { return first_byte_sent_; }
+  uint64_t sent_bytes() const { return sent_bytes_; }
+  uint64_t received_bytes() const { return received_bytes_; }
 
   void Notify(Local<Value> obj) {
     PerformanceEntry::Notify(env(), kind(), obj);
   }
 
  private:
+  int32_t id_;
   uint64_t first_header_;
   uint64_t first_byte_;
+  uint64_t first_byte_sent_;
+  uint64_t sent_bytes_;
+  uint64_t received_bytes_;
 };
 
 class Http2Session::Http2Ping : public AsyncWrap {
diff --git a/src/node_http2_state.h b/src/node_http2_state.h
index af0740c994e765..ed88f068a04b16 100644
--- a/src/node_http2_state.h
+++ b/src/node_http2_state.h
@@ -61,6 +61,29 @@ namespace http2 {
     PADDING_BUF_FIELD_COUNT
   };
 
+  enum Http2StreamStatisticsIndex {
+    IDX_STREAM_STATS_ID,
+    IDX_STREAM_STATS_TIMETOFIRSTBYTE,
+    IDX_STREAM_STATS_TIMETOFIRSTHEADER,
+    IDX_STREAM_STATS_TIMETOFIRSTBYTESENT,
+    IDX_STREAM_STATS_SENTBYTES,
+    IDX_STREAM_STATS_RECEIVEDBYTES,
+    IDX_STREAM_STATS_COUNT
+  };
+
+  enum Http2SessionStatisticsIndex {
+    IDX_SESSION_STATS_TYPE,
+    IDX_SESSION_STATS_PINGRTT,
+    IDX_SESSION_STATS_FRAMESRECEIVED,
+    IDX_SESSION_STATS_FRAMESSENT,
+    IDX_SESSION_STATS_STREAMCOUNT,
+    IDX_SESSION_STATS_STREAMAVERAGEDURATION,
+    IDX_SESSION_STATS_DATA_SENT,
+    IDX_SESSION_STATS_DATA_RECEIVED,
+    IDX_SESSION_STATS_MAX_CONCURRENT_STREAMS,
+    IDX_SESSION_STATS_COUNT
+  };
+
 class http2_state {
  public:
   explicit http2_state(v8::Isolate* isolate) :
@@ -77,6 +100,16 @@ class http2_state {
       offsetof(http2_state_internal, stream_state_buffer),
       IDX_STREAM_STATE_COUNT,
       root_buffer),
+    stream_stats_buffer(
+      isolate,
+      offsetof(http2_state_internal, stream_stats_buffer),
+      IDX_STREAM_STATS_COUNT,
+      root_buffer),
+    session_stats_buffer(
+      isolate,
+      offsetof(http2_state_internal, session_stats_buffer),
+      IDX_SESSION_STATS_COUNT,
+      root_buffer),
     padding_buffer(
       isolate,
       offsetof(http2_state_internal, padding_buffer),
@@ -97,6 +130,8 @@ class http2_state {
   AliasedBuffer<uint8_t, v8::Uint8Array> root_buffer;
   AliasedBuffer<double, v8::Float64Array> session_state_buffer;
   AliasedBuffer<double, v8::Float64Array> stream_state_buffer;
+  AliasedBuffer<double, v8::Float64Array> stream_stats_buffer;
+  AliasedBuffer<double, v8::Float64Array> session_stats_buffer;
   AliasedBuffer<uint32_t, v8::Uint32Array> padding_buffer;
   AliasedBuffer<uint32_t, v8::Uint32Array> options_buffer;
   AliasedBuffer<uint32_t, v8::Uint32Array> settings_buffer;
@@ -106,6 +141,8 @@ class http2_state {
     // doubles first so that they are always sizeof(double)-aligned
     double session_state_buffer[IDX_SESSION_STATE_COUNT];
     double stream_state_buffer[IDX_STREAM_STATE_COUNT];
+    double stream_stats_buffer[IDX_STREAM_STATS_COUNT];
+    double session_stats_buffer[IDX_SESSION_STATS_COUNT];
     uint32_t padding_buffer[PADDING_BUF_FIELD_COUNT];
     uint32_t options_buffer[IDX_OPTIONS_FLAGS + 1];
     uint32_t settings_buffer[IDX_SETTINGS_COUNT + 1];
diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js
index f2ef29cec25e06..07d9c55ed7e0d2 100644
--- a/test/parallel/test-http2-perf_hooks.js
+++ b/test/parallel/test-http2-perf_hooks.js
@@ -8,7 +8,7 @@ const h2 = require('http2');
 
 const { PerformanceObserver } = require('perf_hooks');
 
-const obs = new PerformanceObserver((items) => {
+const obs = new PerformanceObserver(common.mustCall((items) => {
   const entry = items.getEntries()[0];
   assert.strictEqual(entry.entryType, 'http2');
   assert.strictEqual(typeof entry.startTime, 'number');
@@ -19,6 +19,10 @@ const obs = new PerformanceObserver((items) => {
       assert.strictEqual(typeof entry.streamAverageDuration, 'number');
       assert.strictEqual(typeof entry.streamCount, 'number');
       assert.strictEqual(typeof entry.framesReceived, 'number');
+      assert.strictEqual(typeof entry.framesSent, 'number');
+      assert.strictEqual(typeof entry.bytesWritten, 'number');
+      assert.strictEqual(typeof entry.bytesRead, 'number');
+      assert.strictEqual(typeof entry.maxConcurrentStreams, 'number');
       switch (entry.type) {
         case 'server':
           assert.strictEqual(entry.streamCount, 1);
@@ -34,12 +38,15 @@ const obs = new PerformanceObserver((items) => {
       break;
     case 'Http2Stream':
       assert.strictEqual(typeof entry.timeToFirstByte, 'number');
+      assert.strictEqual(typeof entry.timeToFirstByteSent, 'number');
       assert.strictEqual(typeof entry.timeToFirstHeader, 'number');
+      assert.strictEqual(typeof entry.bytesWritten, 'number');
+      assert.strictEqual(typeof entry.bytesRead, 'number');
       break;
     default:
       assert.fail('invalid entry name');
   }
-});
+}, 4));
 obs.observe({ entryTypes: ['http2'] });
 
 const body =

From 3d13a16d96d6457ea993265948f9c8a22d86528c Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Wed, 10 Jan 2018 09:48:21 -0800
Subject: [PATCH 38/77] doc: update pushStream docs to use err first

Refs: https://github.com/nodejs/node/pull/17406#issuecomment-356661798

PR-URL: https://github.com/nodejs/node/pull/18088
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
---
 doc/api/http2.md | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 2706f4cff3f5e2..e1ac74400b49f7 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1174,14 +1174,16 @@ added: v8.4.0
 * Returns: {undefined}
 
 Initiates a push stream. The callback is invoked with the new `Http2Stream`
-instance created for the push stream.
+instance created for the push stream passed as the second argument, or an
+`Error` passed as the first argument.
 
 ```js
 const http2 = require('http2');
 const server = http2.createServer();
 server.on('stream', (stream) => {
   stream.respond({ ':status': 200 });
-  stream.pushStream({ ':path': '/' }, (pushStream) => {
+  stream.pushStream({ ':path': '/' }, (err, pushStream) => {
+    if (err) throw err;
     pushStream.respond({ ':status': 200 });
     pushStream.end('some pushed data');
   });

From de1e52e78993bc44bd0ea8e964d7bfb6996b96a4 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Thu, 11 Jan 2018 10:05:15 -0800
Subject: [PATCH 39/77] doc: fix s/rstStream/close in example

PR-URL: https://github.com/nodejs/node/pull/18088
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
---
 doc/api/http2.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index e1ac74400b49f7..e92baddf3cc695 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1015,7 +1015,7 @@ const { NGHTTP2_CANCEL } = http2.constants;
 const req = client.request({ ':path': '/' });
 
 // Cancel the stream if there's no activity after 5 seconds
-req.setTimeout(5000, () => req.rstStream(NGHTTP2_CANCEL));
+req.setTimeout(5000, () => req.close(NGHTTP2_CANCEL));
 ```
 
 #### http2stream.state

From 6a24afdfefb6e4d23e89cdf4fe121f4b780ebf49 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 8 Jan 2018 13:32:30 -0800
Subject: [PATCH 40/77] perf_hooks,http2: add performance.clear()

Add missing clear() method to `perf_hooks.performance` to
remove the entries from the master timeline to prevent
that from being a memory leak.

PR-URL: https://github.com/nodejs/node/pull/18046
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/perf_hooks.md                  |  8 ++++++++
 lib/perf_hooks.js                      |  4 ++++
 test/parallel/test-http2-perf_hooks.js | 10 +++++++++-
 3 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/doc/api/perf_hooks.md b/doc/api/perf_hooks.md
index 9b4ffb7ccb63c6..2f2910af93db34 100644
--- a/doc/api/perf_hooks.md
+++ b/doc/api/perf_hooks.md
@@ -29,6 +29,14 @@ added: v8.5.0
 The `Performance` provides access to performance metric data. A single
 instance of this class is provided via the `performance` property.
 
+### performance.clearEntries(name)
+<!-- YAML
+added: REPLACEME
+-->
+
+Remove all performance entry objects with `entryType` equal to `name` from the
+Performance Timeline.
+
 ### performance.clearFunctions([name])
 <!-- YAML
 added: v8.5.0
diff --git a/lib/perf_hooks.js b/lib/perf_hooks.js
index 4a05f7ccba7bc8..15256a63c0b97c 100644
--- a/lib/perf_hooks.js
+++ b/lib/perf_hooks.js
@@ -471,6 +471,10 @@ class Performance extends PerformanceObserverEntryList {
     this[kClearEntry]('function', name);
   }
 
+  clearEntries(name) {
+    this[kClearEntry](name);
+  }
+
   timerify(fn) {
     if (typeof fn !== 'function') {
       const errors = lazyErrors();
diff --git a/test/parallel/test-http2-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js
index 07d9c55ed7e0d2..e30d0ac83e0d1f 100644
--- a/test/parallel/test-http2-perf_hooks.js
+++ b/test/parallel/test-http2-perf_hooks.js
@@ -6,7 +6,7 @@ if (!common.hasCrypto)
 const assert = require('assert');
 const h2 = require('http2');
 
-const { PerformanceObserver } = require('perf_hooks');
+const { PerformanceObserver, performance } = require('perf_hooks');
 
 const obs = new PerformanceObserver(common.mustCall((items) => {
   const entry = items.getEntries()[0];
@@ -46,6 +46,7 @@ const obs = new PerformanceObserver(common.mustCall((items) => {
     default:
       assert.fail('invalid entry name');
   }
+  performance.clearEntries('http2');
 }, 4));
 obs.observe({ entryTypes: ['http2'] });
 
@@ -100,3 +101,10 @@ server.on('listening', common.mustCall(() => {
   }));
 
 }));
+
+process.on('exit', () => {
+  const entries = performance.getEntries();
+  // There shouldn't be any http2 entries left over.
+  assert.strictEqual(entries.length, 1);
+  assert.strictEqual(entries[0], performance.nodeTiming);
+});

From 1d5ab0042978cb332cda27527bf7bd71b0ea1575 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 8 Jan 2018 12:18:22 -0800
Subject: [PATCH 41/77] http2: remember sent headers

Add sentHeaders, sentTrailers, and sentInfoHeaders properties
on `Http2Stream`.

PR-URL: https://github.com/nodejs/node/pull/18045
Fixes: https://github.com/nodejs/node/issues/16619
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/http2.md                         | 28 ++++++++++++++
 lib/internal/http2/core.js               | 24 ++++++++++++
 test/parallel/test-http2-sent-headers.js | 47 ++++++++++++++++++++++++
 3 files changed, 99 insertions(+)
 create mode 100644 test/parallel/test-http2-sent-headers.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index e92baddf3cc695..f92aec7288d0e7 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -989,6 +989,34 @@ destroyed after either receiving an `RST_STREAM` frame from the connected peer,
 calling `http2stream.close()`, or `http2stream.destroy()`. Will be
 `undefined` if the `Http2Stream` has not been closed.
 
+#### http2stream.sentHeaders
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {[Headers Object][]}
+
+An object containing the outbound headers sent for this `Http2Stream`.
+
+#### http2stream.sentInfoHeaders
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {[Headers Object][]\[\]}
+
+An array of objects containing the outbound informational (additional) headers
+sent for this `Http2Stream`.
+
+#### http2stream.sentTrailers
+<!-- YAML
+added: REPLACEME
+-->
+
+* Value: {[Headers Object][]}
+
+An object containing the outbound trailers sent for this this `HttpStream`.
+
 #### http2stream.session
 <!-- YAML
 added: v8.4.0
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 204aa2bdbc3069..3e68ed4a59f750 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -74,6 +74,7 @@ const kEncrypted = Symbol('encrypted');
 const kHandle = Symbol('handle');
 const kID = Symbol('id');
 const kInit = Symbol('init');
+const kInfoHeaders = Symbol('sent-info-headers');
 const kMaybeDestroy = Symbol('maybe-destroy');
 const kLocalSettings = Symbol('local-settings');
 const kOptions = Symbol('options');
@@ -82,6 +83,8 @@ const kProceed = Symbol('proceed');
 const kProtocol = Symbol('protocol');
 const kProxySocket = Symbol('proxy-socket');
 const kRemoteSettings = Symbol('remote-settings');
+const kSentHeaders = Symbol('sent-headers');
+const kSentTrailers = Symbol('sent-trailers');
 const kServer = Symbol('server');
 const kSession = Symbol('session');
 const kState = Symbol('state');
@@ -256,6 +259,7 @@ function onStreamTrailers() {
     stream.destroy(headersList);
     return [];
   }
+  stream[kSentTrailers] = trailers;
   return headersList;
 }
 
@@ -1344,6 +1348,7 @@ class ClientHttp2Session extends Http2Session {
       throw headersList;
 
     const stream = new ClientHttp2Stream(this, undefined, undefined, {});
+    stream[kSentHeaders] = headers;
 
     // Close the writable side of the stream if options.endStream is set.
     if (options.endStream)
@@ -1507,6 +1512,18 @@ class Http2Stream extends Duplex {
     return `Http2Stream ${util.format(obj)}`;
   }
 
+  get sentHeaders() {
+    return this[kSentHeaders];
+  }
+
+  get sentTrailers() {
+    return this[kSentTrailers];
+  }
+
+  get sentInfoHeaders() {
+    return this[kInfoHeaders];
+  }
+
   get pending() {
     return this[kID] === undefined;
   }
@@ -1846,6 +1863,7 @@ function processRespondWithFD(self, fd, headers, offset = 0, length = -1,
   state.flags |= STREAM_FLAGS_HEADERS_SENT;
 
   const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
+  self[kSentHeaders] = headers;
   if (!Array.isArray(headersList)) {
     self.destroy(headersList);
     return;
@@ -2076,6 +2094,7 @@ class ServerHttp2Stream extends Http2Stream {
 
     const id = ret.id();
     const stream = new ServerHttp2Stream(session, ret, id, options, headers);
+    stream[kSentHeaders] = headers;
 
     if (options.endStream)
       stream.end();
@@ -2135,6 +2154,7 @@ class ServerHttp2Stream extends Http2Stream {
     const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
     if (!Array.isArray(headersList))
       throw headersList;
+    this[kSentHeaders] = headers;
 
     state.flags |= STREAM_FLAGS_HEADERS_SENT;
 
@@ -2320,6 +2340,10 @@ class ServerHttp2Stream extends Http2Stream {
     const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
     if (!Array.isArray(headersList))
       throw headersList;
+    if (!this[kInfoHeaders])
+      this[kInfoHeaders] = [headers];
+    else
+      this[kInfoHeaders].push(headers);
 
     const ret = this[kHandle].info(headersList);
     if (ret < 0)
diff --git a/test/parallel/test-http2-sent-headers.js b/test/parallel/test-http2-sent-headers.js
new file mode 100644
index 00000000000000..bffa4d71c6d5f3
--- /dev/null
+++ b/test/parallel/test-http2-sent-headers.js
@@ -0,0 +1,47 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const h2 = require('http2');
+
+const server = h2.createServer();
+
+server.on('stream', common.mustCall((stream) => {
+  stream.additionalHeaders({ ':status': 102 });
+  assert.strictEqual(stream.sentInfoHeaders[0][':status'], 102);
+
+  stream.respond({ abc: 'xyz' }, {
+    getTrailers(headers) {
+      headers.xyz = 'abc';
+    }
+  });
+  assert.strictEqual(stream.sentHeaders.abc, 'xyz');
+  assert.strictEqual(stream.sentHeaders[':status'], 200);
+  assert.notStrictEqual(stream.sentHeaders.date, undefined);
+  stream.end();
+  stream.on('close', () => {
+    assert.strictEqual(stream.sentTrailers.xyz, 'abc');
+  });
+}));
+
+server.listen(0, common.mustCall(() => {
+  const client = h2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request();
+
+  req.on('headers', common.mustCall((headers) => {
+    assert.strictEqual(headers[':status'], 102);
+  }));
+
+  assert.strictEqual(req.sentHeaders[':method'], 'GET');
+  assert.strictEqual(req.sentHeaders[':authority'],
+                     `localhost:${server.address().port}`);
+  assert.strictEqual(req.sentHeaders[':scheme'], 'http');
+  assert.strictEqual(req.sentHeaders[':path'], '/');
+  req.resume();
+  req.on('close', () => {
+    server.close();
+    client.close();
+  });
+}));

From 39e27fd9f218db646a7589d52e75768432d3c6f2 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sat, 13 Jan 2018 17:06:51 +0100
Subject: [PATCH 42/77] src: remove declarations for missing functions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PR-URL: https://github.com/nodejs/node/pull/18134
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Jon Moss <me@jonathanmoss.me>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
---
 src/node_http2.h | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/src/node_http2.h b/src/node_http2.h
index a7938a5f800854..710ee91d91535f 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -1048,24 +1048,6 @@ class Http2Session : public AsyncWrap {
       int lib_error_code,
       void* user_data);
 
-
-  static inline ssize_t OnStreamReadFD(
-      nghttp2_session* session,
-      int32_t id,
-      uint8_t* buf,
-      size_t length,
-      uint32_t* flags,
-      nghttp2_data_source* source,
-      void* user_data);
-  static inline ssize_t OnStreamRead(
-      nghttp2_session* session,
-      int32_t id,
-      uint8_t* buf,
-      size_t length,
-      uint32_t* flags,
-      nghttp2_data_source* source,
-      void* user_data);
-
   struct Callbacks {
     inline explicit Callbacks(bool kHasGetPaddingCallback);
     inline ~Callbacks();

From dcb986e07d4588da7a4e34ca0ec4c065871024a9 Mon Sep 17 00:00:00 2001
From: Roman Reiss <me@silverwind.io>
Date: Sun, 14 Jan 2018 22:08:46 +0100
Subject: [PATCH 43/77] src,doc,test: Fix common misspellings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PR-URL: https://github.com/nodejs/node/pull/18151
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Richard Lau <riclau@uk.ibm.com>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
---
 doc/api/stream.md                                            | 4 ++--
 src/node_http2.cc                                            | 4 ++--
 test/async-hooks/test-callback-error.js                      | 2 +-
 test/common/README.md                                        | 2 +-
 test/fixtures/loop.js                                        | 2 +-
 test/sequential/test-inspector-async-hook-setup-at-signal.js | 2 +-
 6 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/doc/api/stream.md b/doc/api/stream.md
index aed4a1fd90e1d9..f0d393543d6516 100644
--- a/doc/api/stream.md
+++ b/doc/api/stream.md
@@ -1457,7 +1457,7 @@ added: v8.0.0
   argument.
 
 The `_destroy()` method is called by [`writable.destroy()`][writable-destroy].
-It can be overriden by child classes but it **must not** be called directly.
+It can be overridden by child classes but it **must not** be called directly.
 
 #### writable.\_final(callback)
 <!-- YAML
@@ -1670,7 +1670,7 @@ added: v8.0.0
   argument.
 
 The `_destroy()` method is called by [`readable.destroy()`][readable-destroy].
-It can be overriden by child classes but it **must not** be called directly.
+It can be overridden by child classes but it **must not** be called directly.
 
 #### readable.push(chunk[, encoding])
 <!-- YAML
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 2bcf7ccfa0554e..e4864c92dee29c 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -832,7 +832,7 @@ inline int Http2Session::OnBeginHeadersCallback(nghttp2_session* handle,
 }
 
 // Called by nghttp2 for each header name/value pair in a HEADERS block.
-// This had to have been preceeded by a call to OnBeginHeadersCallback so
+// This had to have been preceded by a call to OnBeginHeadersCallback so
 // the Http2Stream is guaranteed to already exist.
 inline int Http2Session::OnHeaderCallback(nghttp2_session* handle,
                                           const nghttp2_frame* frame,
@@ -2732,7 +2732,7 @@ void Http2Session::Ping(const FunctionCallbackInfo<Value>& args) {
     return args.GetReturnValue().Set(false);
   }
 
-  // The Ping itself is an Async resource. When the acknowledgement is recieved,
+  // The Ping itself is an Async resource. When the acknowledgement is received,
   // the callback will be invoked and a notification sent out to JS land. The
   // notification will include the duration of the ping, allowing the round
   // trip to be measured.
diff --git a/test/async-hooks/test-callback-error.js b/test/async-hooks/test-callback-error.js
index 09eb2e0b478a6e..e50e069d64af24 100644
--- a/test/async-hooks/test-callback-error.js
+++ b/test/async-hooks/test-callback-error.js
@@ -94,7 +94,7 @@ assert.ok(!arg);
       assert.strictEqual(code, null);
       // most posix systems will show 'SIGABRT', but alpine34 does not
       if (signal !== 'SIGABRT') {
-        console.log(`parent recived signal ${signal}\nchild's stderr:`);
+        console.log(`parent received signal ${signal}\nchild's stderr:`);
         console.log(stderr);
         process.exit(1);
       }
diff --git a/test/common/README.md b/test/common/README.md
index fad4467775ac32..01ed65d95bdac1 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -647,7 +647,7 @@ internet-related tests.
 A set of addresses for internet-related tests. All properties are configurable
 via `NODE_TEST_*` environment variables. For example, to configure
 `internet.addresses.INET_HOST`, set the environment
-vairable `NODE_TEST_INET_HOST` to a specified host.
+variable `NODE_TEST_INET_HOST` to a specified host.
 
 ## WPT Module
 
diff --git a/test/fixtures/loop.js b/test/fixtures/loop.js
index 461fb393583e68..1f093bdf574660 100644
--- a/test/fixtures/loop.js
+++ b/test/fixtures/loop.js
@@ -4,7 +4,7 @@ console.log('A message', 5);
 while (t > 0) {
   if (t++ === 1000) {
     t = 0;
-    console.log(`Outputed message #${k++}`);
+    console.log(`Outputted message #${k++}`);
   }
 }
 process.exit(55);
diff --git a/test/sequential/test-inspector-async-hook-setup-at-signal.js b/test/sequential/test-inspector-async-hook-setup-at-signal.js
index 96e8b28a7a250e..5ff7dec9473ac2 100644
--- a/test/sequential/test-inspector-async-hook-setup-at-signal.js
+++ b/test/sequential/test-inspector-async-hook-setup-at-signal.js
@@ -17,7 +17,7 @@ function waitUntilDebugged() {
   // call stack depth is 0. We need a chance to call
   // Debugger.setAsyncCallStackDepth *before* activating the actual timer for
   // async stack traces to work. Directly using a debugger statement would be
-  // too brittle, and using a longer timeout would unnecesarily slow down the
+  // too brittle, and using a longer timeout would unnecessarily slow down the
   // test on most machines. Triggering a debugger break through an interval is
   // a faster and more reliable way.
   process._rawDebug('Signal received, waiting for debugger setup');

From 3535f6f38756891147a5e5b66bf7dd4155900bdc Mon Sep 17 00:00:00 2001
From: Moritz Peters <maritz@users.noreply.github.com>
Date: Mon, 15 Jan 2018 20:36:55 +0100
Subject: [PATCH 44/77] doc: fix typo in http2stream.close param default
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

PR-URL: https://github.com/nodejs/node/pull/18166
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
---
 doc/api/http2.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index f92aec7288d0e7..cbdb8c2d9f258d 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -919,7 +919,7 @@ added: v8.4.0
 -->
 
 * code {number} Unsigned 32-bit integer identifying the error code. **Default:**
-  `http2.constant.NGHTTP2_NO_ERROR` (`0x00`)
+  `http2.constants.NGHTTP2_NO_ERROR` (`0x00`)
 * `callback` {Function} An optional function registered to listen for the
   `'close'` event.
 * Returns: {undefined}

From f49463541a8cacbe0e74318588f9d2ea05d09e45 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Mon, 8 Jan 2018 01:52:17 +0100
Subject: [PATCH 45/77] src: introduce internal buffer slice constructor

Add a C++ variant of `Buffer.from(arrayBuffer, offset, length)`.

PR-URL: https://github.com/nodejs/node/pull/18030
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 src/node_buffer.cc   | 48 ++++++++++++++++++--------------------------
 src/node_internals.h | 13 ++++++++++++
 2 files changed, 32 insertions(+), 29 deletions(-)

diff --git a/src/node_buffer.cc b/src/node_buffer.cc
index 66ae9feb697a32..0c68e04e5a7b18 100644
--- a/src/node_buffer.cc
+++ b/src/node_buffer.cc
@@ -303,15 +303,14 @@ MaybeLocal<Object> New(Environment* env, size_t length) {
         data,
         length,
         ArrayBufferCreationMode::kInternalized);
-  Local<Uint8Array> ui = Uint8Array::New(ab, 0, length);
-  Maybe<bool> mb =
-      ui->SetPrototype(env->context(), env->buffer_prototype_object());
-  if (mb.FromMaybe(false))
-    return scope.Escape(ui);
+  MaybeLocal<Uint8Array> ui = Buffer::New(env, ab, 0, length);
 
-  // Object failed to be created. Clean up resources.
-  free(data);
-  return Local<Object>();
+  if (ui.IsEmpty()) {
+    // Object failed to be created. Clean up resources.
+    free(data);
+  }
+
+  return scope.Escape(ui.FromMaybe(Local<Uint8Array>()));
 }
 
 
@@ -349,15 +348,14 @@ MaybeLocal<Object> Copy(Environment* env, const char* data, size_t length) {
         new_data,
         length,
         ArrayBufferCreationMode::kInternalized);
-  Local<Uint8Array> ui = Uint8Array::New(ab, 0, length);
-  Maybe<bool> mb =
-      ui->SetPrototype(env->context(), env->buffer_prototype_object());
-  if (mb.FromMaybe(false))
-    return scope.Escape(ui);
+  MaybeLocal<Uint8Array> ui = Buffer::New(env, ab, 0, length);
 
-  // Object failed to be created. Clean up resources.
-  free(new_data);
-  return Local<Object>();
+  if (ui.IsEmpty()) {
+    // Object failed to be created. Clean up resources.
+    free(new_data);
+  }
+
+  return scope.Escape(ui.FromMaybe(Local<Uint8Array>()));
 }
 
 
@@ -392,15 +390,14 @@ MaybeLocal<Object> New(Environment* env,
   // correct.
   if (data == nullptr)
     ab->Neuter();
-  Local<Uint8Array> ui = Uint8Array::New(ab, 0, length);
-  Maybe<bool> mb =
-      ui->SetPrototype(env->context(), env->buffer_prototype_object());
+  MaybeLocal<Uint8Array> ui = Buffer::New(env, ab, 0, length);
 
-  if (!mb.FromMaybe(false))
+  if (ui.IsEmpty()) {
     return Local<Object>();
+  }
 
   CallbackInfo::New(env->isolate(), ab, callback, data, hint);
-  return scope.Escape(ui);
+  return scope.Escape(ui.ToLocalChecked());
 }
 
 
@@ -415,8 +412,6 @@ MaybeLocal<Object> New(Isolate* isolate, char* data, size_t length) {
 
 
 MaybeLocal<Object> New(Environment* env, char* data, size_t length) {
-  EscapableHandleScope scope(env->isolate());
-
   if (length > 0) {
     CHECK_NE(data, nullptr);
     CHECK(length <= kMaxLength);
@@ -427,12 +422,7 @@ MaybeLocal<Object> New(Environment* env, char* data, size_t length) {
                        data,
                        length,
                        ArrayBufferCreationMode::kInternalized);
-  Local<Uint8Array> ui = Uint8Array::New(ab, 0, length);
-  Maybe<bool> mb =
-      ui->SetPrototype(env->context(), env->buffer_prototype_object());
-  if (mb.FromMaybe(false))
-    return scope.Escape(ui);
-  return Local<Object>();
+  return Buffer::New(env, ab, 0, length).FromMaybe(Local<Object>());
 }
 
 namespace {
diff --git a/src/node_internals.h b/src/node_internals.h
index 8f3fb4fb9aa27d..fb288ede410ee2 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -325,6 +325,19 @@ v8::MaybeLocal<v8::Object> New(Environment* env,
 // Mixing operator new and free() is undefined behavior so don't do that.
 v8::MaybeLocal<v8::Object> New(Environment* env, char* data, size_t length);
 
+inline
+v8::MaybeLocal<v8::Uint8Array> New(Environment* env,
+                                   v8::Local<v8::ArrayBuffer> ab,
+                                   size_t byte_offset,
+                                   size_t length) {
+  v8::Local<v8::Uint8Array> ui = v8::Uint8Array::New(ab, byte_offset, length);
+  v8::Maybe<bool> mb =
+      ui->SetPrototype(env->context(), env->buffer_prototype_object());
+  if (mb.IsNothing())
+    return v8::MaybeLocal<v8::Uint8Array>();
+  return ui;
+}
+
 // Construct a Buffer from a MaybeStackBuffer (and also its subclasses like
 // Utf8Value and TwoByteValue).
 // If |buf| is invalidated, an empty MaybeLocal is returned, and nothing is

From cf46b9508ce6ddbae62b3f85263b393b1ab80c58 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Mon, 8 Jan 2018 03:50:51 +0100
Subject: [PATCH 46/77] http2: refactor read mechanism

Refactor the read mechanism to completely avoid copying.

Instead of copying individual `DATA` frame contents into buffers,
create `ArrayBuffer` instances for all socket reads and emit
slices of those `ArrayBuffer`s to JS.

PR-URL: https://github.com/nodejs/node/pull/18030
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 lib/internal/http2/core.js                    |  30 ++-
 src/node_http2.cc                             | 184 ++++++++----------
 src/node_http2.h                              |  18 +-
 test/common/README.md                         |  11 ++
 test/common/index.js                          |   6 +
 test/parallel/test-http2-backpressure.js      |  49 +++++
 ...t-http2-misbehaving-flow-control-paused.js |   3 +
 7 files changed, 174 insertions(+), 127 deletions(-)
 create mode 100644 test/parallel/test-http2-backpressure.js

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 3e68ed4a59f750..bc1067e9963ba1 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -277,7 +277,7 @@ function submitRstStream(code) {
 // point, close them. If there is an open fd for file send, close that also.
 // At this point the underlying node::http2:Http2Stream handle is no
 // longer usable so destroy it also.
-function onStreamClose(code, hasData) {
+function onStreamClose(code) {
   const stream = this[kOwner];
   if (stream.destroyed)
     return;
@@ -285,8 +285,7 @@ function onStreamClose(code, hasData) {
   const state = stream[kState];
 
   debug(`Http2Stream ${stream[kID]} [Http2Session ` +
-        `${sessionName(stream[kSession][kType])}]: closed with code ${code}` +
-        ` [has data? ${hasData}]`);
+        `${sessionName(stream[kSession][kType])}]: closed with code ${code}`);
 
   if (!stream.closed) {
     // Unenroll from timeouts
@@ -304,13 +303,14 @@ function onStreamClose(code, hasData) {
 
   if (state.fd !== undefined)
     tryClose(state.fd);
-  stream[kMaybeDestroy](null, code, hasData);
+  stream.push(null);
+  stream[kMaybeDestroy](null, code);
 }
 
 // Receives a chunk of data for a given stream and forwards it on
 // to the Http2Stream Duplex for processing.
-function onStreamRead(nread, buf, handle) {
-  const stream = handle[kOwner];
+function onStreamRead(nread, buf) {
+  const stream = this[kOwner];
   if (nread >= 0 && !stream.destroyed) {
     debug(`Http2Stream ${stream[kID]} [Http2Session ` +
           `${sessionName(stream[kSession][kType])}]: receiving data chunk ` +
@@ -318,7 +318,7 @@ function onStreamRead(nread, buf, handle) {
     stream[kUpdateTimer]();
     if (!stream.push(buf)) {
       if (!stream.destroyed)  // we have to check a second time
-        handle.readStop();
+        this.readStop();
     }
     return;
   }
@@ -1427,13 +1427,8 @@ function streamOnResume() {
 }
 
 function streamOnPause() {
-  // if (!this.destroyed && !this.pending)
-  //   this[kHandle].readStop();
-}
-
-function handleFlushData(self) {
   if (!this.destroyed && !this.pending)
-    this[kHandle].flushData();
+    this[kHandle].readStop();
 }
 
 // If the writable side of the Http2Stream is still open, emit the
@@ -1679,11 +1674,10 @@ class Http2Stream extends Duplex {
       this.push(null);
       return;
     }
-    const flushfn = handleFlushData.bind(this);
     if (!this.pending) {
-      flushfn();
+      streamOnResume.call(this);
     } else {
-      this.once('ready', flushfn);
+      this.once('ready', streamOnResume);
     }
   }
 
@@ -1822,10 +1816,10 @@ class Http2Stream extends Duplex {
 
   // The Http2Stream can be destroyed if it has closed and if the readable
   // side has received the final chunk.
-  [kMaybeDestroy](error, code = NGHTTP2_NO_ERROR, hasData = true) {
+  [kMaybeDestroy](error, code = NGHTTP2_NO_ERROR) {
     if (error == null) {
       if (code === NGHTTP2_NO_ERROR &&
-          ((!this._readableState.ended && hasData) ||
+          (!this._readableState.ended ||
           !this._writableState.ended ||
           this._writableState.pendingcb > 0 ||
           !this.closed)) {
diff --git a/src/node_http2.cc b/src/node_http2.cc
index e4864c92dee29c..bd7eeee8655e52 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -9,6 +9,7 @@
 
 namespace node {
 
+using v8::ArrayBuffer;
 using v8::Boolean;
 using v8::Context;
 using v8::Float64Array;
@@ -979,7 +980,6 @@ inline int Http2Session::OnStreamClose(nghttp2_session* handle,
   // Intentionally ignore the callback if the stream does not exist or has
   // already been destroyed
   if (stream != nullptr && !stream->IsDestroyed()) {
-    stream->AddChunk(nullptr, 0);
     stream->Close(code);
     // It is possible for the stream close to occur before the stream is
     // ever passed on to the javascript side. If that happens, skip straight
@@ -990,9 +990,8 @@ inline int Http2Session::OnStreamClose(nghttp2_session* handle,
         stream->object()->Get(context, env->onstreamclose_string())
             .ToLocalChecked();
     if (fn->IsFunction()) {
-      Local<Value> argv[2] = {
-        Integer::NewFromUnsigned(isolate, code),
-        Boolean::New(isolate, stream->HasDataChunks(true))
+      Local<Value> argv[] = {
+        Integer::NewFromUnsigned(isolate, code)
       };
       stream->MakeCallback(fn.As<Function>(), arraysize(argv), argv);
     } else {
@@ -1029,6 +1028,8 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
   Http2Session* session = static_cast<Http2Session*>(user_data);
   DEBUG_HTTP2SESSION2(session, "buffering data chunk for stream %d, size: "
               "%d, flags: %d", id, len, flags);
+  Environment* env = session->env();
+  HandleScope scope(env->isolate());
   // We should never actually get a 0-length chunk so this check is
   // only a precaution at this point.
   if (len > 0) {
@@ -1040,8 +1041,25 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
     // If the stream has been destroyed, ignore this chunk
     if (stream->IsDestroyed())
       return 0;
+
     stream->statistics_.received_bytes += len;
-    stream->AddChunk(data, len);
+
+    // There is a single large array buffer for the entire data read from the
+    // network; create a slice of that array buffer and emit it as the
+    // received data buffer.
+    CHECK(!session->stream_buf_ab_.IsEmpty());
+    size_t offset = reinterpret_cast<const char*>(data) - session->stream_buf_;
+    // Verify that the data offset is inside the current read buffer.
+    CHECK_LE(offset, session->stream_buf_size_);
+
+    Local<Object> buf =
+        Buffer::New(env, session->stream_buf_ab_, offset, len).ToLocalChecked();
+
+    stream->EmitData(len, buf, Local<Object>());
+    if (!stream->IsReading())
+      stream->inbound_consumed_data_while_paused_ += len;
+    else
+      nghttp2_session_consume_stream(handle, id, len);
   }
   return 0;
 }
@@ -1227,9 +1245,8 @@ inline void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
 
 
 // Called by OnFrameReceived when a complete DATA frame has been received.
-// If we know that this is the last DATA frame (because the END_STREAM flag
-// is set), then we'll terminate the readable side of the StreamBase. If
-// the StreamBase is flowing, we'll push the chunks of data out to JS land.
+// If we know that this was the last DATA frame (because the END_STREAM flag
+// is set), then we'll terminate the readable side of the StreamBase.
 inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
   int32_t id = GetFrameID(frame);
   DEBUG_HTTP2SESSION2(this, "handling data frame for stream %d", id);
@@ -1240,11 +1257,8 @@ inline void Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
     return;
 
   if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
-    stream->AddChunk(nullptr, 0);
+    stream->EmitData(UV_EOF, Local<Object>(), Local<Object>());
   }
-
-  if (stream->IsReading())
-    stream->FlushDataChunks();
 }
 
 
@@ -1619,45 +1633,67 @@ void Http2Session::OnStreamAllocImpl(size_t suggested_size,
                                      uv_buf_t* buf,
                                      void* ctx) {
   Http2Session* session = static_cast<Http2Session*>(ctx);
-  buf->base = session->stream_alloc();
-  buf->len = kAllocBufferSize;
+  CHECK_EQ(session->stream_buf_, nullptr);
+  CHECK_EQ(session->stream_buf_size_, 0);
+  buf->base = session->stream_buf_ = Malloc(suggested_size);
+  buf->len = session->stream_buf_size_ = suggested_size;
+  session->IncrementCurrentSessionMemory(suggested_size);
 }
 
 // Callback used to receive inbound data from the i/o stream
 void Http2Session::OnStreamReadImpl(ssize_t nread,
-                                    const uv_buf_t* bufs,
+                                    const uv_buf_t* buf,
                                     uv_handle_type pending,
                                     void* ctx) {
   Http2Session* session = static_cast<Http2Session*>(ctx);
   Http2Scope h2scope(session);
   CHECK_NE(session->stream_, nullptr);
   DEBUG_HTTP2SESSION2(session, "receiving %d bytes", nread);
-  if (nread < 0) {
-    uv_buf_t tmp_buf;
-    tmp_buf.base = nullptr;
-    tmp_buf.len = 0;
-    session->prev_read_cb_.fn(nread,
-                              &tmp_buf,
-                              pending,
-                              session->prev_read_cb_.ctx);
-    return;
-  }
-  if (bufs->len > 0) {
+  if (nread <= 0) {
+    free(session->stream_buf_);
+    if (nread < 0) {
+      uv_buf_t tmp_buf = uv_buf_init(nullptr, 0);
+      session->prev_read_cb_.fn(nread,
+                                &tmp_buf,
+                                pending,
+                                session->prev_read_cb_.ctx);
+    }
+  } else {
     // Only pass data on if nread > 0
-    uv_buf_t buf[] { uv_buf_init((*bufs).base, nread) };
+
+    // Verify that currently: There is memory allocated into which
+    // the data has been read, and that memory buffer is at least as large
+    // as the amount of data we have read, but we have not yet made an
+    // ArrayBuffer out of it.
+    CHECK_NE(session->stream_buf_, nullptr);
+    CHECK_EQ(session->stream_buf_, buf->base);
+    CHECK_EQ(session->stream_buf_size_, buf->len);
+    CHECK_GE(session->stream_buf_size_, static_cast<size_t>(nread));
+    CHECK(session->stream_buf_ab_.IsEmpty());
+
+    Environment* env = session->env();
+    Isolate* isolate = env->isolate();
+    HandleScope scope(isolate);
+    Local<Context> context = env->context();
+    Context::Scope context_scope(context);
+
+    // Create an array buffer for the read data. DATA frames will be emitted
+    // as slices of this array buffer to avoid having to copy memory.
+    session->stream_buf_ab_ =
+        ArrayBuffer::New(isolate,
+                         session->stream_buf_,
+                         session->stream_buf_size_,
+                         v8::ArrayBufferCreationMode::kInternalized);
+
+    uv_buf_t buf_ = uv_buf_init(buf->base, nread);
     session->statistics_.data_received += nread;
-    ssize_t ret = session->Write(buf, 1);
+    ssize_t ret = session->Write(&buf_, 1);
 
     // Note: if ssize_t is not defined (e.g. on Win32), nghttp2 will typedef
     // ssize_t to int. Cast here so that the < 0 check actually works on
     // Windows.
     if (static_cast<int>(ret) < 0) {
       DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret);
-      Environment* env = session->env();
-      Isolate* isolate = env->isolate();
-      HandleScope scope(isolate);
-      Local<Context> context = env->context();
-      Context::Scope context_scope(context);
 
       Local<Value> argv[1] = {
         Integer::New(isolate, ret),
@@ -1668,6 +1704,13 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
                           nghttp2_session_want_read(**session));
     }
   }
+
+  // Since we are finished handling this write, reset the stream buffer.
+  // The memory has either been free()d or was handed over to V8.
+  session->DecrementCurrentSessionMemory(session->stream_buf_size_);
+  session->stream_buf_ = nullptr;
+  session->stream_buf_size_ = 0;
+  session->stream_buf_ab_ = Local<ArrayBuffer>();
 }
 
 void Http2Session::OnStreamDestructImpl(void* ctx) {
@@ -1782,30 +1825,6 @@ void Http2Stream::OnTrailers(const SubmitTrailers& submit_trailers) {
   }
 }
 
-inline bool Http2Stream::HasDataChunks(bool ignore_eos) {
-  return data_chunks_.size() > (ignore_eos ? 1 : 0);
-}
-
-// Appends a chunk of received DATA frame data to this Http2Streams internal
-// queue. Note that we must memcpy each chunk because of the way that nghttp2
-// handles it's internal memory`.
-inline void Http2Stream::AddChunk(const uint8_t* data, size_t len) {
-  CHECK(!this->IsDestroyed());
-  if (this->statistics_.first_byte == 0)
-    this->statistics_.first_byte = uv_hrtime();
-  if (flags_ & NGHTTP2_STREAM_FLAG_EOS)
-    return;
-  char* buf = nullptr;
-  if (len > 0 && data != nullptr) {
-    buf = Malloc<char>(len);
-    memcpy(buf, data, len);
-  } else if (data == nullptr) {
-    flags_ |= NGHTTP2_STREAM_FLAG_EOS;
-  }
-  data_chunks_.emplace(uv_buf_init(buf, len));
-}
-
-
 inline void Http2Stream::Close(int32_t code) {
   CHECK(!this->IsDestroyed());
   flags_ |= NGHTTP2_STREAM_FLAG_CLOSED;
@@ -1842,13 +1861,6 @@ inline void Http2Stream::Destroy() {
 
   DEBUG_HTTP2STREAM(this, "destroying stream");
 
-  // Free any remaining incoming data chunks.
-  while (!data_chunks_.empty()) {
-    uv_buf_t buf = data_chunks_.front();
-    free(buf.base);
-    data_chunks_.pop();
-  }
-
   // Wait until the start of the next loop to delete because there
   // may still be some pending operations queued for this stream.
   env()->SetImmediate([](Environment* env, void* data) {
@@ -1874,39 +1886,6 @@ inline void Http2Stream::Destroy() {
 }
 
 
-// Uses the StreamBase API to push a single chunk of queued inbound DATA
-// to JS land.
-void Http2Stream::OnDataChunk(uv_buf_t* chunk) {
-  CHECK(!this->IsDestroyed());
-  Isolate* isolate = env()->isolate();
-  HandleScope scope(isolate);
-  ssize_t len = -1;
-  Local<Object> buf;
-  if (chunk != nullptr) {
-    len = chunk->len;
-    buf = Buffer::New(isolate, chunk->base, len).ToLocalChecked();
-  }
-  EmitData(len, buf, this->object());
-}
-
-
-inline void Http2Stream::FlushDataChunks() {
-  CHECK(!this->IsDestroyed());
-  Http2Scope h2scope(this);
-  if (!data_chunks_.empty()) {
-    uv_buf_t buf = data_chunks_.front();
-    data_chunks_.pop();
-    if (buf.len > 0) {
-      CHECK_EQ(nghttp2_session_consume_stream(session_->session(),
-                                              id_, buf.len), 0);
-      OnDataChunk(&buf);
-    } else {
-      OnDataChunk(nullptr);
-    }
-  }
-}
-
-
 // Initiates a response on the Http2Stream using data provided via the
 // StreamBase Streams API.
 inline int Http2Stream::SubmitResponse(nghttp2_nv* nva,
@@ -2013,13 +1992,20 @@ inline Http2Stream* Http2Stream::SubmitPushPromise(nghttp2_nv* nva,
 // Switch the StreamBase into flowing mode to begin pushing chunks of data
 // out to JS land.
 inline int Http2Stream::ReadStart() {
+  Http2Scope h2scope(this);
   CHECK(!this->IsDestroyed());
   flags_ |= NGHTTP2_STREAM_FLAG_READ_START;
   flags_ &= ~NGHTTP2_STREAM_FLAG_READ_PAUSED;
 
-  // Flush any queued data chunks immediately out to the JS layer
-  FlushDataChunks();
   DEBUG_HTTP2STREAM(this, "reading starting");
+
+  // Tell nghttp2 about our consumption of the data that was handed
+  // off to JS land.
+  nghttp2_session_consume_stream(session_->session(),
+                                 id_,
+                                 inbound_consumed_data_while_paused_);
+  inbound_consumed_data_while_paused_ = 0;
+
   return 0;
 }
 
diff --git a/src/node_http2.h b/src/node_http2.h
index 710ee91d91535f..5acd45dc51ee18 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -549,12 +549,6 @@ class Http2Stream : public AsyncWrap,
 
   inline void EmitStatistics();
 
-  inline bool HasDataChunks(bool ignore_eos = false);
-
-  inline void AddChunk(const uint8_t* data, size_t len);
-
-  inline void FlushDataChunks();
-
   // Process a Data Chunk
   void OnDataChunk(uv_buf_t* chunk);
 
@@ -740,8 +734,11 @@ class Http2Stream : public AsyncWrap,
   uint32_t current_headers_length_ = 0;  // total number of octets
   std::vector<nghttp2_header> current_headers_;
 
-  // Inbound Data... This is the data received via DATA frames for this stream.
-  std::queue<uv_buf_t> data_chunks_;
+  // This keeps track of the amount of data read from the socket while the
+  // socket was in paused mode. When `ReadStart()` is called (and not before
+  // then), we tell nghttp2 that we consumed that data to get proper
+  // backpressure handling.
+  size_t inbound_consumed_data_while_paused_ = 0;
 
   // Outbound Data... This is the data written by the JS layer that is
   // waiting to be written out to the socket.
@@ -1085,8 +1082,9 @@ class Http2Session : public AsyncWrap {
   // use this to allow timeout tracking during long-lasting writes
   uint32_t chunks_sent_since_last_write_ = 0;
 
-  uv_prepare_t* prep_ = nullptr;
-  char stream_buf_[kAllocBufferSize];
+  char* stream_buf_ = nullptr;
+  size_t stream_buf_size_ = 0;
+  v8::Local<v8::ArrayBuffer> stream_buf_ab_;
 
   size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
   std::queue<Http2Ping*> outstanding_pings_;
diff --git a/test/common/README.md b/test/common/README.md
index 01ed65d95bdac1..236b7e5515f042 100644
--- a/test/common/README.md
+++ b/test/common/README.md
@@ -262,6 +262,17 @@ fail.
 
 If `fn` is not provided, an empty function will be used.
 
+### mustCallAsync([fn][, exact])
+* `fn` [&lt;Function>]
+* `exact` [&lt;Number>] default = 1
+* return [&lt;Function>]
+
+The same as `mustCall()`, except that it is also checked that the Promise
+returned by the function is fulfilled for each invocation of the function.
+
+The return value of the wrapped function is the return value of the original
+function, if necessary wrapped as a promise.
+
 ### mustCallAtLeast([fn][, minimum])
 * `fn` [&lt;Function>] default = () => {}
 * `minimum` [&lt;Number>] default = 1
diff --git a/test/common/index.js b/test/common/index.js
index 80ba48d25a1710..cb82cd6a93ee3a 100644
--- a/test/common/index.js
+++ b/test/common/index.js
@@ -442,6 +442,12 @@ exports.mustCallAtLeast = function(fn, minimum) {
   return _mustCallInner(fn, minimum, 'minimum');
 };
 
+exports.mustCallAsync = function(fn, exact) {
+  return exports.mustCall((...args) => {
+    return Promise.resolve(fn(...args)).then(exports.mustCall((val) => val));
+  }, exact);
+};
+
 function _mustCallInner(fn, criteria = 1, field) {
   if (process._exiting)
     throw new Error('Cannot use common.mustCall*() in process exit handler');
diff --git a/test/parallel/test-http2-backpressure.js b/test/parallel/test-http2-backpressure.js
new file mode 100644
index 00000000000000..9b69dddbfd2e26
--- /dev/null
+++ b/test/parallel/test-http2-backpressure.js
@@ -0,0 +1,49 @@
+'use strict';
+
+// Verifies that a full HTTP2 pipeline handles backpressure.
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const makeDuplexPair = require('../common/duplexpair');
+
+common.crashOnUnhandledRejection();
+
+{
+  let req;
+  const server = http2.createServer();
+  server.on('stream', common.mustCallAsync(async (stream, headers) => {
+    stream.respond({
+      'content-type': 'text/html',
+      ':status': 200
+    });
+    req._readableState.highWaterMark = 20;
+    stream._writableState.highWaterMark = 20;
+    assert.strictEqual(stream.write('A'.repeat(5)), true);
+    assert.strictEqual(stream.write('A'.repeat(40)), false);
+    assert.strictEqual(await event(req, 'data'), 'A'.repeat(5));
+    assert.strictEqual(await event(req, 'data'), 'A'.repeat(40));
+    await event(stream, 'drain');
+    assert.strictEqual(stream.write('A'.repeat(5)), true);
+    assert.strictEqual(stream.write('A'.repeat(40)), false);
+  }));
+
+  const { clientSide, serverSide } = makeDuplexPair();
+  server.emit('connection', serverSide);
+
+  const client = http2.connect('http://localhost:80', {
+    createConnection: common.mustCall(() => clientSide)
+  });
+
+  req = client.request({ ':path': '/' });
+  req.setEncoding('utf8');
+  req.end();
+}
+
+function event(ee, eventName) {
+  return new Promise((resolve) => {
+    ee.once(eventName, common.mustCall(resolve));
+  });
+}
diff --git a/test/parallel/test-http2-misbehaving-flow-control-paused.js b/test/parallel/test-http2-misbehaving-flow-control-paused.js
index 0b7299d5ac80a8..d69e0fd802979a 100644
--- a/test/parallel/test-http2-misbehaving-flow-control-paused.js
+++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js
@@ -56,6 +56,9 @@ let client;
 
 const server = h2.createServer({ settings: { initialWindowSize: 36 } });
 server.on('stream', (stream) => {
+  // Set the high water mark to zero, since otherwise we still accept
+  // reads from the source stream (if we can consume them).
+  stream._readableState.highWaterMark = 0;
   stream.pause();
   stream.on('error', common.expectsError({
     code: 'ERR_HTTP2_STREAM_ERROR',

From 9ff0e8c66d1cb75b52341cc3d71bf890d7c17c37 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Tue, 16 Jan 2018 09:34:20 -0800
Subject: [PATCH 47/77] http2: add checks for server close callback

Verify that server close callbacks are being called

PR-URL: https://github.com/nodejs/node/pull/18182
Refs: https://github.com/nodejs/node/issues/18176
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 test/parallel/test-http2-create-client-secure-session.js | 3 ++-
 test/parallel/test-http2-create-client-session.js        | 4 +++-
 test/parallel/test-http2-createwritereq.js               | 2 +-
 test/parallel/test-http2-misbehaving-flow-control.js     | 4 +++-
 4 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/test/parallel/test-http2-create-client-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js
index 6120a58602065d..b0111e15b69c15 100644
--- a/test/parallel/test-http2-create-client-secure-session.js
+++ b/test/parallel/test-http2-create-client-secure-session.js
@@ -38,6 +38,7 @@ function onStream(stream, headers) {
 function verifySecureSession(key, cert, ca, opts) {
   const server = h2.createSecureServer({ cert, key });
   server.on('stream', common.mustCall(onStream));
+  server.on('close', common.mustCall());
   server.listen(0, common.mustCall(() => {
     opts = opts || { };
     opts.secureContext = tls.createSecureContext({ ca });
@@ -72,7 +73,7 @@ function verifySecureSession(key, cert, ca, opts) {
       assert.strictEqual(jsonData.servername,
                          opts.servername || 'localhost');
       assert.strictEqual(jsonData.alpnProtocol, 'h2');
-      server.close();
+      server.close(common.mustCall());
       client[kSocket].destroy();
     }));
   }));
diff --git a/test/parallel/test-http2-create-client-session.js b/test/parallel/test-http2-create-client-session.js
index b5be6bc8581452..963db2faa173b7 100644
--- a/test/parallel/test-http2-create-client-session.js
+++ b/test/parallel/test-http2-create-client-session.js
@@ -29,6 +29,8 @@ function onStream(stream, headers, flags) {
   stream.end(body.slice(20));
 }
 
+server.on('close', common.mustCall());
+
 server.listen(0);
 
 server.on('listening', common.mustCall(() => {
@@ -46,7 +48,7 @@ server.on('listening', common.mustCall(() => {
 
   const countdown = new Countdown(count, () => {
     client.close();
-    server.close();
+    server.close(common.mustCall());
   });
 
   for (let n = 0; n < count; n++) {
diff --git a/test/parallel/test-http2-createwritereq.js b/test/parallel/test-http2-createwritereq.js
index 1d2b31676284d0..1575424d1609b4 100644
--- a/test/parallel/test-http2-createwritereq.js
+++ b/test/parallel/test-http2-createwritereq.js
@@ -60,7 +60,7 @@ server.listen(0, common.mustCall(function() {
       testsFinished++;
 
       if (testsFinished === testsToRun) {
-        server.close();
+        server.close(common.mustCall());
       }
     }));
 
diff --git a/test/parallel/test-http2-misbehaving-flow-control.js b/test/parallel/test-http2-misbehaving-flow-control.js
index 8a0b411b8de65c..161a88ea1fb407 100644
--- a/test/parallel/test-http2-misbehaving-flow-control.js
+++ b/test/parallel/test-http2-misbehaving-flow-control.js
@@ -72,7 +72,7 @@ server.on('stream', (stream) => {
     message: 'Stream closed with error code 3'
   }));
   stream.on('close', common.mustCall(() => {
-    server.close();
+    server.close(common.mustCall());
     client.destroy();
   }));
   stream.resume();
@@ -80,6 +80,8 @@ server.on('stream', (stream) => {
   stream.end('ok');
 });
 
+server.on('close', common.mustCall());
+
 server.listen(0, () => {
   client = net.connect(server.address().port, () => {
     client.write(preamble);

From 4d7549bbe73b1b6510ad569c1e4a83dd5b56dd75 Mon Sep 17 00:00:00 2001
From: Peter Dalgaard-Jensen <nephross.cortex@gmail.com>
Date: Fri, 19 Jan 2018 21:11:09 +0100
Subject: [PATCH 48/77] doc: fix documentation of http2Stream.pushstream()

Improve documentation of callback signature of
http2Stream.pushStream() function to align with
the changes made in https://github.com/nodejs/node/pull/17406.

PR-URL: https://github.com/nodejs/node/pull/18258
Fixes: https://github.com/nodejs/node/issues/18198
Refs: https://github.com/nodejs/node/pull/17406
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 doc/api/http2.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index cbdb8c2d9f258d..f7cae68ce4439b 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1199,6 +1199,10 @@ added: v8.4.0
     created stream is dependent on.
 * `callback` {Function} Callback that is called once the push stream has been
   initiated.
+  * `err` {Error}
+  * `pushStream` {[`ServerHttp2Stream`][]} The returned pushStream object.
+  * `headers` {[Headers Object][]} Headers object the pushStream was initiated
+  with.
 * Returns: {undefined}
 
 Initiates a push stream. The callback is invoked with the new `Http2Stream`
@@ -1210,7 +1214,7 @@ const http2 = require('http2');
 const server = http2.createServer();
 server.on('stream', (stream) => {
   stream.respond({ ':status': 200 });
-  stream.pushStream({ ':path': '/' }, (err, pushStream) => {
+  stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => {
     if (err) throw err;
     pushStream.respond({ ':status': 200 });
     pushStream.end('some pushed data');

From c8a6d8ae889a376d7a2671120fda1bc6c52d1299 Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Sat, 27 Jan 2018 04:35:29 +0200
Subject: [PATCH 49/77] doc: unify type linkification

PR-URL: https://github.com/nodejs/node/pull/18407
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Jon Moss <me@jonathanmoss.me>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
---
 doc/api/http2.md | 70 ++++++++++++++++++++++++------------------------
 1 file changed, 35 insertions(+), 35 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index f7cae68ce4439b..7c874b5b4870f1 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -229,8 +229,8 @@ added: v8.4.0
 
 The `'stream'` event is emitted when a new `Http2Stream` is created. When
 invoked, the handler function will receive a reference to the `Http2Stream`
-object, a [Headers Object][], and numeric flags associated with the creation
-of the stream.
+object, a [HTTP2 Headers Object][], and numeric flags associated with the
+creation of the stream.
 
 ```js
 const http2 = require('http2');
@@ -382,7 +382,7 @@ Transmits a `GOAWAY` frame to the connected peer *without* shutting down the
 added: v8.4.0
 -->
 
-* Value: {[Settings Object][]}
+* Value: {HTTP2 Settings Object}
 
 A prototype-less object describing the current local settings of this
 `Http2Session`. The local settings are local to *this* `Http2Session` instance.
@@ -461,7 +461,7 @@ instance's underlying [`net.Socket`].
 added: v8.4.0
 -->
 
-* Value: {[Settings Object][]}
+* Value: {HTTP2 Settings Object}
 
 A prototype-less object describing the current remote settings of this
 `Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
@@ -570,8 +570,8 @@ An object describing the current status of this `Http2Session`.
 added: v8.4.0
 -->
 
-* `settings` {[Settings Object][]}
-* Returns: {undefined}
+* `settings` {HTTP2 Settings Object}
+* Returns {undefined}
 
 Updates the current local settings for this `Http2Session` and sends a new
 `SETTINGS` frame to the connected HTTP/2 peer.
@@ -707,7 +707,7 @@ client.on('altsvc', (alt, origin, stream) => {
 added: v8.4.0
 -->
 
-* `headers` {[Headers Object][]}
+* `headers` {HTTP2 Headers Object}
 * `options` {Object}
   * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should
     be closed initially, such as when sending a `GET` request that should not
@@ -895,7 +895,7 @@ added: v8.4.0
 
 The `'trailers'` event is emitted when a block of headers associated with
 trailing header fields is received. The listener callback is passed the
-[Headers Object][] and flags associated with the headers.
+[HTTP2 Headers Object][] and flags associated with the headers.
 
 ```js
 stream.on('trailers', (headers, flags) => {
@@ -994,7 +994,7 @@ calling `http2stream.close()`, or `http2stream.destroy()`. Will be
 added: REPLACEME
 -->
 
-* Value: {[Headers Object][]}
+* Value: {HTTP2 Headers Object}
 
 An object containing the outbound headers sent for this `Http2Stream`.
 
@@ -1003,7 +1003,7 @@ An object containing the outbound headers sent for this `Http2Stream`.
 added: REPLACEME
 -->
 
-* Value: {[Headers Object][]\[\]}
+* Value: {HTTP2 Headers Object[]}
 
 An array of objects containing the outbound informational (additional) headers
 sent for this `Http2Stream`.
@@ -1013,7 +1013,7 @@ sent for this `Http2Stream`.
 added: REPLACEME
 -->
 
-* Value: {[Headers Object][]}
+* Value: {HTTP2 Headers Object}
 
 An object containing the outbound trailers sent for this this `HttpStream`.
 
@@ -1096,8 +1096,8 @@ added: v8.4.0
 
 The `'headers'` event is emitted when an additional block of headers is received
 for a stream, such as when a block of `1xx` informational headers is received.
-The listener callback is passed the [Headers Object][] and flags associated with
-the headers.
+The listener callback is passed the [HTTP2 Headers Object][] and flags
+associated with the headers.
 
 ```js
 stream.on('headers', (headers, flags) => {
@@ -1111,8 +1111,8 @@ added: v8.4.0
 -->
 
 The `'push'` event is emitted when response headers for a Server Push stream
-are received. The listener callback is passed the [Headers Object][] and flags
-associated with the headers.
+are received. The listener callback is passed the [HTTP2 Headers Object][] and
+flags associated with the headers.
 
 ```js
 stream.on('push', (headers, flags) => {
@@ -1128,7 +1128,7 @@ added: v8.4.0
 The `'response'` event is emitted when a response `HEADERS` frame has been
 received for this stream from the connected HTTP/2 server. The listener is
 invoked with two arguments: an Object containing the received
-[Headers Object][], and flags associated with the headers.
+[HTTP2 Headers Object][], and flags associated with the headers.
 
 For example:
 
@@ -1158,7 +1158,7 @@ provide additional methods such as `http2stream.pushStream()` and
 added: v8.4.0
 -->
 
-* `headers` {[Headers Object][]}
+* `headers` {HTTP2 Headers Object}
 * Returns: {undefined}
 
 Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer.
@@ -1189,7 +1189,7 @@ accepts push streams, `false` otherwise. Settings are the same for every
 added: v8.4.0
 -->
 
-* `headers` {[Headers Object][]}
+* `headers` {HTTP2 Headers Object}
 * `options` {Object}
   * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
     the created stream is made the sole direct dependency of the parent, with
@@ -1200,9 +1200,9 @@ added: v8.4.0
 * `callback` {Function} Callback that is called once the push stream has been
   initiated.
   * `err` {Error}
-  * `pushStream` {[`ServerHttp2Stream`][]} The returned pushStream object.
-  * `headers` {[Headers Object][]} Headers object the pushStream was initiated
-  with.
+  * `pushStream` {ServerHttp2Stream} The returned pushStream object.
+  * `headers` {HTTP2 Headers Object} Headers object the pushStream was
+  initiated with.
 * Returns: {undefined}
 
 Initiates a push stream. The callback is invoked with the new `Http2Stream`
@@ -1232,7 +1232,7 @@ a `weight` value to `http2stream.priority` with the `silent` option set to
 added: v8.4.0
 -->
 
-* `headers` {[Headers Object][]}
+* `headers` {HTTP2 Headers Object}
 * `options` {Object}
   * `endStream` {boolean} Set to `true` to indicate that the response will not
     include payload data.
@@ -1278,7 +1278,7 @@ added: v8.4.0
 -->
 
 * `fd` {number} A readable file descriptor.
-* `headers` {[Headers Object][]}
+* `headers` {HTTP2 Headers Object}
 * `options` {Object}
   * `statCheck` {Function}
   * `getTrailers` {Function} Callback function invoked to collect trailer
@@ -1362,7 +1362,7 @@ added: v8.4.0
 -->
 
 * `path` {string|Buffer|URL}
-* `headers` {[Headers Object][]}
+* `headers` {HTTP2 Headers Object}
 * `options` {Object}
   * `statCheck` {Function}
   * `onError` {Function} Callback function invoked in the case of an
@@ -1728,7 +1728,7 @@ changes:
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].
-  * `settings` {[Settings Object][]} The initial settings to send to the
+  * `settings` {HTTP2 Settings Object} The initial settings to send to the
     remote peer upon connection.
 * `onRequestHandler` {Function} See [Compatibility API][]
 * Returns: {Http2Server}
@@ -1815,7 +1815,7 @@ changes:
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].
-  * `settings` {[Settings Object][]} The initial settings to send to the
+  * `settings` {HTTP2 Settings Object} The initial settings to send to the
     remote peer upon connection.
   * ...: Any [`tls.createServer()`][] options can be provided. For
     servers, the identity options (`pfx` or `key`/`cert`) are usually required.
@@ -1911,7 +1911,7 @@ changes:
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].
-  * `settings` {[Settings Object][]} The initial settings to send to the
+  * `settings` {HTTP2 Settings Object} The initial settings to send to the
     remote peer upon connection.
   * `createConnection` {Function} An optional callback that receives the `URL`
     instance passed to `connect` and the `options` object, and returns any
@@ -1964,7 +1964,7 @@ a given number of milliseconds set using `http2server.setTimeout()`.
 added: v8.4.0
 -->
 
-* Returns: {[Settings Object][]}
+* Returns: {HTTP2 Settings Object}
 
 Returns an object containing the default settings for an `Http2Session`
 instance. This method returns a new object instance every time it is called
@@ -1975,7 +1975,7 @@ so instances returned may be safely modified for use.
 added: v8.4.0
 -->
 
-* `settings` {[Settings Object][]}
+* `settings` {HTTP2 Settings Object}
 * Returns: {Buffer}
 
 Returns a `Buffer` instance containing serialized representation of the given
@@ -1997,10 +1997,10 @@ added: v8.4.0
 -->
 
 * `buf` {Buffer|Uint8Array} The packed settings.
-* Returns: {[Settings Object][]}
+* Returns: {HTTP2 Settings Object}
 
-Returns a [Settings Object][] containing the deserialized settings from the
-given `Buffer` as generated by `http2.getPackedSettings()`.
+Returns a [HTTP2 Settings Object][] containing the deserialized settings from
+the given `Buffer` as generated by `http2.getPackedSettings()`.
 
 ### Headers Object
 
@@ -2372,7 +2372,7 @@ Example:
 console.log(request.headers);
 ```
 
-See [Headers Object][].
+See [HTTP2 Headers Object][].
 
 *Note*: In HTTP/2, the request path, hostname, protocol, and method are
 represented as special headers prefixed with the `:` character (e.g. `':path'`).
@@ -3094,13 +3094,13 @@ following additional properties:
 [Compatibility API]: #http2_compatibility_api
 [HTTP/1]: http.html
 [HTTP/2]: https://tools.ietf.org/html/rfc7540
+[HTTP2 Headers Object]: #http2_headers_object
+[HTTP2 Settings Object]: #http2_settings_object
 [HTTPS]: https.html
-[Headers Object]: #http2_headers_object
 [Http2Session and Sockets]: #http2_http2session_and_sockets
 [Performance Observer]: perf_hooks.html
 [Readable Stream]: stream.html#stream_class_stream_readable
 [RFC 7838]: https://tools.ietf.org/html/rfc7838
-[Settings Object]: #http2_settings_object
 [Using options.selectPadding]: #http2_using_options_selectpadding
 [Writable Stream]: stream.html#stream_writable_streams
 [`'checkContinue'`]: #http2_event_checkcontinue

From bcfef86dfc71940847a7d22a3cbbf1e615f49b1a Mon Sep 17 00:00:00 2001
From: Kelvin Jin <kelvinjin@google.com>
Date: Mon, 29 Jan 2018 10:58:29 -0800
Subject: [PATCH 50/77] doc: remove removed apis from http2 docs

PR-URL: https://github.com/nodejs/node/pull/18439
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 doc/api/http2.md | 144 +++++++++++++++++++----------------------------
 1 file changed, 59 insertions(+), 85 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 7c874b5b4870f1..8190eab7faa6d8 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -19,8 +19,8 @@ compatibility with the existing [HTTP/1][] module API. However,
 the [Compatibility API][] is.
 
 The `http2` Core API is much more symmetric between client and server than the
-`http` API. For instance, most events, like `error` and `socketError`, can be
-emitted either by client-side code or server-side code.
+`http` API. For instance, most events, like `error`, `connect` and `stream`, can
+be emitted either by client-side code or server-side code.
 
 ### Server-side example
 
@@ -36,7 +36,6 @@ const server = http2.createSecureServer({
   cert: fs.readFileSync('localhost-cert.pem')
 });
 server.on('error', (err) => console.error(err));
-server.on('socketError', (err) => console.error(err));
 
 server.on('stream', (stream, headers) => {
   // stream is a Duplex
@@ -68,7 +67,6 @@ const client = http2.connect('https://localhost:8443', {
   ca: fs.readFileSync('localhost-cert.pem')
 });
 client.on('error', (err) => console.error(err));
-client.on('socketError', (err) => console.error(err));
 
 const req = client.request({ ':path': '/' });
 
@@ -479,44 +477,6 @@ Used to set a callback function that is called when there is no activity on
 the `Http2Session` after `msecs` milliseconds. The given `callback` is
 registered as a listener on the `'timeout'` event.
 
-#### http2session.close(options[, callback])
-<!-- YAML
-added: v8.4.0
--->
-
-* `options` {Object}
-  * `errorCode` {number} The HTTP/2 [error code][] to return. Note that this is
-    *not* the same thing as an HTTP Response Status Code. **Default:** `0x00`
-    (No Error).
-  * `lastStreamID` {number} The Stream ID of the last successfully processed
-    `Http2Stream` on this `Http2Session`. If unspecified, will default to the
-    ID of the most recently received stream.
-  * `opaqueData` {Buffer|Uint8Array} A `Buffer` or `Uint8Array` instance
-    containing arbitrary additional data to send to the peer upon disconnection.
-    This is used, typically, to provide additional data for debugging failures,
-    if necessary.
-* `callback` {Function} A callback that is invoked after the session shutdown
-  has been completed.
-* Returns: {undefined}
-
-Attempts to shut down this `Http2Session` using HTTP/2 defined procedures.
-If specified, the given `callback` function will be invoked once the shutdown
-process has completed.
-
-If the `Http2Session` instance is a server-side session and the `errorCode`
-option is `0x00` (No Error), a "graceful" shutdown will be initiated. During a
-"graceful" shutdown, the session will first send a `GOAWAY` frame to
-the connected peer identifying the last processed stream as 2<sup>31</sup>-1.
-Then, on the next tick of the event loop, a second `GOAWAY` frame identifying
-the most recently processed stream identifier is sent. This process allows the
-remote peer to begin preparing for the connection to be terminated.
-
-```js
-session.close({
-  opaqueData: Buffer.from('add some debugging data here')
-}, () => session.destroy());
-```
-
 #### http2session.socket
 <!-- YAML
 added: v8.4.0
@@ -686,19 +646,23 @@ added: v8.4.0
 added: REPLACEME
 -->
 
+* `alt`: {string}
+* `origin`: {string}
+* `streamId`: {number}
+
 The `'altsvc'` event is emitted whenever an `ALTSVC` frame is received by
 the client. The event is emitted with the `ALTSVC` value, origin, and stream
-ID, if any. If no `origin` is provided in the `ALTSVC` frame, `origin` will
+ID. If no `origin` is provided in the `ALTSVC` frame, `origin` will
 be an empty string.
 
 ```js
 const http2 = require('http2');
 const client = http2.connect('https://example.org');
 
-client.on('altsvc', (alt, origin, stream) => {
+client.on('altsvc', (alt, origin, streamId) => {
   console.log(alt);
   console.log(origin);
-  console.log(stream);
+  console.log(streamId);
 });
 ```
 
@@ -1472,10 +1436,9 @@ added: v8.4.0
 
 * Extends: {net.Server}
 
-In `Http2Server`, there is no `'clientError'` event as there is in
-HTTP1. However, there are `'socketError'`, `'sessionError'`,  and
-`'streamError'`, for errors emitted on the socket, `Http2Session`, or
-`Http2Stream`.
+In `Http2Server`, there are no `'clientError'` events as there are in
+HTTP1. However, there are `'sessionError'`,  and `'streamError'` events for
+errors emitted on the socket, or from `Http2Session` or `Http2Stream` instances.
 
 #### Event: 'checkContinue'
 <!-- YAML
@@ -1580,23 +1543,54 @@ added: v8.4.0
 
 * Extends: {tls.Server}
 
-#### Event: 'sessionError'
+#### Event: 'checkContinue'
+<!-- YAML
+added: v8.5.0
+-->
+
+* `request` {http2.Http2ServerRequest}
+* `response` {http2.Http2ServerResponse}
+
+If a [`'request'`][] listener is registered or [`http2.createSecureServer()`][]
+is supplied a callback function, the `'checkContinue'` event is emitted each
+time a request with an HTTP `Expect: 100-continue` is received. If this event
+is not listened for, the server will automatically respond with a status
+`100 Continue` as appropriate.
+
+Handling this event involves calling [`response.writeContinue()`][] if the client
+should continue to send the request body, or generating an appropriate HTTP
+response (e.g. 400 Bad Request) if the client should not continue to send the
+request body.
+
+Note that when this event is emitted and handled, the [`'request'`][] event will
+not be emitted.
+
+#### Event: 'request'
 <!-- YAML
 added: v8.4.0
 -->
 
-The `'sessionError'` event is emitted when an `'error'` event is emitted by
-an `Http2Session` object associated with the `Http2SecureServer`.
+* `request` {http2.Http2ServerRequest}
+* `response` {http2.Http2ServerResponse}
 
-#### Event: 'unknownProtocol'
+Emitted each time there is a request. Note that there may be multiple requests
+per session. See the [Compatibility API][].
+
+#### Event: 'session'
 <!-- YAML
 added: v8.4.0
 -->
 
-The `'unknownProtocol'` event is emitted when a connecting client fails to
-negotiate an allowed protocol (i.e. HTTP/2 or HTTP/1.1). The event handler
-receives the socket for handling. If no listener is registered for this event,
-the connection is terminated. See the [Compatibility API][].
+The `'session'` event is emitted when a new `Http2Session` is created by the
+`Http2SecureServer`.
+
+#### Event: 'sessionError'
+<!-- YAML
+added: v8.4.0
+-->
+
+The `'sessionError'` event is emitted when an `'error'` event is emitted by
+an `Http2Session` object associated with the `Http2SecureServer`.
 
 #### Event: 'stream'
 <!-- YAML
@@ -1631,43 +1625,23 @@ server.on('stream', (stream, headers, flags) => {
 });
 ```
 
-#### Event: 'request'
+#### Event: 'timeout'
 <!-- YAML
 added: v8.4.0
 -->
 
-* `request` {http2.Http2ServerRequest}
-* `response` {http2.Http2ServerResponse}
-
-Emitted each time there is a request. Note that there may be multiple requests
-per session. See the [Compatibility API][].
+The `'timeout'` event is emitted when there is no activity on the Server for
+a given number of milliseconds set using `http2secureServer.setTimeout()`.
 
-#### Event: 'timeout'
+#### Event: 'unknownProtocol'
 <!-- YAML
 added: v8.4.0
 -->
 
-#### Event: 'checkContinue'
-<!-- YAML
-added: v8.5.0
--->
-
-* `request` {http2.Http2ServerRequest}
-* `response` {http2.Http2ServerResponse}
-
-If a [`'request'`][] listener is registered or [`http2.createSecureServer()`][]
-is supplied a callback function, the `'checkContinue'` event is emitted each
-time a request with an HTTP `Expect: 100-continue` is received. If this event
-is not listened for, the server will automatically respond with a status
-`100 Continue` as appropriate.
-
-Handling this event involves calling [`response.writeContinue()`][] if the client
-should continue to send the request body, or generating an appropriate HTTP
-response (e.g. 400 Bad Request) if the client should not continue to send the
-request body.
-
-Note that when this event is emitted and handled, the [`'request'`][] event will
-not be emitted.
+The `'unknownProtocol'` event is emitted when a connecting client fails to
+negotiate an allowed protocol (i.e. HTTP/2 or HTTP/1.1). The event handler
+receives the socket for handling. If no listener is registered for this event,
+the connection is terminated. See the [Compatibility API][].
 
 ### http2.createServer(options[, onRequestHandler])
 <!-- YAML

From b83c03e8e49e64c4948fe6a383feb36b7574ffff Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Tue, 6 Feb 2018 18:38:31 +0200
Subject: [PATCH 51/77] doc: fix typo in http2.md

PR-URL: https://github.com/nodejs/node/pull/18602
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
---
 doc/api/http2.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 8190eab7faa6d8..3fac9f17ccf216 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1697,8 +1697,8 @@ changes:
        and the total frame length will *not* necessarily be aligned at 8 bytes.
   * `peerMaxConcurrentStreams` {number} Sets the maximum number of concurrent
     streams for the remote peer as if a SETTINGS frame had been received. Will
-    be overridden if the remote peer sets its own value for.
-    `maxConcurrentStreams`. **Default** `100`
+    be overridden if the remote peer sets its own value for
+    `maxConcurrentStreams`. **Default:** `100`
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].

From 7e26992b4852e5cf50fb4a8ba023cafa181323da Mon Sep 17 00:00:00 2001
From: Peter Marton <email@martonpeter.com>
Date: Fri, 27 Oct 2017 09:18:59 +0200
Subject: [PATCH 52/77] http2: add http fallback options to .createServer

This adds the Http1IncomingMessage and Http1ServerReponse options
to http2.createServer().

PR-URL: https://github.com/nodejs/node/pull/15752
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Evan Lucas <evanlucas@me.com>
---
 doc/api/http2.md                              | 10 +++
 lib/internal/http2/core.js                    | 15 +++-
 ...ttp2-https-fallback-http-server-options.js | 90 +++++++++++++++++++
 3 files changed, 114 insertions(+), 1 deletion(-)
 create mode 100644 test/parallel/test-http2-https-fallback-http-server-options.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 3fac9f17ccf216..2478e84ece60ea 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1655,6 +1655,10 @@ changes:
     pr-url: https://github.com/nodejs/node/pull/16676
     description: Added the `maxHeaderListPairs` option with a default limit of
                  128 header pairs.
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/15752
+    description: Added the `Http1IncomingMessage` and `Http1ServerResponse`
+                 option.
 -->
 
 * `options` {Object}
@@ -1704,6 +1708,12 @@ changes:
     used to determine the padding. See [Using options.selectPadding][].
   * `settings` {HTTP2 Settings Object} The initial settings to send to the
     remote peer upon connection.
+  * `Http1IncomingMessage` {http.IncomingMessage} Specifies the IncomingMessage
+    class to used for HTTP/1 fallback. Useful for extending the original
+    `http.IncomingMessage`. **Default:** `http.IncomingMessage`
+  * `Http1ServerResponse` {http.ServerResponse} Specifies the ServerResponse
+    class to used for HTTP/1 fallback. Useful for extending the original
+    `http.ServerResponse`. **Default:** `http.ServerResponse`
 * `onRequestHandler` {Function} See [Compatibility API][]
 * Returns: {Http2Server}
 
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index bc1067e9963ba1..d6694a8428d490 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -5,6 +5,7 @@
 require('internal/util').assertCrypto();
 
 const { async_id_symbol } = process.binding('async_wrap');
+const http = require('http');
 const binding = process.binding('http2');
 const assert = require('assert');
 const { Buffer } = require('buffer');
@@ -67,6 +68,8 @@ const NETServer = net.Server;
 const TLSServer = tls.Server;
 
 const kInspect = require('internal/util').customInspectSymbol;
+const { kIncomingMessage } = require('_http_common');
+const { kServerResponse } = require('_http_server');
 
 const kAlpnProtocol = Symbol('alpnProtocol');
 const kAuthority = Symbol('authority');
@@ -2442,8 +2445,11 @@ function connectionListener(socket) {
 
   if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
     // Fallback to HTTP/1.1
-    if (options.allowHTTP1 === true)
+    if (options.allowHTTP1 === true) {
+      socket.server[kIncomingMessage] = options.Http1IncomingMessage;
+      socket.server[kServerResponse] = options.Http1ServerResponse;
       return httpConnectionListener.call(this, socket);
+    }
     // Let event handler deal with the socket
     if (!this.emit('unknownProtocol', socket))
       socket.destroy();
@@ -2474,6 +2480,13 @@ function initializeOptions(options) {
   options.allowHalfOpen = true;
   assertIsObject(options.settings, 'options.settings');
   options.settings = Object.assign({}, options.settings);
+
+  // Used only with allowHTTP1
+  options.Http1IncomingMessage = options.Http1IncomingMessage ||
+    http.IncomingMessage;
+  options.Http1ServerResponse = options.Http1ServerResponse ||
+    http.ServerResponse;
+
   return options;
 }
 
diff --git a/test/parallel/test-http2-https-fallback-http-server-options.js b/test/parallel/test-http2-https-fallback-http-server-options.js
new file mode 100644
index 00000000000000..20e2b122a24e8c
--- /dev/null
+++ b/test/parallel/test-http2-https-fallback-http-server-options.js
@@ -0,0 +1,90 @@
+// Flags: --expose-http2
+'use strict';
+
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const url = require('url');
+const tls = require('tls');
+const http2 = require('http2');
+const https = require('https');
+const http = require('http');
+
+const key = fixtures.readKey('agent8-key.pem');
+const cert = fixtures.readKey('agent8-cert.pem');
+const ca = fixtures.readKey('fake-startcom-root-cert.pem');
+
+function onRequest(request, response) {
+  const { socket: { alpnProtocol } } = request.httpVersion === '2.0' ?
+    request.stream.session : request;
+  response.status(200);
+  response.end(JSON.stringify({
+    alpnProtocol,
+    httpVersion: request.httpVersion,
+    userAgent: request.getUserAgent()
+  }));
+}
+
+class MyIncomingMessage extends http.IncomingMessage {
+  getUserAgent() {
+    return this.headers['user-agent'] || 'unknown';
+  }
+}
+
+class MyServerResponse extends http.ServerResponse {
+  status(code) {
+    return this.writeHead(code, { 'Content-Type': 'application/json' });
+  }
+}
+
+// HTTP/2 & HTTP/1.1 server
+{
+  const server = http2.createSecureServer(
+    {
+      cert,
+      key, allowHTTP1: true,
+      Http1IncomingMessage: MyIncomingMessage,
+      Http1ServerResponse: MyServerResponse
+    },
+    common.mustCall(onRequest, 1)
+  );
+
+  server.listen(0);
+
+  server.on('listening', common.mustCall(() => {
+    const { port } = server.address();
+    const origin = `https://localhost:${port}`;
+
+    // HTTP/1.1 client
+    https.get(
+      Object.assign(url.parse(origin), {
+        secureContext: tls.createSecureContext({ ca }),
+        headers: { 'User-Agent': 'node-test' }
+      }),
+      common.mustCall((response) => {
+        assert.strictEqual(response.statusCode, 200);
+        assert.strictEqual(response.statusMessage, 'OK');
+        assert.strictEqual(
+          response.headers['content-type'],
+          'application/json'
+        );
+
+        response.setEncoding('utf8');
+        let raw = '';
+        response.on('data', (chunk) => { raw += chunk; });
+        response.on('end', common.mustCall(() => {
+          const { alpnProtocol, httpVersion, userAgent } = JSON.parse(raw);
+          assert.strictEqual(alpnProtocol, false);
+          assert.strictEqual(httpVersion, '1.1');
+          assert.strictEqual(userAgent, 'node-test');
+
+          server.close();
+        }));
+      })
+    );
+  }));
+}

From 740d445f47db4d6d1b2172ac2c7c25e6a933c012 Mon Sep 17 00:00:00 2001
From: Peter Marton <peter@risingstack.com>
Date: Thu, 19 Oct 2017 20:16:02 +0200
Subject: [PATCH 53/77] http: add options to http.createServer()

This adds the optional options argument to `http.createServer()`.
It contains two options: the `IncomingMessage` and `ServerReponse`
option.

PR-URL: https://github.com/nodejs/node/pull/15752
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Evan Lucas <evanlucas@me.com>
---
 doc/api/http.md                               | 15 +++++-
 doc/api/https.md                              |  4 +-
 lib/_http_common.js                           | 10 +++-
 lib/_http_server.js                           | 23 +++++++--
 lib/https.js                                  |  8 ++-
 ...st-http-server-options-incoming-message.js | 41 +++++++++++++++
 ...est-http-server-options-server-response.js | 35 +++++++++++++
 ...t-https-server-options-incoming-message.js | 51 +++++++++++++++++++
 ...st-https-server-options-server-response.js | 47 +++++++++++++++++
 9 files changed, 223 insertions(+), 11 deletions(-)
 create mode 100644 test/parallel/test-http-server-options-incoming-message.js
 create mode 100644 test/parallel/test-http-server-options-server-response.js
 create mode 100644 test/parallel/test-https-server-options-incoming-message.js
 create mode 100644 test/parallel/test-https-server-options-server-response.js

diff --git a/doc/api/http.md b/doc/api/http.md
index 7331d8bc5d969d..284f35c68b442b 100644
--- a/doc/api/http.md
+++ b/doc/api/http.md
@@ -1663,10 +1663,21 @@ A collection of all the standard HTTP response status codes, and the
 short description of each. For example, `http.STATUS_CODES[404] === 'Not
 Found'`.
 
-## http.createServer([requestListener])
+## http.createServer([options][, requestListener])
 <!-- YAML
 added: v0.1.13
--->
+changes:
+  - version: REPLACEME
+    pr-url: https://github.com/nodejs/node/pull/15752
+    description: The `options` argument is supported now.
+-->
+- `options` {Object}
+  * `IncomingMessage` {http.IncomingMessage} Specifies the IncomingMessage class
+    to be used. Useful for extending the original `IncomingMessage`. Defaults
+    to: `IncomingMessage`
+  * `ServerResponse` {http.ServerResponse} Specifies the ServerResponse class to
+    be used. Useful for extending the original `ServerResponse`. Defaults to:
+    `ServerResponse`
 - `requestListener` {Function}
 
 * Returns: {http.Server}
diff --git a/doc/api/https.md b/doc/api/https.md
index daf10ac4a2bb94..490c3f477f9d41 100644
--- a/doc/api/https.md
+++ b/doc/api/https.md
@@ -65,7 +65,8 @@ See [`http.Server#keepAliveTimeout`][].
 <!-- YAML
 added: v0.3.4
 -->
-- `options` {Object} Accepts `options` from [`tls.createServer()`][] and [`tls.createSecureContext()`][].
+- `options` {Object} Accepts `options` from [`tls.createServer()`][],
+ [`tls.createSecureContext()`][] and [`http.createServer()`][].
 - `requestListener` {Function} A listener to be added to the `request` event.
 
 Example:
@@ -255,6 +256,7 @@ const req = https.request(options, (res) => {
 [`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
 [`http.Server#timeout`]: http.html#http_server_timeout
 [`http.Server`]: http.html#http_class_http_server
+[`http.createServer()`]: http.html#httpcreateserveroptions-requestlistener
 [`http.close()`]: http.html#http_server_close_callback
 [`http.get()`]: http.html#http_http_get_options_callback
 [`http.request()`]: http.html#http_http_request_options_callback
diff --git a/lib/_http_common.js b/lib/_http_common.js
index 381ffeb807a84e..9f8f7f524b8450 100644
--- a/lib/_http_common.js
+++ b/lib/_http_common.js
@@ -34,6 +34,7 @@ const {
 
 const debug = require('util').debuglog('http');
 
+const kIncomingMessage = Symbol('IncomingMessage');
 const kOnHeaders = HTTPParser.kOnHeaders | 0;
 const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
 const kOnBody = HTTPParser.kOnBody | 0;
@@ -73,7 +74,11 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
     parser._url = '';
   }
 
-  parser.incoming = new IncomingMessage(parser.socket);
+  // Parser is also used by http client
+  var ParserIncomingMessage = parser.socket && parser.socket.server ?
+    parser.socket.server[kIncomingMessage] : IncomingMessage;
+
+  parser.incoming = new ParserIncomingMessage(parser.socket);
   parser.incoming.httpVersionMajor = versionMajor;
   parser.incoming.httpVersionMinor = versionMinor;
   parser.incoming.httpVersion = `${versionMajor}.${versionMinor}`;
@@ -353,5 +358,6 @@ module.exports = {
   freeParser,
   httpSocketSetup,
   methods,
-  parsers
+  parsers,
+  kIncomingMessage
 };
diff --git a/lib/_http_server.js b/lib/_http_server.js
index be591c437ca083..78da88ba1eed47 100644
--- a/lib/_http_server.js
+++ b/lib/_http_server.js
@@ -33,6 +33,7 @@ const {
   continueExpression,
   chunkExpression,
   httpSocketSetup,
+  kIncomingMessage,
   _checkInvalidHeaderChar: checkInvalidHeaderChar
 } = require('_http_common');
 const { OutgoingMessage } = require('_http_outgoing');
@@ -41,6 +42,9 @@ const {
   defaultTriggerAsyncIdScope,
   getOrSetAsyncId
 } = require('internal/async_hooks');
+const { IncomingMessage } = require('_http_incoming');
+
+const kServerResponse = Symbol('ServerResponse');
 
 const STATUS_CODES = {
   100: 'Continue',
@@ -260,9 +264,19 @@ function writeHead(statusCode, reason, obj) {
 // Docs-only deprecated: DEP0063
 ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead;
 
+function Server(options, requestListener) {
+  if (!(this instanceof Server)) return new Server(options, requestListener);
+
+  if (typeof options === 'function') {
+    requestListener = options;
+    options = {};
+  } else if (options == null || typeof options === 'object') {
+    options = util._extend({}, options);
+  }
+
+  this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
+  this[kServerResponse] = options.ServerResponse || ServerResponse;
 
-function Server(requestListener) {
-  if (!(this instanceof Server)) return new Server(requestListener);
   net.Server.call(this, { allowHalfOpen: true });
 
   if (requestListener) {
@@ -578,7 +592,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
     }
   }
 
-  var res = new ServerResponse(req);
+  var res = new server[kServerResponse](req);
   res._onPendingData = updateOutgoingData.bind(undefined, socket, state);
 
   res.shouldKeepAlive = keepAlive;
@@ -681,5 +695,6 @@ module.exports = {
   STATUS_CODES,
   Server,
   ServerResponse,
-  _connectionListener: connectionListener
+  _connectionListener: connectionListener,
+  kServerResponse
 };
diff --git a/lib/https.js b/lib/https.js
index 6fcd9f65ce7858..a2aea08ac9cbe1 100644
--- a/lib/https.js
+++ b/lib/https.js
@@ -30,6 +30,9 @@ const util = require('util');
 const { inherits } = util;
 const debug = util.debuglog('https');
 const { urlToOptions, searchParamsSymbol } = require('internal/url');
+const { IncomingMessage, ServerResponse } = require('http');
+const { kIncomingMessage } = require('_http_common');
+const { kServerResponse } = require('_http_server');
 
 function Server(opts, requestListener) {
   if (!(this instanceof Server)) return new Server(opts, requestListener);
@@ -51,9 +54,10 @@ function Server(opts, requestListener) {
     opts.ALPNProtocols = ['http/1.1'];
   }
 
-  tls.Server.call(this, opts, http._connectionListener);
+  this[kIncomingMessage] = opts.IncomingMessage || IncomingMessage;
+  this[kServerResponse] = opts.ServerResponse || ServerResponse;
 
-  this.httpAllowHalfOpen = false;
+  tls.Server.call(this, opts, http._connectionListener);
 
   if (requestListener) {
     this.addListener('request', requestListener);
diff --git a/test/parallel/test-http-server-options-incoming-message.js b/test/parallel/test-http-server-options-incoming-message.js
new file mode 100644
index 00000000000000..a4bfa1b7646fc6
--- /dev/null
+++ b/test/parallel/test-http-server-options-incoming-message.js
@@ -0,0 +1,41 @@
+'use strict';
+
+/**
+ * This test covers http.Server({ IncomingMessage }) option:
+ * With IncomingMessage option the server should use
+ * the new class for creating req Object instead of the default
+ * http.IncomingMessage.
+ */
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+
+class MyIncomingMessage extends http.IncomingMessage {
+  getUserAgent() {
+    return this.headers['user-agent'] || 'unknown';
+  }
+}
+
+const server = http.Server({
+  IncomingMessage: MyIncomingMessage
+}, common.mustCall(function(req, res) {
+  assert.strictEqual(req.getUserAgent(), 'node-test');
+  res.statusCode = 200;
+  res.end();
+}));
+server.listen();
+
+server.on('listening', function makeRequest() {
+  http.get({
+    port: this.address().port,
+    headers: {
+      'User-Agent': 'node-test'
+    }
+  }, (res) => {
+    assert.strictEqual(res.statusCode, 200);
+    res.on('end', () => {
+      server.close();
+    });
+    res.resume();
+  });
+});
diff --git a/test/parallel/test-http-server-options-server-response.js b/test/parallel/test-http-server-options-server-response.js
new file mode 100644
index 00000000000000..f5adf39bed6d16
--- /dev/null
+++ b/test/parallel/test-http-server-options-server-response.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * This test covers http.Server({ ServerResponse }) option:
+ * With ServerResponse option the server should use
+ * the new class for creating res Object instead of the default
+ * http.ServerResponse.
+ */
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+
+class MyServerResponse extends http.ServerResponse {
+  status(code) {
+    return this.writeHead(code, { 'Content-Type': 'text/plain' });
+  }
+}
+
+const server = http.Server({
+  ServerResponse: MyServerResponse
+}, common.mustCall(function(req, res) {
+  res.status(200);
+  res.end();
+}));
+server.listen();
+
+server.on('listening', function makeRequest() {
+  http.get({ port: this.address().port }, (res) => {
+    assert.strictEqual(res.statusCode, 200);
+    res.on('end', () => {
+      server.close();
+    });
+    res.resume();
+  });
+});
diff --git a/test/parallel/test-https-server-options-incoming-message.js b/test/parallel/test-https-server-options-incoming-message.js
new file mode 100644
index 00000000000000..102ee56751b800
--- /dev/null
+++ b/test/parallel/test-https-server-options-incoming-message.js
@@ -0,0 +1,51 @@
+'use strict';
+
+/**
+ * This test covers http.Server({ IncomingMessage }) option:
+ * With IncomingMessage option the server should use
+ * the new class for creating req Object instead of the default
+ * http.IncomingMessage.
+ */
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const http = require('http');
+const https = require('https');
+
+class MyIncomingMessage extends http.IncomingMessage {
+  getUserAgent() {
+    return this.headers['user-agent'] || 'unknown';
+  }
+}
+
+const server = https.createServer({
+  key: fixtures.readKey('agent1-key.pem'),
+  cert: fixtures.readKey('agent1-cert.pem'),
+  ca: fixtures.readKey('ca1-cert.pem'),
+  IncomingMessage: MyIncomingMessage
+}, common.mustCall(function(req, res) {
+  assert.strictEqual(req.getUserAgent(), 'node-test');
+  res.statusCode = 200;
+  res.end();
+}));
+server.listen();
+
+server.on('listening', function makeRequest() {
+  https.get({
+    port: this.address().port,
+    rejectUnauthorized: false,
+    headers: {
+      'User-Agent': 'node-test'
+    }
+  }, (res) => {
+    assert.strictEqual(res.statusCode, 200);
+    res.on('end', () => {
+      server.close();
+    });
+    res.resume();
+  });
+});
diff --git a/test/parallel/test-https-server-options-server-response.js b/test/parallel/test-https-server-options-server-response.js
new file mode 100644
index 00000000000000..8745415f8b6596
--- /dev/null
+++ b/test/parallel/test-https-server-options-server-response.js
@@ -0,0 +1,47 @@
+'use strict';
+
+/**
+ * This test covers http.Server({ ServerResponse }) option:
+ * With ServerResponse option the server should use
+ * the new class for creating res Object instead of the default
+ * http.ServerResponse.
+ */
+const common = require('../common');
+const fixtures = require('../common/fixtures');
+
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const http = require('http');
+const https = require('https');
+
+class MyServerResponse extends http.ServerResponse {
+  status(code) {
+    return this.writeHead(code, { 'Content-Type': 'text/plain' });
+  }
+}
+
+const server = https.createServer({
+  key: fixtures.readKey('agent1-key.pem'),
+  cert: fixtures.readKey('agent1-cert.pem'),
+  ca: fixtures.readKey('ca1-cert.pem'),
+  ServerResponse: MyServerResponse
+}, common.mustCall(function(req, res) {
+  res.status(200);
+  res.end();
+}));
+server.listen();
+
+server.on('listening', function makeRequest() {
+  https.get({
+    port: this.address().port,
+    rejectUnauthorized: false
+  }, (res) => {
+    assert.strictEqual(res.statusCode, 200);
+    res.on('end', () => {
+      server.close();
+    });
+    res.resume();
+  });
+});

From 7e2181a4ebf8699caf1312f4b57c95a935924d9e Mon Sep 17 00:00:00 2001
From: Peter Marton <peter@risingstack.com>
Date: Thu, 5 Oct 2017 14:24:12 +0200
Subject: [PATCH 54/77] http2: add req and res options to server creation

Add optional Http2ServerRequest and Http2ServerResponse options
to createServer and createSecureServer. Allows custom req & res
classes that extend the default ones to be used without
overriding the prototype.

PR-URL: https://github.com/nodejs/node/pull/15560
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 doc/api/http2.md                              |  8 ++++
 lib/internal/http2/compat.js                  |  8 ++--
 lib/internal/http2/core.js                    | 10 ++++-
 .../test-http2-options-server-request.js      | 40 +++++++++++++++++++
 .../test-http2-options-server-response.js     | 34 ++++++++++++++++
 5 files changed, 95 insertions(+), 5 deletions(-)
 create mode 100644 test/parallel/test-http2-options-server-request.js
 create mode 100644 test/parallel/test-http2-options-server-response.js

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 2478e84ece60ea..f0468f7e6dc406 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1714,6 +1714,14 @@ changes:
   * `Http1ServerResponse` {http.ServerResponse} Specifies the ServerResponse
     class to used for HTTP/1 fallback. Useful for extending the original
     `http.ServerResponse`. **Default:** `http.ServerResponse`
+  * `Http2ServerRequest` {http2.Http2ServerRequest} Specifies the
+    Http2ServerRequest class to use.
+    Useful for extending the original `Http2ServerRequest`.
+    **Default:** `Http2ServerRequest`
+  * `Http2ServerResponse` {htt2.Http2ServerResponse} Specifies the
+    Http2ServerResponse class to use.
+    Useful for extending the original `Http2ServerResponse`.
+    **Default:** `Http2ServerResponse`
 * `onRequestHandler` {Function} See [Compatibility API][]
 * Returns: {Http2Server}
 
diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js
index b5dd81c80f4038..5e6c51377e94ba 100644
--- a/lib/internal/http2/compat.js
+++ b/lib/internal/http2/compat.js
@@ -661,11 +661,11 @@ class Http2ServerResponse extends Stream {
   }
 }
 
-function onServerStream(stream, headers, flags, rawHeaders) {
+function onServerStream(ServerRequest, ServerResponse,
+                        stream, headers, flags, rawHeaders) {
   const server = this;
-  const request = new Http2ServerRequest(stream, headers, undefined,
-                                         rawHeaders);
-  const response = new Http2ServerResponse(stream);
+  const request = new ServerRequest(stream, headers, undefined, rawHeaders);
+  const response = new ServerResponse(stream);
 
   // Check for the CONNECT method
   const method = headers[HTTP2_HEADER_METHOD];
diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index d6694a8428d490..8edb41851d521c 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -2487,6 +2487,10 @@ function initializeOptions(options) {
   options.Http1ServerResponse = options.Http1ServerResponse ||
     http.ServerResponse;
 
+  options.Http2ServerRequest = options.Http2ServerRequest ||
+                                       Http2ServerRequest;
+  options.Http2ServerResponse = options.Http2ServerResponse ||
+                                        Http2ServerResponse;
   return options;
 }
 
@@ -2552,7 +2556,11 @@ class Http2Server extends NETServer {
 function setupCompat(ev) {
   if (ev === 'request') {
     this.removeListener('newListener', setupCompat);
-    this.on('stream', onServerStream);
+    this.on('stream', onServerStream.bind(
+      this,
+      this[kOptions].Http2ServerRequest,
+      this[kOptions].Http2ServerResponse)
+    );
   }
 }
 
diff --git a/test/parallel/test-http2-options-server-request.js b/test/parallel/test-http2-options-server-request.js
new file mode 100644
index 00000000000000..2143d379823d51
--- /dev/null
+++ b/test/parallel/test-http2-options-server-request.js
@@ -0,0 +1,40 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const h2 = require('http2');
+
+class MyServerRequest extends h2.Http2ServerRequest {
+  getUserAgent() {
+    return this.headers['user-agent'] || 'unknown';
+  }
+}
+
+const server = h2.createServer({
+  Http2ServerRequest: MyServerRequest
+}, (req, res) => {
+  assert.strictEqual(req.getUserAgent(), 'node-test');
+
+  res.writeHead(200, { 'Content-Type': 'text/plain' });
+  res.end();
+});
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+  const client = h2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request({
+    ':path': '/',
+    'User-Agent': 'node-test'
+  });
+
+  req.on('response', common.mustCall());
+
+  req.resume();
+  req.on('end', common.mustCall(() => {
+    server.close();
+    client.destroy();
+  }));
+}));
diff --git a/test/parallel/test-http2-options-server-response.js b/test/parallel/test-http2-options-server-response.js
new file mode 100644
index 00000000000000..6f1ae1881d22d8
--- /dev/null
+++ b/test/parallel/test-http2-options-server-response.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const h2 = require('http2');
+
+class MyServerResponse extends h2.Http2ServerResponse {
+  status(code) {
+    return this.writeHead(code, { 'Content-Type': 'text/plain' });
+  }
+}
+
+const server = h2.createServer({
+  Http2ServerResponse: MyServerResponse
+}, (req, res) => {
+  res.status(200);
+  res.end();
+});
+server.listen(0);
+
+server.on('listening', common.mustCall(() => {
+
+  const client = h2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request({ ':path': '/' });
+
+  req.on('response', common.mustCall());
+
+  req.resume();
+  req.on('end', common.mustCall(() => {
+    server.close();
+    client.destroy();
+  }));
+}));

From 7b03c17e2f53940b77a30b586820a4d8d50786d4 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Wed, 7 Feb 2018 01:38:45 +0100
Subject: [PATCH 55/77] http2: use `_final` instead of `on('finish')`

PR-URL: https://github.com/nodejs/node/pull/18609
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 lib/internal/http2/core.js | 37 ++++++++++++++++++++++++-------------
 1 file changed, 24 insertions(+), 13 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 8edb41851d521c..3b5e4b02d53bfc 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1412,18 +1412,6 @@ function afterDoStreamWrite(status, handle, req) {
   req.handle = undefined;
 }
 
-function onHandleFinish() {
-  const handle = this[kHandle];
-  if (this[kID] === undefined) {
-    this.once('ready', onHandleFinish);
-  } else if (handle !== undefined) {
-    const req = new ShutdownWrap();
-    req.oncomplete = () => {};
-    req.handle = handle;
-    handle.shutdown(req);
-  }
-}
-
 function streamOnResume() {
   if (!this.destroyed && !this.pending)
     this[kHandle].readStart();
@@ -1444,6 +1432,13 @@ function abort(stream) {
   }
 }
 
+function afterShutdown() {
+  this.callback();
+  const stream = this.handle[kOwner];
+  if (stream)
+    stream[kMaybeDestroy]();
+}
+
 // An Http2Stream is a Duplex stream that is backed by a
 // node::http2::Http2Stream handle implementing StreamBase.
 class Http2Stream extends Duplex {
@@ -1466,7 +1461,6 @@ class Http2Stream extends Duplex {
       writeQueueSize: 0
     };
 
-    this.once('finish', onHandleFinish);
     this.on('resume', streamOnResume);
     this.on('pause', streamOnPause);
   }
@@ -1672,6 +1666,23 @@ class Http2Stream extends Duplex {
     trackWriteState(this, req.bytes);
   }
 
+  _final(cb) {
+    const handle = this[kHandle];
+    if (this[kID] === undefined) {
+      this.once('ready', () => this._final(cb));
+    } else if (handle !== undefined) {
+      debug(`Http2Stream ${this[kID]} [Http2Session ` +
+            `${sessionName(this[kSession][kType])}]: _final shutting down`);
+      const req = new ShutdownWrap();
+      req.oncomplete = afterShutdown;
+      req.callback = cb;
+      req.handle = handle;
+      handle.shutdown(req);
+    } else {
+      cb();
+    }
+  }
+
   _read(nread) {
     if (this.destroyed) {
       this.push(null);

From a10b36525038ae08c1165e12872f7c7eeff287dc Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Tue, 13 Feb 2018 17:41:39 +0100
Subject: [PATCH 56/77] doc: warn against concurrent http2stream.respondWithFD

Upcoming changes to move away from synchronous I/O on the main
thread will imply that using the same file descriptor to
respond on multiple HTTP/2 streams at the same time is invalid,
because at least on Windows `uv_fs_read()` is race-y.

Therefore, warn against such usage.

PR-URL: https://github.com/nodejs/node/pull/18762
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
---
 doc/api/http2.md | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index f0468f7e6dc406..e1b747c3d804e0 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1262,10 +1262,10 @@ automatically.
 const http2 = require('http2');
 const fs = require('fs');
 
-const fd = fs.openSync('/some/file', 'r');
-
 const server = http2.createServer();
 server.on('stream', (stream) => {
+  const fd = fs.openSync('/some/file', 'r');
+
   const stat = fs.fstatSync(fd);
   const headers = {
     'content-length': stat.size,
@@ -1273,8 +1273,8 @@ server.on('stream', (stream) => {
     'content-type': 'text/plain'
   };
   stream.respondWithFD(fd, headers);
+  stream.on('close', () => fs.closeSync(fd));
 });
-server.on('close', () => fs.closeSync(fd));
 ```
 
 The optional `options.statCheck` function may be specified to give user code
@@ -1287,6 +1287,12 @@ The `offset` and `length` options may be used to limit the response to a
 specific range subset. This can be used, for instance, to support HTTP Range
 requests.
 
+The file descriptor is not closed when the stream is closed, so it will need
+to be closed manually once it is no longer needed.
+Note that using the same file descriptor concurrently for multiple streams
+is not supported and may result in data loss. Re-using a file descriptor
+after a stream has finished is supported.
+
 When set, the `options.getTrailers()` function is called immediately after
 queuing the last chunk of payload data to be sent. The callback is passed a
 single object (with a `null` prototype) that the listener may use to specify
@@ -1296,10 +1302,10 @@ the trailing header fields to send to the peer.
 const http2 = require('http2');
 const fs = require('fs');
 
-const fd = fs.openSync('/some/file', 'r');
-
 const server = http2.createServer();
 server.on('stream', (stream) => {
+  const fd = fs.openSync('/some/file', 'r');
+
   const stat = fs.fstatSync(fd);
   const headers = {
     'content-length': stat.size,
@@ -1311,8 +1317,9 @@ server.on('stream', (stream) => {
       trailers['ABC'] = 'some value to send';
     }
   });
+
+  stream.on('close', () => fs.closeSync(fd));
 });
-server.on('close', () => fs.closeSync(fd));
 ```
 
 *Note*: The HTTP/1 specification forbids trailers from containing HTTP/2

From c32ac60aa24dbde4a27ef58f3a7ebfa08d0ec9bb Mon Sep 17 00:00:00 2001
From: Daniel Bevenius <daniel.bevenius@gmail.com>
Date: Fri, 16 Feb 2018 15:01:16 +0100
Subject: [PATCH 57/77] src: add nullptr check for session in DEBUG macro

Currenlty when configuring --debug-http2
/test/parallel/test-http2-getpackedsettings.js will segment fault:

$ out/Debug/node test/parallel/test-http2-getpackedsettings.js
Segmentation fault: 11

This is happening because the settings is created with the Environment in
PackSettings:
Http2Session::Http2Settings settings(env);
This will cause the session to be set to nullptr. When the init
function is later called the expanded DEBUG_HTTP2SESSION macro will
cause the segment fault when the session is dereferenced.

PR-URL: https://github.com/nodejs/node/pull/18815
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 src/node_http2.h | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/src/node_http2.h b/src/node_http2.h
index 5acd45dc51ee18..c2c4bdb62ab62a 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -39,16 +39,20 @@ void inline debug_vfprintf(const char* format, ...) {
 #define DEBUG_HTTP2(...) debug_vfprintf(__VA_ARGS__);
 #define DEBUG_HTTP2SESSION(session, message)                                  \
   do {                                                                        \
-    DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n",                      \
-                   session->TypeName(),                                       \
-                   session->get_async_id());                                  \
+    if (session != nullptr) {                                                 \
+      DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n",                    \
+                  session->TypeName(),                                        \
+                  session->get_async_id());                                   \
+     }                                                                        \
   } while (0)
 #define DEBUG_HTTP2SESSION2(session, message, ...)                            \
   do {                                                                        \
-    DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n",                      \
-                   session->TypeName(),                                       \
-                   session->get_async_id(),                                   \
+    if (session != nullptr) {                                                 \
+      DEBUG_HTTP2("Http2Session %s (%.0lf) " message "\n",                    \
+                  session->TypeName(),                                        \
+                  session->get_async_id(),                                    \
                   __VA_ARGS__);                                               \
+    }                                                                         \
   } while (0)
 #define DEBUG_HTTP2STREAM(stream, message)                                    \
   do {                                                                        \

From 7c2bc72bc5977b12d5efb924e683214e5215c32b Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Mon, 19 Feb 2018 23:26:45 +0200
Subject: [PATCH 58/77] doc: fix typo in http2.md

PR-URL: https://github.com/nodejs/node/pull/18872
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
---
 doc/api/http2.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index e1b747c3d804e0..f30228c92373b1 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1725,7 +1725,7 @@ changes:
     Http2ServerRequest class to use.
     Useful for extending the original `Http2ServerRequest`.
     **Default:** `Http2ServerRequest`
-  * `Http2ServerResponse` {htt2.Http2ServerResponse} Specifies the
+  * `Http2ServerResponse` {http2.Http2ServerResponse} Specifies the
     Http2ServerResponse class to use.
     Useful for extending the original `Http2ServerResponse`.
     **Default:** `Http2ServerResponse`

From ab416d77671dc11e3c6002c61262470c84b3f3b2 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sun, 4 Feb 2018 19:32:26 +0100
Subject: [PATCH 59/77] deps,src: align ssize_t ABI between Node & nghttp2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Previously, we performed casts that are considered undefined behavior.
Instead, just define `ssize_t` for nghttp2 the same way we define it
for the rest of Node.

Also, remove a TODO comment that would probably also be *technically*
correct but shouldn’t matter as long as nobody is complaining.

PR-URL: https://github.com/nodejs/node/pull/18565
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
---
 deps/nghttp2/lib/includes/config.h | 14 ++++++++++++--
 src/node.h                         |  1 -
 src/node_http2.cc                  | 10 ++--------
 3 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/deps/nghttp2/lib/includes/config.h b/deps/nghttp2/lib/includes/config.h
index 0346e0614fdb8d..242bbcfb62ff7a 100644
--- a/deps/nghttp2/lib/includes/config.h
+++ b/deps/nghttp2/lib/includes/config.h
@@ -1,8 +1,18 @@
 /* Hint to the compiler that a function never returns */
 #define NGHTTP2_NORETURN
 
-/* Define to `int' if <sys/types.h> does not define. */
-#define ssize_t int
+/* Edited to match src/node.h. */
+#include <stdint.h>
+
+#ifdef _WIN32
+#if !defined(_SSIZE_T_) && !defined(_SSIZE_T_DEFINED)
+typedef intptr_t ssize_t;
+# define _SSIZE_T_
+# define _SSIZE_T_DEFINED
+#endif
+#else  // !_WIN32
+# include <sys/types.h>  // size_t, ssize_t
+#endif  // _WIN32
 
 /* Define to 1 if you have the `std::map::emplace`. */
 #define HAVE_STD_MAP_EMPLACE 1
diff --git a/src/node.h b/src/node.h
index a48c9bc86f6904..78b2b2b64a6feb 100644
--- a/src/node.h
+++ b/src/node.h
@@ -178,7 +178,6 @@ NODE_EXTERN v8::Local<v8::Value> MakeCallback(
 #endif
 
 #ifdef _WIN32
-// TODO(tjfontaine) consider changing the usage of ssize_t to ptrdiff_t
 #if !defined(_SSIZE_T_) && !defined(_SSIZE_T_DEFINED)
 typedef intptr_t ssize_t;
 # define _SSIZE_T_
diff --git a/src/node_http2.cc b/src/node_http2.cc
index bd7eeee8655e52..33d8b359f3efc7 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -775,13 +775,7 @@ inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) {
                                bufs[n].len);
     CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
 
-    // If there is an error calling any of the callbacks, ret will be a
-    // negative number identifying the error code. This can happen, for
-    // instance, if the session is destroyed during any of the JS callbacks
-    // Note: if ssize_t is not defined (e.g. on Win32), nghttp2 will typedef
-    // ssize_t to int. Cast here so that the < 0 check actually works on
-    // Windows.
-    if (static_cast<int>(ret) < 0)
+    if (ret < 0)
       return ret;
 
     total += ret;
@@ -1693,7 +1687,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
     // ssize_t to int. Cast here so that the < 0 check actually works on
     // Windows.
     if (static_cast<int>(ret) < 0) {
-      DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret);
+      DEBUG_HTTP2SESSION2(this, "fatal error receiving data: %d", ret);
 
       Local<Value> argv[1] = {
         Integer::New(isolate, ret),

From 0cdcd5853be000de138a7be5adc8b6a37b8beccd Mon Sep 17 00:00:00 2001
From: Matteo Collina <hello@matteocollina.com>
Date: Tue, 20 Feb 2018 00:42:48 +0100
Subject: [PATCH 60/77] http2: fix condition where data is lost

PR-URL: https://github.com/nodejs/node/pull/18895
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 lib/internal/http2/core.js                    | 55 ++++++++++++++-----
 ...http2-compat-short-stream-client-server.js | 50 +++++++++++++++++
 .../test-http2-short-stream-client-server.js  | 55 +++++++++++++++++++
 3 files changed, 146 insertions(+), 14 deletions(-)
 create mode 100644 test/parallel/test-http2-compat-short-stream-client-server.js
 create mode 100644 test/parallel/test-http2-short-stream-client-server.js

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 3b5e4b02d53bfc..e9a2ec40ba4fb0 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -306,8 +306,23 @@ function onStreamClose(code) {
 
   if (state.fd !== undefined)
     tryClose(state.fd);
-  stream.push(null);
-  stream[kMaybeDestroy](null, code);
+
+  // Defer destroy we actually emit end.
+  if (stream._readableState.endEmitted || code !== NGHTTP2_NO_ERROR) {
+    // If errored or ended, we can destroy immediately.
+    stream[kMaybeDestroy](null, code);
+  } else {
+    // Wait for end to destroy.
+    stream.on('end', stream[kMaybeDestroy]);
+    // Push a null so the stream can end whenever the client consumes
+    // it completely.
+    stream.push(null);
+
+    // Same as net.
+    if (stream._readableState.length === 0) {
+      stream.read(0);
+    }
+  }
 }
 
 // Receives a chunk of data for a given stream and forwards it on
@@ -325,11 +340,19 @@ function onStreamRead(nread, buf) {
     }
     return;
   }
+
   // Last chunk was received. End the readable side.
   debug(`Http2Stream ${stream[kID]} [Http2Session ` +
         `${sessionName(stream[kSession][kType])}]: ending readable.`);
-  stream.push(null);
-  stream[kMaybeDestroy]();
+
+  // defer this until we actually emit end
+  if (stream._readableState.endEmitted) {
+    stream[kMaybeDestroy]();
+  } else {
+    stream.on('end', stream[kMaybeDestroy]);
+    stream.push(null);
+    stream.read(0);
+  }
 }
 
 // Called when the remote peer settings have been updated.
@@ -1826,21 +1849,25 @@ class Http2Stream extends Duplex {
     session[kMaybeDestroy]();
     process.nextTick(emit, this, 'close', code);
     callback(err);
-  }
 
+  }
   // The Http2Stream can be destroyed if it has closed and if the readable
   // side has received the final chunk.
   [kMaybeDestroy](error, code = NGHTTP2_NO_ERROR) {
-    if (error == null) {
-      if (code === NGHTTP2_NO_ERROR &&
-          (!this._readableState.ended ||
-          !this._writableState.ended ||
-          this._writableState.pendingcb > 0 ||
-          !this.closed)) {
-        return;
-      }
+    if (error || code !== NGHTTP2_NO_ERROR) {
+      this.destroy(error);
+      return;
+    }
+
+    // TODO(mcollina): remove usage of _*State properties
+    if (this._readableState.ended &&
+        this._writableState.ended &&
+        this._writableState.pendingcb === 0 &&
+        this.closed) {
+      this.destroy();
+      // This should return, but eslint complains.
+      // return
     }
-    this.destroy(error);
   }
 }
 
diff --git a/test/parallel/test-http2-compat-short-stream-client-server.js b/test/parallel/test-http2-compat-short-stream-client-server.js
new file mode 100644
index 00000000000000..f7ef9412106f59
--- /dev/null
+++ b/test/parallel/test-http2-compat-short-stream-client-server.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const { Readable } = require('stream');
+
+const server = http2.createServer(common.mustCall((req, res) => {
+  res.setHeader('content-type', 'text/html');
+  const input = new Readable({
+    read() {
+      this.push('test');
+      this.push(null);
+    }
+  });
+  input.pipe(res);
+}));
+
+server.listen(0, common.mustCall(() => {
+  const port = server.address().port;
+  const client = http2.connect(`http://localhost:${port}`);
+
+  const req = client.request();
+
+  req.on('response', common.mustCall((headers) => {
+    assert.strictEqual(headers[':status'], 200);
+    assert.strictEqual(headers['content-type'], 'text/html');
+  }));
+
+  let data = '';
+
+  const notCallClose = common.mustNotCall();
+
+  setTimeout(() => {
+    req.setEncoding('utf8');
+    req.removeListener('close', notCallClose);
+    req.on('close', common.mustCall(() => {
+      server.close();
+      client.close();
+    }));
+    req.on('data', common.mustCallAtLeast((d) => data += d));
+    req.on('end', common.mustCall(() => {
+      assert.strictEqual(data, 'test');
+    }));
+  }, common.platformTimeout(100));
+
+  req.on('close', notCallClose);
+}));
diff --git a/test/parallel/test-http2-short-stream-client-server.js b/test/parallel/test-http2-short-stream-client-server.js
new file mode 100644
index 00000000000000..e632b8d96b9ea9
--- /dev/null
+++ b/test/parallel/test-http2-short-stream-client-server.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const { Readable } = require('stream');
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream) => {
+  stream.respond({
+    ':status': 200,
+    'content-type': 'text/html'
+  });
+  const input = new Readable({
+    read() {
+      this.push('test');
+      this.push(null);
+    }
+  });
+  input.pipe(stream);
+}));
+
+
+server.listen(0, common.mustCall(() => {
+  const port = server.address().port;
+  const client = http2.connect(`http://localhost:${port}`);
+
+  const req = client.request();
+
+  req.on('response', common.mustCall((headers) => {
+    assert.strictEqual(headers[':status'], 200);
+    assert.strictEqual(headers['content-type'], 'text/html');
+  }));
+
+  let data = '';
+
+  const notCallClose = common.mustNotCall();
+
+  setTimeout(() => {
+    req.setEncoding('utf8');
+    req.removeListener('close', notCallClose);
+    req.on('close', common.mustCall(() => {
+      server.close();
+      client.close();
+    }));
+    req.on('data', common.mustCallAtLeast((d) => data += d));
+    req.on('end', common.mustCall(() => {
+      assert.strictEqual(data, 'test');
+    }));
+  }, common.platformTimeout(100));
+
+  req.on('close', notCallClose);
+}));

From 265baaf48572b5d152ecb4901e7d06c893f83f19 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sun, 25 Feb 2018 21:54:18 +0100
Subject: [PATCH 61/77] http2: send error text in case of ALPN mismatch

Send a human-readable HTTP/1 response in case of an unexpected
ALPN protocol. This helps with debugging this condition,
since previously the only result of it would be a closed socket.

PR-URL: https://github.com/nodejs/node/pull/18986
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 lib/internal/http2/core.js                 | 13 +++++++++++--
 test/parallel/test-http2-https-fallback.js | 13 ++++++++++---
 2 files changed, 21 insertions(+), 5 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index e9a2ec40ba4fb0..80c64c7ba3048b 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -2489,8 +2489,17 @@ function connectionListener(socket) {
       return httpConnectionListener.call(this, socket);
     }
     // Let event handler deal with the socket
-    if (!this.emit('unknownProtocol', socket))
-      socket.destroy();
+    debug(`Unknown protocol from ${socket.remoteAddress}:${socket.remotePort}`);
+    if (!this.emit('unknownProtocol', socket)) {
+      // We don't know what to do, so let's just tell the other side what's
+      // going on in a format that they *might* understand.
+      socket.end('HTTP/1.0 403 Forbidden\r\n' +
+                 'Content-Type: text/plain\r\n\r\n' +
+                 'Unknown ALPN Protocol, expected `h2` to be available.\n' +
+                 'If this is a HTTP request: The server was not ' +
+                 'configured with the `allowHTTP1` option or a ' +
+                 'listener for the `unknownProtocol` event.\n');
+    }
     return;
   }
 
diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js
index 01b694e586dd49..5d9a7e17103e56 100644
--- a/test/parallel/test-http2-https-fallback.js
+++ b/test/parallel/test-http2-https-fallback.js
@@ -6,7 +6,7 @@ const fixtures = require('../common/fixtures');
 if (!common.hasCrypto)
   common.skip('missing crypto');
 
-const { strictEqual } = require('assert');
+const { strictEqual, ok } = require('assert');
 const { createSecureContext } = require('tls');
 const { createSecureServer, connect } = require('http2');
 const { get } = require('https');
@@ -131,10 +131,17 @@ function onSession(session) {
 
     // HTTP/1.1 client
     get(Object.assign(parse(origin), clientOptions), common.mustNotCall())
-      .on('error', common.mustCall(cleanup));
+      .on('error', common.mustCall(cleanup))
+      .end();
 
     // Incompatible ALPN TLS client
+    let text = '';
     tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions))
-      .on('error', common.mustCall(cleanup));
+      .setEncoding('utf8')
+      .on('data', (chunk) => text += chunk)
+      .on('end', common.mustCall(() => {
+        ok(/Unknown ALPN Protocol, expected `h2` to be available/.test(text));
+        cleanup();
+      }));
   }));
 }

From 78b6542d44a5796973e3d70f98aebdf6f09093ae Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Sun, 25 Feb 2018 21:46:10 +0100
Subject: [PATCH 62/77] http2: use original error for cancelling pending
 streams

Previously, if `session.destroy()` was called with an error object,
the information contained in it would be discarded and a generic
`ERR_HTTP2_STREAM_CANCEL` would be used for all pending streams.

Instead, make the information from the original error object
available.

PR-URL: https://github.com/nodejs/node/pull/18988
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 lib/internal/http2/core.js                          |  5 +++++
 test/parallel/test-http2-client-onconnect-errors.js | 11 ++++++++---
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 80c64c7ba3048b..0a3bcaf2e0cbbd 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1129,6 +1129,11 @@ class Http2Session extends EventEmitter {
 
     // Destroy any pending and open streams
     const cancel = new errors.Error('ERR_HTTP2_STREAM_CANCEL');
+    if (error) {
+      cancel.cause = error;
+      if (typeof error.message === 'string')
+        cancel.message += ` (caused by: ${error.message})`;
+    }
     state.pendingStreams.forEach((stream) => stream.destroy(cancel));
     state.streams.forEach((stream) => stream.destroy(error));
 
diff --git a/test/parallel/test-http2-client-onconnect-errors.js b/test/parallel/test-http2-client-onconnect-errors.js
index 44fe6875602187..af67a0d0ae27db 100644
--- a/test/parallel/test-http2-client-onconnect-errors.js
+++ b/test/parallel/test-http2-client-onconnect-errors.js
@@ -88,9 +88,14 @@ function runTest(test) {
     req.on('error', errorMustCall);
   } else {
     client.on('error', errorMustCall);
-    req.on('error', common.expectsError({
-      code: 'ERR_HTTP2_STREAM_CANCEL'
-    }));
+    req.on('error', (err) => {
+      common.expectsError({
+        code: 'ERR_HTTP2_STREAM_CANCEL'
+      })(err);
+      common.expectsError({
+        code: 'ERR_HTTP2_ERROR'
+      })(err.cause);
+    });
   }
 
   req.on('end', common.mustCall());

From c46ee1ffe3494bc87f911185312cc775fc0bcca9 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Thu, 22 Feb 2018 02:52:59 +0100
Subject: [PATCH 63/77] http2: fix endless loop when writing empty string

PR-URL: https://github.com/nodejs/node/pull/18924
Fixes: https://github.com/nodejs/node/issues/18169
Refs: https://github.com/nodejs/node/pull/18673
Refs: https://github.com/nodejs/node/blob/v9.5.0/src/node_http2.cc#L1481-L1484
Refs: https://github.com/nodejs/node/blob/v9.5.0/lib/_http_outgoing.js#L659-L661
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Khaidi Chu <i@2333.moe>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 src/node_http2.cc | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/node_http2.cc b/src/node_http2.cc
index 33d8b359f3efc7..bb600e988361d3 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -2209,6 +2209,17 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
 
   size_t amount = 0;          // amount of data being sent in this data frame.
 
+  // Remove all empty chunks from the head of the queue.
+  // This is done here so that .write('', cb) is still a meaningful way to
+  // find out when the HTTP2 stream wants to consume data, and because the
+  // StreamBase API allows empty input chunks.
+  while (!stream->queue_.empty() && stream->queue_.front().buf.len == 0) {
+    WriteWrap* finished = stream->queue_.front().req_wrap;
+    stream->queue_.pop();
+    if (finished != nullptr)
+      finished->Done(0);
+  }
+
   if (!stream->queue_.empty()) {
     DEBUG_HTTP2SESSION2(session, "stream %d has pending outbound data", id);
     amount = std::min(stream->available_outbound_length_, length);
@@ -2222,7 +2233,8 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
     }
   }
 
-  if (amount == 0 && stream->IsWritable() && stream->queue_.empty()) {
+  if (amount == 0 && stream->IsWritable()) {
+    CHECK(stream->queue_.empty());
     DEBUG_HTTP2SESSION2(session, "deferring stream %d", id);
     return NGHTTP2_ERR_DEFERRED;
   }

From 74212aacc4ac8c32ac90f04324f28c4d6aca6eed Mon Sep 17 00:00:00 2001
From: XadillaX <i@2333.moe>
Date: Sun, 11 Feb 2018 15:57:35 +0800
Subject: [PATCH 64/77] test: check endless loop while writing empty string

Refs: https://github.com/nodejs/node/pull/18673
PR-URL: https://github.com/nodejs/node/pull/18924
Refs: https://github.com/nodejs/node/pull/18673
Refs: https://github.com/nodejs/node/blob/v9.5.0/src/node_http2.cc#L1481-L1484
Refs: https://github.com/nodejs/node/blob/v9.5.0/lib/_http_outgoing.js#L659-L661
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
---
 .../test-http2-client-write-empty-string.js   | 54 +++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 test/parallel/test-http2-client-write-empty-string.js

diff --git a/test/parallel/test-http2-client-write-empty-string.js b/test/parallel/test-http2-client-write-empty-string.js
new file mode 100644
index 00000000000000..c10698d417038d
--- /dev/null
+++ b/test/parallel/test-http2-client-write-empty-string.js
@@ -0,0 +1,54 @@
+'use strict';
+
+const assert = require('assert');
+const http2 = require('http2');
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+for (const chunkSequence of [
+  [ '' ],
+  [ '', '' ]
+]) {
+  const server = http2.createServer();
+  server.on('stream', common.mustCall((stream, headers, flags) => {
+    stream.respond({ 'content-type': 'text/html' });
+
+    let data = '';
+    stream.on('data', common.mustNotCall((chunk) => {
+      data += chunk.toString();
+    }));
+    stream.on('end', common.mustCall(() => {
+      stream.end(`"${data}"`);
+    }));
+  }));
+
+  server.listen(0, common.mustCall(() => {
+    const port = server.address().port;
+    const client = http2.connect(`http://localhost:${port}`);
+
+    const req = client.request({
+      ':method': 'POST',
+      ':path': '/'
+    });
+
+    req.on('response', common.mustCall((headers) => {
+      assert.strictEqual(headers[':status'], 200);
+      assert.strictEqual(headers['content-type'], 'text/html');
+    }));
+
+    let data = '';
+    req.setEncoding('utf8');
+    req.on('data', common.mustCallAtLeast((d) => data += d));
+    req.on('end', common.mustCall(() => {
+      assert.strictEqual(data, '""');
+      server.close();
+      client.close();
+    }));
+
+    for (const chunk of chunkSequence)
+      req.write(chunk);
+    req.end();
+  }));
+}

From 16e9b8dafad6e9be6788ba8cf15d09580037e56c Mon Sep 17 00:00:00 2001
From: Matteo Collina <hello@matteocollina.com>
Date: Fri, 2 Mar 2018 20:38:51 +0100
Subject: [PATCH 65/77] http2: fix flaky test-http2-https-fallback

The test was flaky because it relied on a specific order of
asynchronous operation that were fired paralellely. This was true
on most platform and conditions, but not all the time.

See: https://github.com/nodejs/node/pull/18986

PR-URL: https://github.com/nodejs/node/pull/19093
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 test/parallel/test-http2-https-fallback.js | 45 ++++++++++++++--------
 1 file changed, 29 insertions(+), 16 deletions(-)

diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js
index 5d9a7e17103e56..a872d686d34f85 100644
--- a/test/parallel/test-http2-https-fallback.js
+++ b/test/parallel/test-http2-https-fallback.js
@@ -31,7 +31,7 @@ function onRequest(request, response) {
   }));
 }
 
-function onSession(session) {
+function onSession(session, next) {
   const headers = {
     ':path': '/',
     ':method': 'GET',
@@ -54,6 +54,10 @@ function onSession(session) {
 
     session.close();
     this.cleanup();
+
+    if (typeof next === 'function') {
+      next();
+    }
   }));
   request.end();
 }
@@ -126,22 +130,31 @@ function onSession(session) {
     connect(
       origin,
       clientOptions,
-      common.mustCall(onSession.bind({ cleanup, server }))
+      common.mustCall(function(session) {
+        onSession.call({ cleanup, server },
+                       session,
+                       common.mustCall(testNoTls));
+      })
     );
 
-    // HTTP/1.1 client
-    get(Object.assign(parse(origin), clientOptions), common.mustNotCall())
-      .on('error', common.mustCall(cleanup))
-      .end();
-
-    // Incompatible ALPN TLS client
-    let text = '';
-    tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions))
-      .setEncoding('utf8')
-      .on('data', (chunk) => text += chunk)
-      .on('end', common.mustCall(() => {
-        ok(/Unknown ALPN Protocol, expected `h2` to be available/.test(text));
-        cleanup();
-      }));
+    function testNoTls() {
+      // HTTP/1.1 client
+      get(Object.assign(parse(origin), clientOptions), common.mustNotCall)
+        .on('error', common.mustCall(cleanup))
+        .on('error', common.mustCall(testWrongALPN))
+        .end();
+    }
+
+    function testWrongALPN() {
+      // Incompatible ALPN TLS client
+      let text = '';
+      tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions))
+        .setEncoding('utf8')
+        .on('data', (chunk) => text += chunk)
+        .on('end', common.mustCall(() => {
+          ok(/Unknown ALPN Protocol, expected `h2` to be available/.test(text));
+          cleanup();
+        }));
+    }
   }));
 }

From 4188136355c164160900c3e530796e7dfff90fa0 Mon Sep 17 00:00:00 2001
From: Anna Henningsen <anna@addaleax.net>
Date: Mon, 26 Feb 2018 15:23:25 +0100
Subject: [PATCH 66/77] http2: no stream destroy while its data is on the wire
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This fixes a crash that occurred when a `Http2Stream` write
is completed after it is already destroyed.

Instead, don’t destroy the stream in that case and wait for
GC to take over.

Backport-PR-URL: https://github.com/nodejs/node/pull/19185
PR-URL: https://github.com/nodejs/node/pull/19002
Fixes: https://github.com/nodejs/node/issues/18973
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 src/node_http2.cc                             | 19 ++++--
 src/node_http2.h                              |  3 +
 ...tp2-write-finishes-after-stream-destroy.js | 62 +++++++++++++++++++
 3 files changed, 80 insertions(+), 4 deletions(-)
 create mode 100644 test/parallel/test-http2-write-finishes-after-stream-destroy.js

diff --git a/src/node_http2.cc b/src/node_http2.cc
index bb600e988361d3..b31878582301ed 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -1712,6 +1712,14 @@ void Http2Session::OnStreamDestructImpl(void* ctx) {
   session->stream_ = nullptr;
 }
 
+bool Http2Session::HasWritesOnSocketForStream(Http2Stream* stream) {
+  for (const nghttp2_stream_write& wr : outgoing_buffers_) {
+    if (wr.req_wrap != nullptr && wr.req_wrap->stream() == stream)
+      return true;
+  }
+  return false;
+}
+
 // Every Http2Session session is tightly bound to a single i/o StreamBase
 // (typically a net.Socket or tls.TLSSocket). The lifecycle of the two is
 // tightly coupled with all data transfer between the two happening at the
@@ -1770,13 +1778,12 @@ Http2Stream::Http2Stream(
 
 
 Http2Stream::~Http2Stream() {
+  DEBUG_HTTP2STREAM(this, "tearing down stream");
   if (session_ != nullptr) {
     session_->RemoveStream(this);
     session_ = nullptr;
   }
 
-  if (!object().IsEmpty())
-    ClearWrap(object());
   persistent().Reset();
   CHECK(persistent().IsEmpty());
 }
@@ -1861,7 +1868,7 @@ inline void Http2Stream::Destroy() {
     Http2Stream* stream = static_cast<Http2Stream*>(data);
     // Free any remaining outgoing data chunks here. This should be done
     // here because it's possible for destroy to have been called while
-    // we still have qeueued outbound writes.
+    // we still have queued outbound writes.
     while (!stream->queue_.empty()) {
       nghttp2_stream_write& head = stream->queue_.front();
       if (head.req_wrap != nullptr)
@@ -1869,7 +1876,11 @@ inline void Http2Stream::Destroy() {
       stream->queue_.pop();
     }
 
-    delete stream;
+    // We can destroy the stream now if there are no writes for it
+    // already on the socket. Otherwise, we'll wait for the garbage collector
+    // to take care of cleaning up.
+    if (!stream->session()->HasWritesOnSocketForStream(stream))
+      delete stream;
   }, this, this->object());
 
   statistics_.end_time = uv_hrtime();
diff --git a/src/node_http2.h b/src/node_http2.h
index c2c4bdb62ab62a..3deaf185996516 100644
--- a/src/node_http2.h
+++ b/src/node_http2.h
@@ -869,6 +869,9 @@ class Http2Session : public AsyncWrap {
   // Removes a stream instance from this session
   inline void RemoveStream(Http2Stream* stream);
 
+  // Indicates whether there currently exist outgoing buffers for this stream.
+  bool HasWritesOnSocketForStream(Http2Stream* stream);
+
   // Write data to the session
   inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
 
diff --git a/test/parallel/test-http2-write-finishes-after-stream-destroy.js b/test/parallel/test-http2-write-finishes-after-stream-destroy.js
new file mode 100644
index 00000000000000..3b2dd4bcd4e548
--- /dev/null
+++ b/test/parallel/test-http2-write-finishes-after-stream-destroy.js
@@ -0,0 +1,62 @@
+// Flags: --expose-gc
+'use strict';
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const makeDuplexPair = require('../common/duplexpair');
+
+// Make sure the Http2Stream destructor works, since we don't clean the
+// stream up like we would otherwise do.
+process.on('exit', global.gc);
+
+{
+  const { clientSide, serverSide } = makeDuplexPair();
+
+  let serverSideHttp2Stream;
+  let serverSideHttp2StreamDestroyed = false;
+  const server = http2.createServer();
+  server.on('stream', common.mustCall((stream, headers) => {
+    serverSideHttp2Stream = stream;
+    stream.respond({
+      'content-type': 'text/html',
+      ':status': 200
+    });
+
+    const originalWrite = serverSide._write;
+    serverSide._write = (buf, enc, cb) => {
+      if (serverSideHttp2StreamDestroyed) {
+        serverSide.destroy();
+        serverSide.write = () => {};
+      } else {
+        setImmediate(() => {
+          originalWrite.call(serverSide, buf, enc, () => setImmediate(cb));
+        });
+      }
+    };
+
+    // Enough data to fit into a single *session* window,
+    // not enough data to fit into a single *stream* window.
+    stream.write(Buffer.alloc(40000));
+  }));
+
+  server.emit('connection', serverSide);
+
+  const client = http2.connect('http://localhost:80', {
+    createConnection: common.mustCall(() => clientSide)
+  });
+
+  const req = client.request({ ':path': '/' });
+
+  req.on('response', common.mustCall((headers) => {
+    assert.strictEqual(headers[':status'], 200);
+  }));
+
+  req.on('data', common.mustCallAtLeast(() => {
+    if (!serverSideHttp2StreamDestroyed) {
+      serverSideHttp2Stream.destroy();
+      serverSideHttp2StreamDestroyed = true;
+    }
+  }));
+}

From 89c42a304a268e9275db306ce790df756354b85d Mon Sep 17 00:00:00 2001
From: Steven <steven@ceriously.com>
Date: Tue, 20 Mar 2018 09:06:02 -0400
Subject: [PATCH 67/77] doc: add note about browsers and HTTP/2

PR-URL: https://github.com/nodejs/node/pull/19476
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 doc/api/http2.md | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index f30228c92373b1..e76c4e54608e7f 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -24,8 +24,11 @@ be emitted either by client-side code or server-side code.
 
 ### Server-side example
 
-The following illustrates a simple, plain-text HTTP/2 server using the
-Core API:
+The following illustrates a simple HTTP/2 server using the Core API.
+Since there are no browsers known that support
+[unencrypted HTTP/2][HTTP/2 Unencrypted], the use of
+[`http2.createSecureServer()`][] is necessary when communicating
+with browser clients.
 
 ```js
 const http2 = require('http2');
@@ -253,7 +256,7 @@ and would instead register a handler for the `'stream'` event emitted by the
 ```js
 const http2 = require('http2');
 
-// Create a plain-text HTTP/2 server
+// Create an unencrypted HTTP/2 server
 const server = http2.createServer();
 
 server.on('stream', (stream, headers) => {
@@ -1735,10 +1738,18 @@ changes:
 Returns a `net.Server` instance that creates and manages `Http2Session`
 instances.
 
+Since there are no browsers known that support
+[unencrypted HTTP/2][HTTP/2 Unencrypted], the use of
+[`http2.createSecureServer()`][] is necessary when communicating
+with browser clients.
+
 ```js
 const http2 = require('http2');
 
-// Create a plain-text HTTP/2 server
+// Create an unencrypted HTTP/2 server.
+// Since there are no browsers known that support
+// unencrypted HTTP/2, the use of `http2.createSecureServer()`
+// is necessary when communicating with browser clients.
 const server = http2.createServer();
 
 server.on('stream', (stream, headers) => {
@@ -3093,6 +3104,7 @@ following additional properties:
 [Compatibility API]: #http2_compatibility_api
 [HTTP/1]: http.html
 [HTTP/2]: https://tools.ietf.org/html/rfc7540
+[HTTP/2 Unencrypted]: https://http2.github.io/faq/#does-http2-require-encryption
 [HTTP2 Headers Object]: #http2_headers_object
 [HTTP2 Settings Object]: #http2_settings_object
 [HTTPS]: https.html

From c0c96641b6ada8db9b835f861b3dba685c1a8322 Mon Sep 17 00:00:00 2001
From: James M Snell <jasnell@gmail.com>
Date: Mon, 19 Mar 2018 08:17:26 -0700
Subject: [PATCH 68/77] http2: remove some unnecessary next ticks

PR-URL: https://github.com/nodejs/node/pull/19451
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 lib/internal/http2/core.js | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 0a3bcaf2e0cbbd..9641726d5c0f90 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -364,7 +364,7 @@ function onSettings() {
   session[kUpdateTimer]();
   debug(`Http2Session ${sessionName(session[kType])}: new settings received`);
   session[kRemoteSettings] = undefined;
-  process.nextTick(emit, session, 'remoteSettings', session.remoteSettings);
+  session.emit('remoteSettings', session.remoteSettings);
 }
 
 // If the stream exists, an attempt will be made to emit an event
@@ -380,7 +380,7 @@ function onPriority(id, parent, weight, exclusive) {
   const emitter = session[kState].streams.get(id) || session;
   if (!emitter.destroyed) {
     emitter[kUpdateTimer]();
-    process.nextTick(emit, emitter, 'priority', id, parent, weight, exclusive);
+    emitter.emit('priority', id, parent, weight, exclusive);
   }
 }
 
@@ -394,7 +394,7 @@ function onFrameError(id, type, code) {
         `type ${type} on stream ${id}, code: ${code}`);
   const emitter = session[kState].streams.get(id) || session;
   emitter[kUpdateTimer]();
-  process.nextTick(emit, emitter, 'frameError', type, code, id);
+  emitter.emit('frameError', type, code, id);
 }
 
 function onAltSvc(stream, origin, alt) {
@@ -404,7 +404,7 @@ function onAltSvc(stream, origin, alt) {
   debug(`Http2Session ${sessionName(session[kType])}: altsvc received: ` +
         `stream: ${stream}, origin: ${origin}, alt: ${alt}`);
   session[kUpdateTimer]();
-  process.nextTick(emit, session, 'altsvc', alt, origin, stream);
+  session.emit('altsvc', alt, origin, stream);
 }
 
 // Receiving a GOAWAY frame from the connected peer is a signal that no
@@ -734,7 +734,7 @@ function setupHandle(socket, type, options) {
   // core will check for session.destroyed before progressing, this
   // ensures that those at l`east get cleared out.
   if (this.destroyed) {
-    process.nextTick(emit, this, 'connect', this, socket);
+    this.emit('connect', this, socket);
     return;
   }
   debug(`Http2Session ${sessionName(type)}: setting up session handle`);
@@ -776,7 +776,7 @@ function setupHandle(socket, type, options) {
     options.settings : {};
 
   this.settings(settings);
-  process.nextTick(emit, this, 'connect', this, socket);
+  this.emit('connect', this, socket);
 }
 
 // Emits a close event followed by an error event if err is truthy. Used
@@ -1227,7 +1227,7 @@ class Http2Session extends EventEmitter {
       }
     }
 
-    process.nextTick(emit, this, 'timeout');
+    this.emit('timeout');
   }
 
   ref() {
@@ -1455,8 +1455,8 @@ function streamOnPause() {
 function abort(stream) {
   if (!stream.aborted &&
       !(stream._writableState.ended || stream._writableState.ending)) {
-    process.nextTick(emit, stream, 'aborted');
     stream[kState].flags |= STREAM_FLAGS_ABORTED;
+    stream.emit('aborted');
   }
 }
 
@@ -1578,7 +1578,7 @@ class Http2Stream extends Duplex {
       }
     }
 
-    process.nextTick(emit, this, 'timeout');
+    this.emit('timeout');
   }
 
   // true if the HEADERS frame has been sent

From f6387a4f4f410fd71faba243a78f98d4a54a9704 Mon Sep 17 00:00:00 2001
From: Timothy Gu <timothygu99@gmail.com>
Date: Sun, 25 Mar 2018 19:56:02 -0700
Subject: [PATCH 69/77] doc: rename HTTP2 to HTTP/2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Previously, "HTTP/2" was strictly used to describe the protocol, and
HTTP2 the module. This distinction is deemed unnecessary, and
consistency between the two terms is enforced.

PR-URL: https://github.com/nodejs/node/pull/19603
Reviewed-By: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Chen Gang <gangc.cxy@foxmail.com>
Reviewed-By: Shingo Inoue <leko.noor@gmail.com>
---
 doc/api/http2.md         | 62 +++++++++++++++++++---------------------
 tools/doc/type-parser.js |  4 +--
 2 files changed, 32 insertions(+), 34 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index e76c4e54608e7f..55d35040592e50 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1,4 +1,4 @@
-# HTTP2
+# HTTP/2
 
 <!--introduced_in=v8.4.0-->
 
@@ -230,7 +230,7 @@ added: v8.4.0
 
 The `'stream'` event is emitted when a new `Http2Stream` is created. When
 invoked, the handler function will receive a reference to the `Http2Stream`
-object, a [HTTP2 Headers Object][], and numeric flags associated with the
+object, a [HTTP/2 Headers Object][], and numeric flags associated with the
 creation of the stream.
 
 ```js
@@ -383,7 +383,7 @@ Transmits a `GOAWAY` frame to the connected peer *without* shutting down the
 added: v8.4.0
 -->
 
-* Value: {HTTP2 Settings Object}
+* Value: {HTTP/2 Settings Object}
 
 A prototype-less object describing the current local settings of this
 `Http2Session`. The local settings are local to *this* `Http2Session` instance.
@@ -462,7 +462,7 @@ instance's underlying [`net.Socket`].
 added: v8.4.0
 -->
 
-* Value: {HTTP2 Settings Object}
+* Value: {HTTP/2 Settings Object}
 
 A prototype-less object describing the current remote settings of this
 `Http2Session`. The remote settings are set by the *connected* HTTP/2 peer.
@@ -533,8 +533,7 @@ An object describing the current status of this `Http2Session`.
 added: v8.4.0
 -->
 
-* `settings` {HTTP2 Settings Object}
-* Returns {undefined}
+* `settings` {HTTP/2 Settings Object}
 
 Updates the current local settings for this `Http2Session` and sends a new
 `SETTINGS` frame to the connected HTTP/2 peer.
@@ -674,7 +673,7 @@ client.on('altsvc', (alt, origin, streamId) => {
 added: v8.4.0
 -->
 
-* `headers` {HTTP2 Headers Object}
+* `headers` {HTTP/2 Headers Object}
 * `options` {Object}
   * `endStream` {boolean} `true` if the `Http2Stream` *writable* side should
     be closed initially, such as when sending a `GET` request that should not
@@ -862,7 +861,7 @@ added: v8.4.0
 
 The `'trailers'` event is emitted when a block of headers associated with
 trailing header fields is received. The listener callback is passed the
-[HTTP2 Headers Object][] and flags associated with the headers.
+[HTTP/2 Headers Object][] and flags associated with the headers.
 
 ```js
 stream.on('trailers', (headers, flags) => {
@@ -961,7 +960,7 @@ calling `http2stream.close()`, or `http2stream.destroy()`. Will be
 added: REPLACEME
 -->
 
-* Value: {HTTP2 Headers Object}
+* Value: {HTTP/2 Headers Object}
 
 An object containing the outbound headers sent for this `Http2Stream`.
 
@@ -970,7 +969,7 @@ An object containing the outbound headers sent for this `Http2Stream`.
 added: REPLACEME
 -->
 
-* Value: {HTTP2 Headers Object[]}
+* Value: {HTTP/2 Headers Object[]}
 
 An array of objects containing the outbound informational (additional) headers
 sent for this `Http2Stream`.
@@ -980,7 +979,7 @@ sent for this `Http2Stream`.
 added: REPLACEME
 -->
 
-* Value: {HTTP2 Headers Object}
+* Value: {HTTP/2 Headers Object}
 
 An object containing the outbound trailers sent for this this `HttpStream`.
 
@@ -1063,7 +1062,7 @@ added: v8.4.0
 
 The `'headers'` event is emitted when an additional block of headers is received
 for a stream, such as when a block of `1xx` informational headers is received.
-The listener callback is passed the [HTTP2 Headers Object][] and flags
+The listener callback is passed the [HTTP/2 Headers Object][] and flags
 associated with the headers.
 
 ```js
@@ -1078,7 +1077,7 @@ added: v8.4.0
 -->
 
 The `'push'` event is emitted when response headers for a Server Push stream
-are received. The listener callback is passed the [HTTP2 Headers Object][] and
+are received. The listener callback is passed the [HTTP/2 Headers Object][] and
 flags associated with the headers.
 
 ```js
@@ -1095,7 +1094,7 @@ added: v8.4.0
 The `'response'` event is emitted when a response `HEADERS` frame has been
 received for this stream from the connected HTTP/2 server. The listener is
 invoked with two arguments: an Object containing the received
-[HTTP2 Headers Object][], and flags associated with the headers.
+[HTTP/2 Headers Object][], and flags associated with the headers.
 
 For example:
 
@@ -1125,8 +1124,7 @@ provide additional methods such as `http2stream.pushStream()` and
 added: v8.4.0
 -->
 
-* `headers` {HTTP2 Headers Object}
-* Returns: {undefined}
+* `headers` {HTTP/2 Headers Object}
 
 Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer.
 
@@ -1156,7 +1154,7 @@ accepts push streams, `false` otherwise. Settings are the same for every
 added: v8.4.0
 -->
 
-* `headers` {HTTP2 Headers Object}
+* `headers` {HTTP/2 Headers Object}
 * `options` {Object}
   * `exclusive` {boolean} When `true` and `parent` identifies a parent Stream,
     the created stream is made the sole direct dependency of the parent, with
@@ -1168,7 +1166,7 @@ added: v8.4.0
   initiated.
   * `err` {Error}
   * `pushStream` {ServerHttp2Stream} The returned pushStream object.
-  * `headers` {HTTP2 Headers Object} Headers object the pushStream was
+  * `headers` {HTTP/2 Headers Object} Headers object the pushStream was
   initiated with.
 * Returns: {undefined}
 
@@ -1199,7 +1197,7 @@ a `weight` value to `http2stream.priority` with the `silent` option set to
 added: v8.4.0
 -->
 
-* `headers` {HTTP2 Headers Object}
+* `headers` {HTTP/2 Headers Object}
 * `options` {Object}
   * `endStream` {boolean} Set to `true` to indicate that the response will not
     include payload data.
@@ -1245,7 +1243,7 @@ added: v8.4.0
 -->
 
 * `fd` {number} A readable file descriptor.
-* `headers` {HTTP2 Headers Object}
+* `headers` {HTTP/2 Headers Object}
 * `options` {Object}
   * `statCheck` {Function}
   * `getTrailers` {Function} Callback function invoked to collect trailer
@@ -1336,7 +1334,7 @@ added: v8.4.0
 -->
 
 * `path` {string|Buffer|URL}
-* `headers` {HTTP2 Headers Object}
+* `headers` {HTTP/2 Headers Object}
 * `options` {Object}
   * `statCheck` {Function}
   * `onError` {Function} Callback function invoked in the case of an
@@ -1716,7 +1714,7 @@ changes:
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].
-  * `settings` {HTTP2 Settings Object} The initial settings to send to the
+  * `settings` {HTTP/2 Settings Object} The initial settings to send to the
     remote peer upon connection.
   * `Http1IncomingMessage` {http.IncomingMessage} Specifies the IncomingMessage
     class to used for HTTP/1 fallback. Useful for extending the original
@@ -1825,7 +1823,7 @@ changes:
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].
-  * `settings` {HTTP2 Settings Object} The initial settings to send to the
+  * `settings` {HTTP/2 Settings Object} The initial settings to send to the
     remote peer upon connection.
   * ...: Any [`tls.createServer()`][] options can be provided. For
     servers, the identity options (`pfx` or `key`/`cert`) are usually required.
@@ -1921,7 +1919,7 @@ changes:
   * `selectPadding` {Function} When `options.paddingStrategy` is equal to
     `http2.constants.PADDING_STRATEGY_CALLBACK`, provides the callback function
     used to determine the padding. See [Using options.selectPadding][].
-  * `settings` {HTTP2 Settings Object} The initial settings to send to the
+  * `settings` {HTTP/2 Settings Object} The initial settings to send to the
     remote peer upon connection.
   * `createConnection` {Function} An optional callback that receives the `URL`
     instance passed to `connect` and the `options` object, and returns any
@@ -1974,7 +1972,7 @@ a given number of milliseconds set using `http2server.setTimeout()`.
 added: v8.4.0
 -->
 
-* Returns: {HTTP2 Settings Object}
+* Returns: {HTTP/2 Settings Object}
 
 Returns an object containing the default settings for an `Http2Session`
 instance. This method returns a new object instance every time it is called
@@ -1985,7 +1983,7 @@ so instances returned may be safely modified for use.
 added: v8.4.0
 -->
 
-* `settings` {HTTP2 Settings Object}
+* `settings` {HTTP/2 Settings Object}
 * Returns: {Buffer}
 
 Returns a `Buffer` instance containing serialized representation of the given
@@ -2007,9 +2005,9 @@ added: v8.4.0
 -->
 
 * `buf` {Buffer|Uint8Array} The packed settings.
-* Returns: {HTTP2 Settings Object}
+* Returns: {HTTP/2 Settings Object}
 
-Returns a [HTTP2 Settings Object][] containing the deserialized settings from
+Returns a [HTTP/2 Settings Object][] containing the deserialized settings from
 the given `Buffer` as generated by `http2.getPackedSettings()`.
 
 ### Headers Object
@@ -2274,7 +2272,7 @@ In order to create a mixed [HTTPS][] and HTTP/2 server, refer to the
 [ALPN negotiation][] section.
 Upgrading from non-tls HTTP/1 servers is not supported.
 
-The HTTP2 compatibility API is composed of [`Http2ServerRequest`]() and
+The HTTP/2 compatibility API is composed of [`Http2ServerRequest`]() and
 [`Http2ServerResponse`](). They aim at API compatibility with HTTP/1, but
 they do not hide the differences between the protocols. As an example,
 the status message for HTTP codes is ignored.
@@ -2382,7 +2380,7 @@ Example:
 console.log(request.headers);
 ```
 
-See [HTTP2 Headers Object][].
+See [HTTP/2 Headers Object][].
 
 *Note*: In HTTP/2, the request path, hostname, protocol, and method are
 represented as special headers prefixed with the `:` character (e.g. `':path'`).
@@ -3105,8 +3103,8 @@ following additional properties:
 [HTTP/1]: http.html
 [HTTP/2]: https://tools.ietf.org/html/rfc7540
 [HTTP/2 Unencrypted]: https://http2.github.io/faq/#does-http2-require-encryption
-[HTTP2 Headers Object]: #http2_headers_object
-[HTTP2 Settings Object]: #http2_settings_object
+[HTTP/2 Headers Object]: #http2_headers_object
+[HTTP/2 Settings Object]: #http2_settings_object
 [HTTPS]: https.html
 [Http2Session and Sockets]: #http2_http2session_and_sockets
 [Performance Observer]: perf_hooks.html
diff --git a/tools/doc/type-parser.js b/tools/doc/type-parser.js
index 0ab73162dd59e0..7999d55d740719 100644
--- a/tools/doc/type-parser.js
+++ b/tools/doc/type-parser.js
@@ -54,8 +54,8 @@ const typeMap = {
   'http.ServerResponse': 'http.html#http_class_http_serverresponse',
 
   'ClientHttp2Stream': 'http2.html#http2_class_clienthttp2stream',
-  'HTTP2 Headers Object': 'http2.html#http2_headers_object',
-  'HTTP2 Settings Object': 'http2.html#http2_settings_object',
+  'HTTP/2 Headers Object': 'http2.html#http2_headers_object',
+  'HTTP/2 Settings Object': 'http2.html#http2_settings_object',
   'http2.Http2ServerRequest': 'http2.html#http2_class_http2_http2serverrequest',
   'http2.Http2ServerResponse':
     'http2.html#http2_class_http2_http2serverresponse',

From 3ba1bebf880ddbee884db151d27ca1696e23e68f Mon Sep 17 00:00:00 2001
From: Trivikram <16024985+trivikr@users.noreply.github.com>
Date: Sun, 18 Feb 2018 19:30:51 -0800
Subject: [PATCH 70/77] test: http2 stream.respond() error checks

Backport-PR-URL: https://github.com/nodejs/node/pull/19579
PR-URL: https://github.com/nodejs/node/pull/18861
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 test/parallel/test-http2-respond-errors.js    | 144 ++++++++----------
 .../test-http2-respond-nghttperrors.js        |  99 ++++++++++++
 2 files changed, 165 insertions(+), 78 deletions(-)
 create mode 100644 test/parallel/test-http2-respond-nghttperrors.js

diff --git a/test/parallel/test-http2-respond-errors.js b/test/parallel/test-http2-respond-errors.js
index 629fec4fa684d2..5854c4fb8d02e4 100644
--- a/test/parallel/test-http2-respond-errors.js
+++ b/test/parallel/test-http2-respond-errors.js
@@ -5,93 +5,81 @@ const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');
 const http2 = require('http2');
-const {
-  constants,
-  Http2Stream,
-  nghttp2ErrorString
-} = process.binding('http2');
+const { Http2Stream } = process.binding('http2');
+
+const types = {
+  boolean: true,
+  function: () => {},
+  number: 1,
+  object: {},
+  array: [],
+  null: null,
+  symbol: Symbol('test')
+};
 
-// tests error handling within respond
-// - every other NGHTTP2 error from binding (should emit stream error)
+const server = http2.createServer();
 
-const specificTestKeys = [];
+Http2Stream.prototype.respond = () => 1;
+server.on('stream', common.mustCall((stream) => {
 
-const specificTests = [];
+  // Check for all possible TypeError triggers on options.getTrailers
+  Object.entries(types).forEach(([type, value]) => {
+    if (type === 'function') {
+      return;
+    }
 
-const genericTests = Object.getOwnPropertyNames(constants)
-  .filter((key) => (
-    key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
-  ))
-  .map((key) => ({
-    ngError: constants[key],
-    error: {
-      code: 'ERR_HTTP2_ERROR',
+    common.expectsError(
+      () => stream.respond({
+        'content-type': 'text/plain'
+      }, {
+        ['getTrailers']: value
+      }),
+      {
+        type: TypeError,
+        code: 'ERR_INVALID_OPT_VALUE',
+        message: `The value "${String(value)}" is invalid ` +
+                  'for option "getTrailers"'
+      }
+    );
+  });
+
+  // Send headers
+  stream.respond({
+    'content-type': 'text/plain'
+  }, {
+    ['getTrailers']: () => common.mustCall()
+  });
+
+  // Should throw if headers already sent
+  common.expectsError(
+    () => stream.respond(),
+    {
       type: Error,
-      message: nghttp2ErrorString(constants[key])
-    },
-    type: 'stream'
-  }));
-
-
-const tests = specificTests.concat(genericTests);
-
-let currentError;
-
-// mock submitResponse because we only care about testing error handling
-Http2Stream.prototype.respond = () => currentError.ngError;
-
-const server = http2.createServer();
-server.on('stream', common.mustCall((stream, headers) => {
-  const errorMustCall = common.expectsError(currentError.error);
-  const errorMustNotCall = common.mustNotCall(
-    `${currentError.error.code} should emit on ${currentError.type}`
+      code: 'ERR_HTTP2_HEADERS_SENT',
+      message: 'Response has already been initiated.'
+    }
   );
 
-  if (currentError.type === 'stream') {
-    stream.session.on('error', errorMustNotCall);
-    stream.on('error', errorMustCall);
-    stream.on('error', common.mustCall(() => {
-      stream.destroy();
-    }));
-  } else {
-    stream.session.once('error', errorMustCall);
-    stream.on('error', errorMustNotCall);
-  }
-
-  stream.respond();
-}, tests.length));
-
-server.listen(0, common.mustCall(() => runTest(tests.shift())));
-
-function runTest(test) {
-  const port = server.address().port;
-  const url = `http://localhost:${port}`;
-  const headers = {
-    ':path': '/',
-    ':method': 'POST',
-    ':scheme': 'http',
-    ':authority': `localhost:${port}`
-  };
-
-  const client = http2.connect(url);
-  const req = client.request(headers);
-  req.on('error', common.expectsError({
-    code: 'ERR_HTTP2_STREAM_ERROR',
-    type: Error,
-    message: 'Stream closed with error code 2'
-  }));
+  // Should throw if stream already destroyed
+  stream.destroy();
+  common.expectsError(
+    () => stream.respond(),
+    {
+      type: Error,
+      code: 'ERR_HTTP2_INVALID_STREAM',
+      message: 'The stream has been destroyed'
+    }
+  );
+}));
 
-  currentError = test;
-  req.resume();
-  req.end();
+server.listen(0, common.mustCall(() => {
+  const client = http2.connect(`http://localhost:${server.address().port}`);
+  const req = client.request();
 
   req.on('end', common.mustCall(() => {
     client.close();
-
-    if (!tests.length) {
-      server.close();
-    } else {
-      runTest(tests.shift());
-    }
+    server.close();
   }));
-}
+  req.resume();
+  req.end();
+}));
diff --git a/test/parallel/test-http2-respond-nghttperrors.js b/test/parallel/test-http2-respond-nghttperrors.js
new file mode 100644
index 00000000000000..5ec953c5442360
--- /dev/null
+++ b/test/parallel/test-http2-respond-nghttperrors.js
@@ -0,0 +1,99 @@
+'use strict';
+// Flags: --expose-internals
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const http2 = require('http2');
+const {
+  constants,
+  Http2Stream,
+  nghttp2ErrorString
+} = process.binding('http2');
+const { NghttpError } = require('internal/http2/util');
+
+// tests error handling within respond
+// - every other NGHTTP2 error from binding (should emit stream error)
+
+const specificTestKeys = [];
+
+const specificTests = [];
+
+const genericTests = Object.getOwnPropertyNames(constants)
+  .filter((key) => (
+    key.indexOf('NGHTTP2_ERR') === 0 && specificTestKeys.indexOf(key) < 0
+  ))
+  .map((key) => ({
+    ngError: constants[key],
+    error: {
+      code: 'ERR_HTTP2_ERROR',
+      type: NghttpError,
+      name: 'Error [ERR_HTTP2_ERROR]',
+      message: nghttp2ErrorString(constants[key])
+    },
+    type: 'stream'
+  }));
+
+
+const tests = specificTests.concat(genericTests);
+
+let currentError;
+
+// mock submitResponse because we only care about testing error handling
+Http2Stream.prototype.respond = () => currentError.ngError;
+
+const server = http2.createServer();
+server.on('stream', common.mustCall((stream, headers) => {
+  const errorMustCall = common.expectsError(currentError.error);
+  const errorMustNotCall = common.mustNotCall(
+    `${currentError.error.code} should emit on ${currentError.type}`
+  );
+
+  if (currentError.type === 'stream') {
+    stream.session.on('error', errorMustNotCall);
+    stream.on('error', errorMustCall);
+    stream.on('error', common.mustCall(() => {
+      stream.destroy();
+    }));
+  } else {
+    stream.session.once('error', errorMustCall);
+    stream.on('error', errorMustNotCall);
+  }
+
+  stream.respond();
+}, tests.length));
+
+server.listen(0, common.mustCall(() => runTest(tests.shift())));
+
+function runTest(test) {
+  const port = server.address().port;
+  const url = `http://localhost:${port}`;
+  const headers = {
+    ':path': '/',
+    ':method': 'POST',
+    ':scheme': 'http',
+    ':authority': `localhost:${port}`
+  };
+
+  const client = http2.connect(url);
+  const req = client.request(headers);
+  req.on('error', common.expectsError({
+    code: 'ERR_HTTP2_STREAM_ERROR',
+    type: Error,
+    message: 'Stream closed with error code 2'
+  }));
+
+  currentError = test;
+  req.resume();
+  req.end();
+
+  req.on('end', common.mustCall(() => {
+    client.close();
+
+    if (!tests.length) {
+      server.close();
+    } else {
+      runTest(tests.shift());
+    }
+  }));
+}

From d932237e53cf6e579e4c3112c4a5b630fd959efc Mon Sep 17 00:00:00 2001
From: Sarat Addepalli <sarat.addepalli@paytm.com>
Date: Fri, 16 Mar 2018 17:29:47 +0530
Subject: [PATCH 71/77] http2: destroy() stream, upon errnoException

First steps towards #19060

PR-URL: https://github.com/nodejs/node/pull/19389
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 lib/internal/http2/core.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 9641726d5c0f90..2e91344e87b4e0 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1647,7 +1647,7 @@ class Http2Stream extends Duplex {
     req.async = false;
     const err = createWriteReq(req, handle, data, encoding);
     if (err)
-      throw util._errnoException(err, 'write', req.error);
+      return this.destroy(util._errnoException(err, 'write', req.error), cb);
     trackWriteState(this, req.bytes);
   }
 
@@ -1690,7 +1690,7 @@ class Http2Stream extends Duplex {
     }
     const err = handle.writev(req, chunks);
     if (err)
-      throw util._errnoException(err, 'write', req.error);
+      return this.destroy(util._errnoException(err, 'write', req.error), cb);
     trackWriteState(this, req.bytes);
   }
 

From 0682140d56ee055b9a8e709460b9f17ae0354119 Mon Sep 17 00:00:00 2001
From: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Date: Wed, 28 Mar 2018 06:02:50 +0300
Subject: [PATCH 72/77] doc: guard against md list parsing edge case

PR-URL: https://github.com/nodejs/node/pull/19647
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Chen Gang <gangc.cxy@foxmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
---
 doc/api/http2.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 55d35040592e50..62556c8cb2930d 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -2073,8 +2073,8 @@ properties.
 * `maxConcurrentStreams` {number} Specifies the maximum number of concurrent
   streams permitted on an `Http2Session`. There is no default value which
   implies, at least theoretically, 2<sup>31</sup>-1 streams may be open
-  concurrently at any given time in an `Http2Session`. The minimum value is
-  0. The maximum allowed value is 2<sup>31</sup>-1.
+  concurrently at any given time in an `Http2Session`. The minimum value
+  is 0. The maximum allowed value is 2<sup>31</sup>-1.
 * `maxHeaderListSize` {number} Specifies the maximum size (uncompressed octets)
   of header list that will be accepted. The minimum allowed value is 0. The
   maximum allowed value is 2<sup>32</sup>-1. **Default:** 65535.

From 9d885b94698f043599c776c1c48b93e6f654c480 Mon Sep 17 00:00:00 2001
From: Trivikram <16024985+trivikr@users.noreply.github.com>
Date: Sun, 18 Feb 2018 00:11:06 -0800
Subject: [PATCH 73/77] test: http2 errors on req.close()

Backport-PR-URL: https://github.com/nodejs/node/pull/19579
PR-URL: https://github.com/nodejs/node/pull/18854
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
---
 ...t-http2-client-rststream-before-connect.js | 22 ++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js
index b0faaa5de2a398..7909fd97fc313b 100644
--- a/test/parallel/test-http2-client-rststream-before-connect.js
+++ b/test/parallel/test-http2-client-rststream-before-connect.js
@@ -16,18 +16,30 @@ server.on('stream', (stream) => {
 server.listen(0, common.mustCall(() => {
   const client = h2.connect(`http://localhost:${server.address().port}`);
   const req = client.request();
-  req.close(1);
+  const closeCode = 1;
+
+  common.expectsError(
+    () => req.close(2 ** 32),
+    {
+      type: RangeError,
+      code: 'ERR_OUT_OF_RANGE',
+      message: 'The "code" argument is out of range'
+    }
+  );
+  assert.strictEqual(req.closed, false);
+
+  req.close(closeCode, common.mustCall());
   assert.strictEqual(req.closed, true);
 
   // make sure that destroy is called
   req._destroy = common.mustCall(req._destroy.bind(req));
 
-  // second call doesn't do anything
-  assert.doesNotThrow(() => req.close(8));
+  // Second call doesn't do anything.
+  req.close(closeCode + 1);
 
   req.on('close', common.mustCall((code) => {
     assert.strictEqual(req.destroyed, true);
-    assert.strictEqual(code, 1);
+    assert.strictEqual(code, closeCode);
     server.close();
     client.close();
   }));
@@ -35,7 +47,7 @@ server.listen(0, common.mustCall(() => {
   req.on('error', common.expectsError({
     code: 'ERR_HTTP2_STREAM_ERROR',
     type: Error,
-    message: 'Stream closed with error code 1'
+    message: `Stream closed with error code ${closeCode}`
   }));
 
   req.on('response', common.mustCall());

From dedf0c0be4cdcf826945f8189afafdc1db442dcc Mon Sep 17 00:00:00 2001
From: Trivikram <16024985+trivikr@users.noreply.github.com>
Date: Wed, 28 Feb 2018 20:48:29 +0530
Subject: [PATCH 74/77] http2: callback valid check before closing request
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Do not close the request if callback is not a function, and
throw ERR_INVALID_CALLBACK TypeError

Backport-PR-URL: https://github.com/nodejs/node/pull/19229
PR-URL: https://github.com/nodejs/node/pull/19061
Fixes: https://github.com/nodejs/node/issues/18855
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Shingo Inoue <leko.noor@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
---
 lib/internal/http2/core.js                           |  4 ++--
 .../test-http2-client-rststream-before-connect.js    | 12 ++++++++++++
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index 2e91344e87b4e0..b5b1dc732ebde3 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -1764,6 +1764,8 @@ class Http2Stream extends Duplex {
       throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'code', 'number');
     if (code < 0 || code > kMaxInt)
       throw new errors.RangeError('ERR_OUT_OF_RANGE', 'code');
+    if (callback !== undefined && typeof callback !== 'function')
+      throw new errors.TypeError('ERR_INVALID_CALLBACK');
 
     // Unenroll the timeout.
     unenroll(this);
@@ -1781,8 +1783,6 @@ class Http2Stream extends Duplex {
     state.rstCode = code;
 
     if (callback !== undefined) {
-      if (typeof callback !== 'function')
-        throw new errors.TypeError('ERR_INVALID_CALLBACK');
       this.once('close', callback);
     }
 
diff --git a/test/parallel/test-http2-client-rststream-before-connect.js b/test/parallel/test-http2-client-rststream-before-connect.js
index 7909fd97fc313b..aeb31949db074e 100644
--- a/test/parallel/test-http2-client-rststream-before-connect.js
+++ b/test/parallel/test-http2-client-rststream-before-connect.js
@@ -28,6 +28,18 @@ server.listen(0, common.mustCall(() => {
   );
   assert.strictEqual(req.closed, false);
 
+  [true, 1, {}, [], null, 'test'].forEach((notFunction) => {
+    common.expectsError(
+      () => req.close(closeCode, notFunction),
+      {
+        type: TypeError,
+        code: 'ERR_INVALID_CALLBACK',
+        message: 'callback must be a function'
+      }
+    );
+    assert.strictEqual(req.closed, false);
+  });
+
   req.close(closeCode, common.mustCall());
   assert.strictEqual(req.closed, true);
 

From 7d3ff815a7a5c798b2997fcdd6329e6f292eee03 Mon Sep 17 00:00:00 2001
From: Chris Miller <chrismilleruk@gmail.com>
Date: Wed, 4 Apr 2018 13:32:18 +0100
Subject: [PATCH 75/77] doc, http2: add sections for server.close()

Clarify current behavior of http2server.close() and
http2secureServer.close() w.r.t. perceived differences
when compared with httpServer.close().

Fixes: https://github.com/nodejs/node/issues/19711

PR-URL: https://github.com/nodejs/node/pull/19802
Reviewed-By: Vse Mozhet Byt <vsemozhetbyt@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
---
 doc/api/http2.md | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index 62556c8cb2930d..fa1cb61536a93f 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -1544,6 +1544,18 @@ added: v8.4.0
 The `'timeout'` event is emitted when there is no activity on the Server for
 a given number of milliseconds set using `http2server.setTimeout()`.
 
+#### server.close([callback])
+<!-- YAML
+added: v8.4.0
+-->
+- `callback` {Function}
+
+Stops the server from accepting new connections.  See [`net.Server.close()`][].
+
+Note that this is not analogous to restricting new requests since HTTP/2
+connections are persistent. To achieve a similar graceful shutdown behavior,
+consider also using [`http2session.close()`] on active sessions.
+
 ### Class: Http2SecureServer
 <!-- YAML
 added: v8.4.0
@@ -1651,6 +1663,18 @@ negotiate an allowed protocol (i.e. HTTP/2 or HTTP/1.1). The event handler
 receives the socket for handling. If no listener is registered for this event,
 the connection is terminated. See the [Compatibility API][].
 
+#### server.close([callback])
+<!-- YAML
+added: v8.4.0
+-->
+- `callback` {Function}
+
+Stops the server from accepting new connections.  See [`tls.Server.close()`][].
+
+Note that this is not analogous to restricting new requests since HTTP/2
+connections are persistent. To achieve a similar graceful shutdown behavior,
+consider also using [`http2session.close()`] on active sessions.
+
 ### http2.createServer(options[, onRequestHandler])
 <!-- YAML
 added: v8.4.0
@@ -3126,7 +3150,9 @@ following additional properties:
 [`http2.createSecureServer()`]: #http2_http2_createsecureserver_options_onrequesthandler
 [`http2.Server`]: #http2_class_http2server
 [`http2.createServer()`]: #http2_http2_createserver_options_onrequesthandler
+[`http2session.close()`]: #http2_http2session_close_callback
 [`http2stream.pushStream()`]: #http2_http2stream_pushstream_headers_options_callback
+[`net.Server.close()`]: net.html#net_server_close_callback
 [`net.Socket`]: net.html#net_class_net_socket
 [`net.Socket.prototype.ref`]: net.html#net_socket_ref
 [`net.Socket.prototype.unref`]: net.html#net_socket_unref
@@ -3139,6 +3165,7 @@ following additional properties:
 [`response.write(data, encoding)`]: http.html#http_response_write_chunk_encoding_callback
 [`response.writeContinue()`]: #http2_response_writecontinue
 [`response.writeHead()`]: #http2_response_writehead_statuscode_statusmessage_headers
+[`tls.Server.close()`]: tls.html#tls_server_close_callback
 [`tls.TLSSocket`]: tls.html#tls_class_tls_tlssocket
 [`tls.connect()`]: tls.html#tls_tls_connect_options_callback
 [`tls.createServer()`]: tls.html#tls_tls_createserver_options_secureconnectionlistener

From c53bc0f1f20575d00e693228f6f0a4870278e9cf Mon Sep 17 00:00:00 2001
From: Pieter Mees <pieter.mees@zentrick.com>
Date: Fri, 6 Apr 2018 17:42:24 -0400
Subject: [PATCH 76/77] doc: add Http2Session.connecting property

PR-URL: https://github.com/nodejs/node/pull/19842
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 doc/api/http2.md | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/doc/api/http2.md b/doc/api/http2.md
index fa1cb61536a93f..6d69709e224c8d 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -321,6 +321,17 @@ added: REPLACEME
 Will be `true` if this `Http2Session` instance has been closed, otherwise
 `false`.
 
+#### http2session.connecting
+<!-- YAML
+added: REPLACEME
+-->
+
+* {boolean}
+
+Will be `true` if this `Http2Session` instance is still connecting, will be set
+to `false` before emitting `connect` event and/or calling the `http2.connect`
+callback.
+
 #### http2session.destroy([error,][code])
 <!-- YAML
 added: v8.4.0

From b2fd50463d6d6e7fa62e4b30fb330648f23218d5 Mon Sep 17 00:00:00 2001
From: Pieter Mees <pieter.mees@zentrick.com>
Date: Mon, 9 Apr 2018 15:19:59 -0400
Subject: [PATCH 77/77] http2: emit session connect on next tick

PR-URL: https://github.com/nodejs/node/pull/19842
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
---
 lib/internal/http2/core.js          |  4 ++--
 test/parallel/test-http2-connect.js | 25 +++++++++++++++++++++++++
 2 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js
index b5b1dc732ebde3..68bfbb043b0b63 100644
--- a/lib/internal/http2/core.js
+++ b/lib/internal/http2/core.js
@@ -734,7 +734,7 @@ function setupHandle(socket, type, options) {
   // core will check for session.destroyed before progressing, this
   // ensures that those at l`east get cleared out.
   if (this.destroyed) {
-    this.emit('connect', this, socket);
+    process.nextTick(emit, this, 'connect', this, socket);
     return;
   }
   debug(`Http2Session ${sessionName(type)}: setting up session handle`);
@@ -776,7 +776,7 @@ function setupHandle(socket, type, options) {
     options.settings : {};
 
   this.settings(settings);
-  this.emit('connect', this, socket);
+  process.nextTick(emit, this, 'connect', this, socket);
 }
 
 // Emits a close event followed by an error event if err is truthy. Used
diff --git a/test/parallel/test-http2-connect.js b/test/parallel/test-http2-connect.js
index 894c51fe3d9330..325a420b7e6e49 100644
--- a/test/parallel/test-http2-connect.js
+++ b/test/parallel/test-http2-connect.js
@@ -5,6 +5,9 @@ if (!hasCrypto)
   skip('missing crypto');
 const { doesNotThrow, throws } = require('assert');
 const { createServer, connect } = require('http2');
+const { connect: netConnect } = require('net');
+
+// check for session connect callback and event
 {
   const server = createServer();
   server.listen(0, mustCall(() => {
@@ -30,6 +33,28 @@ const { createServer, connect } = require('http2');
   }));
 }
 
+// check for session connect callback on already connected socket
+{
+  const server = createServer();
+  server.listen(0, mustCall(() => {
+    const { port } = server.address();
+
+    const onSocketConnect = () => {
+      const authority = `http://localhost:${port}`;
+      const createConnection = mustCall(() => socket);
+      const options = { createConnection };
+      connect(authority, options, mustCall(onSessionConnect));
+    };
+
+    const onSessionConnect = (session) => {
+      session.close();
+      server.close();
+    };
+
+    const socket = netConnect(port, mustCall(onSocketConnect));
+  }));
+}
+
 // check for https as protocol
 {
   const authority = 'https://localhost';