Skip to content

Commit 062541a

Browse files
addaleaxMylesBorins
authored andcommittedApr 4, 2021
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 <jasnell@gmail.com> Reviewed-By: Yongsheng Zhang <zyszys98@gmail.com>
1 parent 51e7a33 commit 062541a

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
@@ -971,6 +971,7 @@ E('ERR_HTTP2_STREAM_CANCEL', function(error) {
971971
E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error);
972972
E('ERR_HTTP2_STREAM_SELF_DEPENDENCY',
973973
'A stream cannot depend on itself', Error);
974+
E('ERR_HTTP2_TOO_MANY_INVALID_FRAMES', 'Too many invalid HTTP/2 frames', Error);
974975
E('ERR_HTTP2_TRAILERS_ALREADY_SENT',
975976
'Trailing headers have already been sent', Error);
976977
E('ERR_HTTP2_TRAILERS_NOT_READY',

‎lib/internal/http2/core.js

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

778778
// When an error occurs internally at the binding level, immediately
779779
// destroy the session.
780-
function onSessionInternalError(code) {
780+
function onSessionInternalError(integerCode, customErrorCode) {
781781
if (this[kOwner] !== undefined)
782-
this[kOwner].destroy(new NghttpError(code));
782+
this[kOwner].destroy(new NghttpError(integerCode, customErrorCode));
783783
}
784784

785785
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;
@@ -732,7 +733,7 @@ ssize_t Http2Session::OnMaxFrameSizePadding(size_t frameLen,
732733
// various callback functions. Each of these will typically result in a call
733734
// out to JavaScript so this particular function is rather hot and can be
734735
// quite expensive. This is a potential performance optimization target later.
735-
ssize_t Http2Session::ConsumeHTTP2Data() {
736+
void Http2Session::ConsumeHTTP2Data() {
736737
CHECK_NOT_NULL(stream_buf_.base);
737738
CHECK_LE(stream_buf_offset_, stream_buf_.len);
738739
size_t read_len = stream_buf_.len - stream_buf_offset_;
@@ -742,12 +743,14 @@ ssize_t Http2Session::ConsumeHTTP2Data() {
742743
read_len,
743744
nghttp2_session_want_read(session_.get()));
744745
set_receive_paused(false);
746+
custom_recv_error_code_ = nullptr;
745747
ssize_t ret =
746748
nghttp2_session_mem_recv(session_.get(),
747749
reinterpret_cast<uint8_t*>(stream_buf_.base) +
748750
stream_buf_offset_,
749751
read_len);
750752
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
753+
CHECK_IMPLIES(custom_recv_error_code_ != nullptr, ret < 0);
751754

752755
if (is_receive_paused()) {
753756
CHECK(is_reading_stopped());
@@ -759,7 +762,7 @@ ssize_t Http2Session::ConsumeHTTP2Data() {
759762
// Even if all bytes were received, a paused stream may delay the
760763
// nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
761764
stream_buf_offset_ += ret;
762-
return ret;
765+
goto done;
763766
}
764767

765768
// We are done processing the current input chunk.
@@ -769,14 +772,34 @@ ssize_t Http2Session::ConsumeHTTP2Data() {
769772
stream_buf_allocation_.clear();
770773
stream_buf_ = uv_buf_init(nullptr, 0);
771774

772-
if (ret < 0)
773-
return ret;
774-
775775
// Send any data that was queued up while processing the received data.
776-
if (!is_destroyed()) {
776+
if (ret >= 0 && !is_destroyed()) {
777777
SendPendingData();
778778
}
779-
return ret;
779+
780+
done:
781+
if (UNLIKELY(ret < 0)) {
782+
Isolate* isolate = env()->isolate();
783+
Debug(this,
784+
"fatal error receiving data: %d (%s)",
785+
ret,
786+
custom_recv_error_code_ != nullptr ?
787+
custom_recv_error_code_ : "(no custom error code)");
788+
Local<Value> args[] = {
789+
Integer::New(isolate, static_cast<int32_t>(ret)),
790+
Null(isolate)
791+
};
792+
if (custom_recv_error_code_ != nullptr) {
793+
args[1] = String::NewFromUtf8(
794+
isolate,
795+
custom_recv_error_code_,
796+
NewStringType::kInternalized).ToLocalChecked();
797+
}
798+
MakeCallback(
799+
env()->http2session_on_error_function(),
800+
arraysize(args),
801+
args);
802+
}
780803
}
781804

782805

@@ -900,14 +923,17 @@ int Http2Session::OnInvalidFrame(nghttp2_session* handle,
900923
int lib_error_code,
901924
void* user_data) {
902925
Http2Session* session = static_cast<Http2Session*>(user_data);
926+
const uint32_t max_invalid_frames = session->js_fields_->max_invalid_frames;
903927

904928
Debug(session,
905929
"invalid frame received (%u/%u), code: %d",
906930
session->invalid_frame_count_,
907-
session->js_fields_->max_invalid_frames,
931+
max_invalid_frames,
908932
lib_error_code);
909-
if (session->invalid_frame_count_++ > session->js_fields_->max_invalid_frames)
933+
if (session->invalid_frame_count_++ > max_invalid_frames) {
934+
session->custom_recv_error_code_ = "ERR_HTTP2_TOO_MANY_INVALID_FRAMES";
910935
return 1;
936+
}
911937

912938
// If the error is fatal or if error code is ERR_STREAM_CLOSED... emit error
913939
if (nghttp2_is_fatal(lib_error_code) ||
@@ -1286,6 +1312,7 @@ int Http2Session::HandleDataFrame(const nghttp2_frame* frame) {
12861312
stream->EmitRead(UV_EOF);
12871313
} else if (frame->hd.length == 0) {
12881314
if (invalid_frame_count_++ > js_fields_->max_invalid_frames) {
1315+
custom_recv_error_code_ = "ERR_HTTP2_TOO_MANY_INVALID_FRAMES";
12891316
Debug(this, "rejecting empty-frame-without-END_STREAM flood\n");
12901317
// Consider a flood of 0-length frames without END_STREAM an error.
12911318
return 1;
@@ -1470,7 +1497,7 @@ void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
14701497
ConsumeHTTP2Data();
14711498
}
14721499

1473-
if (!is_write_scheduled()) {
1500+
if (!is_write_scheduled() && !is_destroyed()) {
14741501
// Schedule a new write if nghttp2 wants to send data.
14751502
MaybeScheduleWrite();
14761503
}
@@ -1798,21 +1825,12 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
17981825
// offset of a DATA frame's data into the socket read buffer.
17991826
stream_buf_ = uv_buf_init(buf.data(), static_cast<unsigned int>(nread));
18001827

1801-
Isolate* isolate = env()->isolate();
1802-
18031828
// Store this so we can create an ArrayBuffer for read data from it.
18041829
// DATA frames will be emitted as slices of that ArrayBuffer to avoid having
18051830
// to copy memory.
18061831
stream_buf_allocation_ = std::move(buf);
18071832

1808-
ssize_t ret = ConsumeHTTP2Data();
1809-
1810-
if (UNLIKELY(ret < 0)) {
1811-
Debug(this, "fatal error receiving data: %d", ret);
1812-
Local<Value> arg = Integer::New(isolate, static_cast<int32_t>(ret));
1813-
MakeCallback(env()->http2session_on_error_function(), 1, &arg);
1814-
return;
1815-
}
1833+
ConsumeHTTP2Data();
18161834

18171835
MaybeStopReading();
18181836
}

‎src/node_http2.h

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

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

666667
void MemoryInfo(MemoryTracker* tracker) const override;
667668
SET_MEMORY_INFO_NAME(Http2Session)
@@ -895,6 +896,9 @@ class Http2Session : public AsyncWrap,
895896
v8::Global<v8::ArrayBuffer> stream_buf_ab_;
896897
AllocatedBuffer stream_buf_allocation_;
897898
size_t stream_buf_offset_ = 0;
899+
// Custom error code for errors that originated inside one of the callbacks
900+
// called by nghttp2_session_mem_recv.
901+
const char* custom_recv_error_code_ = nullptr;
898902

899903
size_t max_outstanding_pings_ = kDefaultMaxPings;
900904
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)
Please sign in to comment.