Skip to content

Commit 9d07c0a

Browse files
addaleaxjasnell
authored andcommitted
http2: add specific error code for custom frames
As suggested in #37849 (comment) improve the error presented when encountering a large number of invalid frames by giving this situation a specific error code (which we should have had from the beginning). PR-URL: #37936 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Yongsheng Zhang <[email protected]>
1 parent e8b1ea4 commit 9d07c0a

File tree

7 files changed

+70
-31
lines changed

7 files changed

+70
-31
lines changed

doc/api/errors.md

+9
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,15 @@ When setting the priority for an HTTP/2 stream, the stream may be marked as
13641364
a dependency for a parent stream. This error code is used when an attempt is
13651365
made to mark a stream and dependent of itself.
13661366

1367+
<a id="ERR_HTTP2_TOO_MANY_INVALID_FRAMES"></a>
1368+
### `ERR_HTTP2_TOO_MANY_INVALID_FRAMES`
1369+
<!--
1370+
added: REPLACEME
1371+
-->
1372+
1373+
The limit of acceptable invalid HTTP/2 protocol frames sent by the peer,
1374+
as specified through the `maxSessionInvalidFrames` option, has been exceeded.
1375+
13671376
<a id="ERR_HTTP2_TRAILERS_ALREADY_SENT"></a>
13681377
### `ERR_HTTP2_TRAILERS_ALREADY_SENT`
13691378

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ E('ERR_HTTP2_STREAM_CANCEL', function(error) {
992992
E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error);
993993
E('ERR_HTTP2_STREAM_SELF_DEPENDENCY',
994994
'A stream cannot depend on itself', Error);
995+
E('ERR_HTTP2_TOO_MANY_INVALID_FRAMES', 'Too many invalid HTTP/2 frames', Error);
995996
E('ERR_HTTP2_TRAILERS_ALREADY_SENT',
996997
'Trailing headers have already been sent', Error);
997998
E('ERR_HTTP2_TRAILERS_NOT_READY',

lib/internal/http2/core.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -778,9 +778,9 @@ const setAndValidatePriorityOptions = hideStackFrames((options) => {
778778

779779
// When an error occurs internally at the binding level, immediately
780780
// destroy the session.
781-
function onSessionInternalError(code) {
781+
function onSessionInternalError(integerCode, customErrorCode) {
782782
if (this[kOwner] !== undefined)
783-
this[kOwner].destroy(new NghttpError(code));
783+
this[kOwner].destroy(new NghttpError(integerCode, customErrorCode));
784784
}
785785

786786
function settingsCallback(cb, ack, duration) {

lib/internal/http2/util.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929
ERR_INVALID_HTTP_TOKEN
3030
},
3131
addCodeToName,
32+
getMessage,
3233
hideStackFrames
3334
} = require('internal/errors');
3435

@@ -543,11 +544,13 @@ function mapToHeaders(map,
543544
}
544545

545546
class NghttpError extends Error {
546-
constructor(ret) {
547-
super(binding.nghttp2ErrorString(ret));
548-
this.code = 'ERR_HTTP2_ERROR';
549-
this.errno = ret;
550-
addCodeToName(this, super.name, 'ERR_HTTP2_ERROR');
547+
constructor(integerCode, customErrorCode) {
548+
super(customErrorCode ?
549+
getMessage(customErrorCode, [], null) :
550+
binding.nghttp2ErrorString(integerCode));
551+
this.code = customErrorCode || 'ERR_HTTP2_ERROR';
552+
this.errno = integerCode;
553+
addCodeToName(this, super.name, this.code);
551554
}
552555

553556
toString() {

src/node_http2.cc

+38-20
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ using v8::Integer;
3232
using v8::Isolate;
3333
using v8::Local;
3434
using v8::MaybeLocal;
35+
using v8::NewStringType;
3536
using v8::Number;
3637
using v8::Object;
3738
using v8::ObjectTemplate;
@@ -782,7 +783,7 @@ ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
782783
// various callback functions. Each of these will typically result in a call
783784
// out to JavaScript so this particular function is rather hot and can be
784785
// quite expensive. This is a potential performance optimization target later.
785-
ssize_t Http2Session::ConsumeHTTP2Data() {
786+
void Http2Session::ConsumeHTTP2Data() {
786787
CHECK_NOT_NULL(stream_buf_.base);
787788
CHECK_LE(stream_buf_offset_, stream_buf_.len);
788789
size_t read_len = stream_buf_.len - stream_buf_offset_;
@@ -792,12 +793,14 @@ ssize_t Http2Session::ConsumeHTTP2Data() {
792793
read_len,
793794
nghttp2_session_want_read(session_.get()));
794795
set_receive_paused(false);
796+
custom_recv_error_code_ = nullptr;
795797
ssize_t ret =
796798
nghttp2_session_mem_recv(session_.get(),
797799
reinterpret_cast<uint8_t*>(stream_buf_.base) +
798800
stream_buf_offset_,
799801
read_len);
800802
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
803+
CHECK_IMPLIES(custom_recv_error_code_ != nullptr, ret < 0);
801804

802805
if (is_receive_paused()) {
803806
CHECK(is_reading_stopped());
@@ -809,7 +812,7 @@ ssize_t Http2Session::ConsumeHTTP2Data() {
809812
// Even if all bytes were received, a paused stream may delay the
810813
// nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
811814
stream_buf_offset_ += ret;
812-
return ret;
815+
goto done;
813816
}
814817

815818
// We are done processing the current input chunk.
@@ -819,14 +822,34 @@ ssize_t Http2Session::ConsumeHTTP2Data() {
819822
stream_buf_allocation_.clear();
820823
stream_buf_ = uv_buf_init(nullptr, 0);
821824

822-
if (ret < 0)
823-
return ret;
824-
825825
// Send any data that was queued up while processing the received data.
826-
if (!is_destroyed()) {
826+
if (ret >= 0 && !is_destroyed()) {
827827
SendPendingData();
828828
}
829-
return ret;
829+
830+
done:
831+
if (UNLIKELY(ret < 0)) {
832+
Isolate* isolate = env()->isolate();
833+
Debug(this,
834+
"fatal error receiving data: %d (%s)",
835+
ret,
836+
custom_recv_error_code_ != nullptr ?
837+
custom_recv_error_code_ : "(no custom error code)");
838+
Local<Value> args[] = {
839+
Integer::New(isolate, static_cast<int32_t>(ret)),
840+
Null(isolate)
841+
};
842+
if (custom_recv_error_code_ != nullptr) {
843+
args[1] = String::NewFromUtf8(
844+
isolate,
845+
custom_recv_error_code_,
846+
NewStringType::kInternalized).ToLocalChecked();
847+
}
848+
MakeCallback(
849+
env()->http2session_on_error_function(),
850+
arraysize(args),
851+
args);
852+
}
830853
}
831854

832855

@@ -950,14 +973,17 @@ int Http2Session::OnInvalidFrame(nghttp2_session* handle,
950973
int lib_error_code,
951974
void* user_data) {
952975
Http2Session* session = static_cast<Http2Session*>(user_data);
976+
const uint32_t max_invalid_frames = session->js_fields_->max_invalid_frames;
953977

954978
Debug(session,
955979
"invalid frame received (%u/%u), code: %d",
956980
session->invalid_frame_count_,
957-
session->js_fields_->max_invalid_frames,
981+
max_invalid_frames,
958982
lib_error_code);
959-
if (session->invalid_frame_count_++ > session->js_fields_->max_invalid_frames)
983+
if (session->invalid_frame_count_++ > max_invalid_frames) {
984+
session->custom_recv_error_code_ = "ERR_HTTP2_TOO_MANY_INVALID_FRAMES";
960985
return 1;
986+
}
961987

962988
// If the error is fatal or if error code is ERR_STREAM_CLOSED... emit error
963989
if (nghttp2_is_fatal(lib_error_code) ||
@@ -1336,6 +1362,7 @@ int Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
13361362
stream->EmitRead(UV_EOF);
13371363
} else if (frame->hd.length == 0) {
13381364
if (invalid_frame_count_++ > js_fields_->max_invalid_frames) {
1365+
custom_recv_error_code_ = "ERR_HTTP2_TOO_MANY_INVALID_FRAMES";
13391366
Debug(this, "rejecting empty-frame-without-END_STREAM flood\n");
13401367
// Consider a flood of 0-length frames without END_STREAM an error.
13411368
return 1;
@@ -1520,7 +1547,7 @@ void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
15201547
ConsumeHTTP2Data();
15211548
}
15221549

1523-
if (!is_write_scheduled()) {
1550+
if (!is_write_scheduled() && !is_destroyed()) {
15241551
// Schedule a new write if nghttp2 wants to send data.
15251552
MaybeScheduleWrite();
15261553
}
@@ -1848,21 +1875,12 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
18481875
// offset of a DATA frame's data into the socket read buffer.
18491876
stream_buf_ = uv_buf_init(buf.data(), static_cast<unsigned int>(nread));
18501877

1851-
Isolate* isolate = env()->isolate();
1852-
18531878
// Store this so we can create an ArrayBuffer for read data from it.
18541879
// DATA frames will be emitted as slices of that ArrayBuffer to avoid having
18551880
// to copy memory.
18561881
stream_buf_allocation_ = std::move(buf);
18571882

1858-
ssize_t ret = ConsumeHTTP2Data();
1859-
1860-
if (UNLIKELY(ret < 0)) {
1861-
Debug(this, "fatal error receiving data: %d", ret);
1862-
Local<Value> arg = Integer::New(isolate, static_cast<int32_t>(ret));
1863-
MakeCallback(env()->http2session_on_error_function(), 1, &arg);
1864-
return;
1865-
}
1883+
ConsumeHTTP2Data();
18661884

18671885
MaybeStopReading();
18681886
}

src/node_http2.h

+6-2
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,9 @@ class Http2Session : public AsyncWrap,
662662
// Indicates whether there currently exist outgoing buffers for this stream.
663663
bool HasWritesOnSocketForStream(Http2Stream* stream);
664664

665-
// Write data from stream_buf_ to the session
666-
ssize_t ConsumeHTTP2Data();
665+
// Write data from stream_buf_ to the session.
666+
// This will call the error callback if an error occurs.
667+
void ConsumeHTTP2Data();
667668

668669
void MemoryInfo(MemoryTracker* tracker) const override;
669670
SET_MEMORY_INFO_NAME(Http2Session)
@@ -898,6 +899,9 @@ class Http2Session : public AsyncWrap,
898899
v8::Global<v8::ArrayBuffer> stream_buf_ab_;
899900
AllocatedBuffer stream_buf_allocation_;
900901
size_t stream_buf_offset_ = 0;
902+
// Custom error code for errors that originated inside one of the callbacks
903+
// called by nghttp2_session_mem_recv.
904+
const char* custom_recv_error_code_ = nullptr;
901905

902906
size_t max_outstanding_pings_ = kDefaultMaxPings;
903907
std::queue<BaseObjectPtr<Http2Ping>> outstanding_pings_;

test/parallel/test-http2-empty-frame-without-eof.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ async function main() {
2626
stream.on('error', common.mustNotCall());
2727
client.on('error', common.mustNotCall());
2828
} else {
29-
stream.on('error', common.mustCall());
30-
client.on('error', common.mustCall());
29+
const expected = {
30+
code: 'ERR_HTTP2_TOO_MANY_INVALID_FRAMES',
31+
message: 'Too many invalid HTTP/2 frames'
32+
};
33+
stream.on('error', common.expectsError(expected));
34+
client.on('error', common.expectsError(expected));
3135
}
3236
stream.resume();
3337
await once(stream, 'end');

0 commit comments

Comments
 (0)