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/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 does not define. */ -#define ssize_t int +/* Edited to match src/node.h. */ +#include + +#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 // 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/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); diff --git a/doc/api/errors.md b/doc/api/errors.md index e895accada72f2..a540de4c1bce5c 100755 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -638,6 +638,21 @@ 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. + +### ERR_HTTP2_ALREADY_SHUTDOWN + +Occurs with multiple attempts to shutdown an HTTP/2 session. + + +### ERR_HTTP2_ALTSVC_INVALID_ORIGIN + +HTTP/2 ALTSVC frames require a valid origin. + + +### ERR_HTTP2_ALTSVC_LENGTH + +HTTP/2 ALTSVC frames are limited to a maximum of 16,382 payload bytes. + ### ERR_HTTP2_CONNECT_AUTHORITY @@ -661,6 +676,12 @@ forbidden. A failure occurred sending an individual frame on the HTTP/2 session. + +### ERR_HTTP2_GOAWAY_SESSION + +New HTTP/2 Streams may not be opened after the `Http2Session` has received a +`GOAWAY` frame from the connected peer. + ### ERR_HTTP2_HEADER_REQUIRED @@ -749,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. @@ -775,7 +796,7 @@ forbidden. ### ERR_HTTP2_PING_CANCEL -An HTTP/2 ping was cancelled. +An HTTP/2 ping was canceled. ### ERR_HTTP2_PING_LENGTH @@ -800,6 +821,11 @@ client. An attempt was made to use the `Http2Stream.prototype.responseWithFile()` API to send something other than a regular file. + +### ERR_HTTP2_SESSION_ERROR + +The `Http2Session` closed with a non-zero error code. + ### ERR_HTTP2_SOCKET_BOUND @@ -817,10 +843,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). - -### ERR_HTTP2_STREAM_CLOSED + +### 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. ### ERR_HTTP2_STREAM_ERROR @@ -1001,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][]. + + +### 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. ### ERR_NAPI_CONS_FUNCTION 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]) +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/http2.md b/doc/api/http2.md index 756fb6d041320d..6d69709e224c8d 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -1,4 +1,4 @@ -# HTTP2 +# HTTP/2 @@ -19,13 +19,16 @@ 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 -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'); @@ -36,7 +39,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 @@ -67,7 +69,6 @@ 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)); const req = client.request({ ':path': '/' }); @@ -83,7 +84,7 @@ let data = ''; req.on('data', (chunk) => { data += chunk; }); req.on('end', () => { console.log(`\n${data}`); - client.destroy(); + client.close(); }); req.end(); ``` @@ -127,7 +128,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' -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. @@ -229,24 +230,18 @@ 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 [HTTP/2 Headers Object][], and numeric flags associated with the +creation 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'); @@ -261,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) => { @@ -275,19 +270,6 @@ server.on('stream', (stream, headers) => { server.listen(80); ``` -#### Event: 'socketError' - - -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' + +* 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]) + + +* `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 + + +* Value: {boolean} + +Will be `true` if this `Http2Session` instance has been closed, otherwise +`false`. + +#### http2session.connecting + + +* {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]) +* `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` associated with the +`Http2Session`, those will also be destroyed. + #### http2session.destroyed + +* 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]]]) + + +* `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 -* Value: {[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. +#### http2session.originSet + + +* 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 -* Value: {[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. +Calls [`ref()`][`net.Socket.prototype.ref`] on this `Http2Session` +instance's underlying [`net.Socket`]. -#### http2session.request(headers[, options]) +#### http2session.remoteSettings -* `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', () => { /** .. **/ }); -}); -``` +* Value: {HTTP/2 Settings Object} -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. +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) - -* `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`. - * `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 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 232-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, - opaqueData: Buffer.from('add some debugging data here') -}, () => session.destroy()); -``` - #### http2session.socket -* `settings` {[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. @@ -568,8 +554,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 @@ -584,6 +570,178 @@ The `http2session.type` will be equal to server, and `http2.constants.NGHTTP2_SESSION_CLIENT` if the instance is a client. +#### http2session.unref() + + +Calls [`unref()`][`net.Socket.prototype.unref`] on this `Http2Session` +instance's underlying [`net.Socket`]. + +### Class: ServerHttp2Session + + +#### serverhttp2session.altsvc(alt, originOrStream) + + +* `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 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. + +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 + + +#### Event: 'altsvc' + + +* `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 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, streamId) => { + console.log(alt); + console.log(origin); + console.log(streamId); +}); +``` + +#### clienthttp2session.request(headers[, options]) + + +* `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 + 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 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 +will be emitted if the `getTrailers` callback attempts to set such header +fields. + +The `:method` and `:path` pseudo-headers are not specified within `headers`, +they respectively default to: + +* `:method` = `'GET'` +* `:path` = `/` + ### Class: Http2Stream 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' @@ -714,7 +872,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. +[HTTP/2 Headers Object][] and flags associated with the headers. ```js stream.on('trailers', (headers, flags) => { @@ -732,6 +890,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]) + + +* code {number} Unsigned 32-bit integer identifying the error code. **Default:** + `http2.constants.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 + + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance has been closed. + #### http2stream.destroyed + +* Value: {boolean} + +Set to `true` if the `Http2Stream` instance has not yet been assigned a +numeric stream identifier. + #### http2stream.priority(options) - -* 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() - - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x00` (No Error). - -#### http2stream.rstWithProtocolError() +#### http2stream.sentHeaders - -* Returns: {undefined} - -Shortcut for `http2stream.rstStream()` using error code `0x01` (Protocol Error). - -#### http2stream.rstWithCancel() - -* Returns: {undefined} +* Value: {HTTP/2 Headers Object} -Shortcut for `http2stream.rstStream()` using error code `0x08` (Cancel). +An object containing the outbound headers sent for this `Http2Stream`. -#### http2stream.rstWithRefuse() +#### http2stream.sentInfoHeaders -* Returns: {undefined} +* Value: {HTTP/2 Headers Object[]} -Shortcut for `http2stream.rstStream()` using error code `0x07` (Refused Stream). +An array of objects containing the outbound informational (additional) headers +sent for this `Http2Stream`. -#### http2stream.rstWithInternalError() +#### http2stream.sentTrailers -* Returns: {undefined} +* Value: {HTTP/2 Headers Object} -Shortcut for `http2stream.rstStream()` using error code `0x02` (Internal Error). +An object containing the outbound trailers sent for this this `HttpStream`. #### http2stream.session 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. -The listener callback is passed the [Headers Object][] and flags associated with -the headers. +for a stream, such as when a block of `1xx` informational headers is received. +The listener callback is passed the [HTTP/2 Headers Object][] and flags +associated with the headers. ```js stream.on('headers', (headers, flags) => { @@ -926,8 +1088,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 [HTTP/2 Headers Object][] and +flags associated with the headers. ```js stream.on('push', (headers, flags) => { @@ -943,7 +1105,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. +[HTTP/2 Headers Object][], and flags associated with the headers. For example: @@ -973,8 +1135,7 @@ provide additional methods such as `http2stream.pushStream()` and added: v8.4.0 --> -* `headers` {[Headers Object][]} -* Returns: {undefined} +* `headers` {HTTP/2 Headers Object} Sends an additional informational `HEADERS` frame to the connected HTTP/2 peer. @@ -1004,7 +1165,7 @@ accepts push streams, `false` otherwise. Settings are the same for every added: v8.4.0 --> -* `headers` {[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 @@ -1014,17 +1175,23 @@ 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` {HTTP/2 Headers Object} Headers object the pushStream was + initiated with. * 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, headers) => { + if (err) throw err; pushStream.respond({ ':status': 200 }); pushStream.end('some pushed data'); }); @@ -1041,7 +1208,7 @@ a `weight` value to `http2stream.priority` with the `silent` option set to added: v8.4.0 --> -* `headers` {[Headers Object][]} +* `headers` {HTTP/2 Headers Object} * `options` {Object} * `endStream` {boolean} Set to `true` to indicate that the response will not include payload data. @@ -1060,7 +1227,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 @@ -1077,7 +1244,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. @@ -1087,7 +1254,7 @@ added: v8.4.0 --> * `fd` {number} A readable file descriptor. -* `headers` {[Headers Object][]} +* `headers` {HTTP/2 Headers Object} * `options` {Object} * `statCheck` {Function} * `getTrailers` {Function} Callback function invoked to collect trailer @@ -1107,10 +1274,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, @@ -1118,8 +1285,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 @@ -1132,19 +1299,25 @@ 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 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 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, @@ -1156,12 +1329,13 @@ 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 -"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. @@ -1171,7 +1345,7 @@ added: v8.4.0 --> * `path` {string|Buffer|URL} -* `headers` {[Headers Object][]} +* `headers` {HTTP/2 Headers Object} * `options` {Object} * `statCheck` {Function} * `onError` {Function} Callback function invoked in the case of an @@ -1193,7 +1367,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: @@ -1253,7 +1427,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 @@ -1270,7 +1444,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. @@ -1281,18 +1455,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. +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' + + +* `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: 'socketError' +#### Event: 'request' -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' + + +The `'session'` event is emitted when a new `Http2Session` is created by the +`Http2Server`. #### Event: 'sessionError' 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' -* `socket` {ServerHttp2Stream} - -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' @@ -1344,24 +1547,32 @@ server.on('stream', (stream, headers, flags) => { }); ``` -#### Event: 'request' +#### Event: 'timeout' -* `request` {http2.Http2ServerRequest} -* `response` {http2.Http2ServerResponse} +The `'timeout'` event is emitted when there is no activity on the Server for +a given number of milliseconds set using `http2server.setTimeout()`. -Emitted each time there is a request. Note that there may be multiple requests -per session. See the [Compatibility API][]. +#### server.close([callback]) + +- `callback` {Function} -#### Event: 'timeout' +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 -The `'timeout'` event is emitted when there is no activity on the Server for -a given number of milliseconds set using `http2server.setTimeout()`. +* Extends: {tls.Server} #### Event: 'checkContinue' -* Extends: {tls.Server} - -#### Event: 'sessionError' - +* `request` {http2.Http2ServerRequest} +* `response` {http2.Http2ServerResponse} -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. +Emitted each time there is a request. Note that there may be multiple requests +per session. See the [Compatibility API][]. -#### Event: 'socketError' +#### Event: 'session' -The `'socketError'` event is emitted when a `'socketError'` event is emitted by -an `Http2Session` associated with the server. +The `'session'` event is emitted when a new `Http2Session` is created by the +`Http2SecureServer`. -#### Event: 'unknownProtocol' +#### Event: 'sessionError' -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 `'sessionError'` event is emitted when an `'error'` event is emitted by +an `Http2Session` object associated with the `Http2SecureServer`. #### Event: 'stream' -* `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' -#### Event: 'checkContinue' +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][]. + +#### server.close([callback]) +- `callback` {Function} -* `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. +Stops the server from accepting new connections. See [`tls.Server.close()`][]. -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. +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]) * `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, @@ -1526,25 +1735,54 @@ 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 + 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. * `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][]. - * `settings` {[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 + `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` + * `Http2ServerRequest` {http2.Http2ServerRequest} Specifies the + Http2ServerRequest class to use. + Useful for extending the original `Http2ServerRequest`. + **Default:** `Http2ServerRequest` + * `Http2ServerResponse` {http2.Http2ServerResponse} Specifies the + Http2ServerResponse class to use. + Useful for extending the original `Http2ServerResponse`. + **Default:** `Http2ServerResponse` * `onRequestHandler` {Function} See [Compatibility API][] * Returns: {Http2Server} 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) => { @@ -1578,6 +1816,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, @@ -1597,6 +1844,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 + 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. * `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 @@ -1604,7 +1858,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` {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. @@ -1654,6 +1908,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, @@ -1677,6 +1940,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 + 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. * `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 @@ -1684,16 +1954,16 @@ 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` {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 [`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'); @@ -1701,7 +1971,7 @@ const client = http2.connect('https://localhost:1234'); /** use the client **/ -client.destroy(); +client.close(); ``` ### http2.constants @@ -1737,7 +2007,7 @@ a given number of milliseconds set using `http2server.setTimeout()`. added: v8.4.0 --> -* Returns: {[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 @@ -1748,7 +2018,7 @@ so instances returned may be safely modified for use. added: v8.4.0 --> -* `settings` {[Settings Object][]} +* `settings` {HTTP/2 Settings Object} * Returns: {Buffer} Returns a `Buffer` instance containing serialized representation of the given @@ -1770,10 +2040,10 @@ added: v8.4.0 --> * `buf` {Buffer|Uint8Array} The packed settings. -* Returns: {[Settings Object][]} +* Returns: {HTTP/2 Settings Object} -Returns a [Settings Object][] containing the deserialized settings from the -given `Buffer` as generated by `http2.getPackedSettings()`. +Returns a [HTTP/2 Settings Object][] containing the deserialized settings from +the given `Buffer` as generated by `http2.getPackedSettings()`. ### Headers Object @@ -1838,8 +2108,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, 231-1 streams may be open - concurrently at any given time in an `Http2Session`. The minimum value is - 0. The maximum allowed value is 231-1. + concurrently at any given time in an `Http2Session`. The minimum value + is 0. The maximum allowed value is 231-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 232-1. **Default:** 65535. @@ -1957,6 +2227,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 +2235,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 +2247,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 +2276,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'); ``` @@ -2014,8 +2285,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. @@ -2036,14 +2307,14 @@ 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. ### 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 @@ -2085,7 +2356,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 @@ -2144,9 +2415,9 @@ Example: console.log(request.headers); ``` -See [Headers Object][]. +See [HTTP/2 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 @@ -2179,7 +2450,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 @@ -2800,15 +3071,80 @@ 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: + +* `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`. +* `streamAverageDuration` {number} The average duration (in milliseconds) for + all `Http2Stream` instances. +* `streamCount` {number} The number of `Http2Stream` instances processed by + the `Http2Session`. +* `type` {string} Either `'server'` or `'client'` to identify the type of + `Http2Session`. + + [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 +[HTTP/2 Unencrypted]: https://http2.github.io/faq/#does-http2-require-encryption +[HTTP/2 Headers Object]: #http2_headers_object +[HTTP/2 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 -[Settings Object]: #http2_settings_object +[RFC 7838]: https://tools.ietf.org/html/rfc7838 [Using options.selectPadding]: #http2_using_options_selectpadding [Writable Stream]: stream.html#stream_writable_streams [`'checkContinue'`]: #http2_event_checkcontinue @@ -2825,8 +3161,12 @@ if the stream is closed. [`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 [`net.connect()`]: net.html#net_net_connect [`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed [`response.end()`]: #http2_response_end_data_encoding_callback @@ -2836,6 +3176,7 @@ if the stream is closed. [`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 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`][]. -- `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/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) + + +Remove all performance entry objects with `entryType` equal to `name` from the +Performance Timeline. + ### performance.clearFunctions([name]) +```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. + + +```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. + + +```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 frame = 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. + + +```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 frame = new http2.HeadersFrame(id, payload, padlen, final); + +socket.write(frame.data); +``` + +### Class: SettingsFrame + +The `http2.SettingsFrame` is a subclass of `http2.Frame` that serializes an +empty `SETTINGS` frame. + + +```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`. + + +```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`. + + +```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. + + +```js +socket.write(http2.kClientMagic); +``` + ## Internet Module The `common/internet` module provides utilities for working with @@ -535,20 +658,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. - -## tmpdir Module - -The `tmpdir` module supports the use of a temporary directory for testing. - -### path -* [<String>] - -The realpath of the testing temporary directory. - -### refresh() - -Deletes and recreates the testing temporary directory. +variable `NODE_TEST_INET_HOST` to a specified host. ## WPT Module @@ -559,6 +669,7 @@ Node.js implementation with tests from [W3C Web Platform Tests](https://github.com/w3c/web-platform-tests). + [<Array>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array [<ArrayBufferView[]>]: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView [<Boolean>]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type diff --git a/test/common/http2.js b/test/common/http2.js new file mode 100644 index 00000000000000..1d4c269fffd5b5 --- /dev/null +++ b/test/common/http2.js @@ -0,0 +1,139 @@ +/* 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); + } +} + +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/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/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/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-accessor-properties.js b/test/parallel/test-accessor-properties.js new file mode 100644 index 00000000000000..13535ceda9667f --- /dev/null +++ b/test/parallel/test-accessor-properties.js @@ -0,0 +1,78 @@ +'use strict'; + +const common = 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; + +{ + // 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); + + // 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' + ); + + 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' + ); + } +} 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-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()); +})); 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-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..af67a0d0ae27db 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,20 @@ 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', (err) => { + common.expectsError({ + code: 'ERR_HTTP2_STREAM_CANCEL' + })(err); + common.expectsError({ + code: 'ERR_HTTP2_ERROR' + })(err.cause); + }); } - 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..aeb31949db074e 100644 --- a/test/parallel/test-http2-client-rststream-before-connect.js +++ b/test/parallel/test-http2-client-rststream-before-connect.js @@ -8,33 +8,61 @@ 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.listen(0, common.mustCall(() => { + const client = h2.connect(`http://localhost:${server.address().port}`); + const req = client.request(); + const closeCode = 1; -server.on('listening', common.mustCall(() => { + 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); - const client = h2.connect(`http://localhost:${server.address().port}`); + [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); + }); - const req = client.request({ ':path': '/' }); - req.rstStream(0); + 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.rstStream(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, 0); + assert.strictEqual(code, closeCode); server.close(); - client.destroy(); + client.close(); + })); + + req.on('error', common.expectsError({ + code: 'ERR_HTTP2_STREAM_ERROR', + type: Error, + message: `Stream closed with error code ${closeCode}` })); - 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-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(); + })); +} 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-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-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..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(() => { @@ -20,7 +23,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(); @@ -30,10 +33,36 @@ 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'; - 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-secure-session.js b/test/parallel/test-http2-create-client-secure-session.js index 811ef772d5903a..b0111e15b69c15 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({ @@ -30,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 }); @@ -39,6 +48,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'); @@ -53,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 149b5164231a21..963db2faa173b7 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 = '

this is some data

'; @@ -23,21 +25,34 @@ 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.on('close', common.mustCall()); + 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(101); - const client = h2.connect(`http://localhost:${this.address().port}`); + client.on('goaway', console.log); - const headers = { ':path': '/' }; + 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(common.mustCall()); + }); 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 +66,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..1575424d1609b4 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'); @@ -54,14 +56,23 @@ server.listen(0, common.mustCall(function() { req.resume(); req.on('end', common.mustCall(function() { - client.destroy(); + client.close(); testsFinished++; if (testsFinished === testsToRun) { - server.close(); + server.close(common.mustCall()); } })); + // 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(); }); })); 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-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(); + })); + }) + ); + })); +} diff --git a/test/parallel/test-http2-https-fallback.js b/test/parallel/test-http2-https-fallback.js index 04e9ca480c9099..a872d686d34f85 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'); @@ -31,7 +31,7 @@ function onRequest(request, response) { })); } -function onSession(session) { +function onSession(session, next) { const headers = { ':path': '/', ':method': 'GET', @@ -52,8 +52,12 @@ function onSession(session) { strictEqual(alpnProtocol, 'h2'); strictEqual(httpVersion, '2.0'); - session.destroy(); + session.close(); this.cleanup(); + + if (typeof next === 'function') { + next(); + } })); request.end(); } @@ -126,15 +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)); - - // Incompatible ALPN TLS client - tls(Object.assign({ port, ALPNProtocols: ['fake'] }, clientOptions)) - .on('error', common.mustCall(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(); + })); + } })); } 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..d69e0fd802979a 100644 --- a/test/parallel/test-http2-misbehaving-flow-control-paused.js +++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js @@ -56,32 +56,27 @@ let client; const server = h2.createServer({ settings: { initialWindowSize: 36 } }); server.on('stream', (stream) => { - - // Not reading causes the flow control window to get backed up. + // 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.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..161a88ea1fb407 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,25 @@ 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); - server.close(); + 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(common.mustCall()); client.destroy(); })); - + stream.resume(); stream.respond(); stream.end('ok'); }); +server.on('close', common.mustCall()); + 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-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())); +}); 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-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(); + })); +})); 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 = '

Hello World.

'; + 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(); +} 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-perf_hooks.js b/test/parallel/test-http2-perf_hooks.js new file mode 100644 index 00000000000000..e30d0ac83e0d1f --- /dev/null +++ b/test/parallel/test-http2-perf_hooks.js @@ -0,0 +1,110 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); +const assert = require('assert'); +const h2 = require('http2'); + +const { PerformanceObserver, performance } = require('perf_hooks'); + +const obs = new PerformanceObserver(common.mustCall((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'); + 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); + 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.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'); + } + performance.clearEntries('http2'); +}, 4)); +obs.observe({ entryTypes: ['http2'] }); + +const body = + '

this is some data

'; + +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(); + })); + +})); + +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); +}); 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-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-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()); + } +})); 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..5854c4fb8d02e4 100644 --- a/test/parallel/test-http2-respond-errors.js +++ b/test/parallel/test-http2-respond-errors.js @@ -5,88 +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}` - }; + // 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' + } + ); +})); - const client = http2.connect(url); - const req = client.request(headers); - - 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.destroy(); - - if (!tests.length) { - server.close(); - } else { - runTest(tests.shift()); - } + client.close(); + server.close(); })); -} + req.resume(); + req.end(); +})); 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-rststream-errors.js b/test/parallel/test-http2-respond-nghttperrors.js similarity index 78% rename from test/parallel/test-http2-rststream-errors.js rename to test/parallel/test-http2-respond-nghttperrors.js index eacf7855117503..5ec953c5442360 100644 --- a/test/parallel/test-http2-rststream-errors.js +++ b/test/parallel/test-http2-respond-nghttperrors.js @@ -1,5 +1,5 @@ -// Flags: --expose-http2 'use strict'; +// Flags: --expose-internals const common = require('../common'); if (!common.hasCrypto) @@ -10,11 +10,13 @@ const { Http2Stream, nghttp2ErrorString } = process.binding('http2'); +const { NghttpError } = require('internal/http2/util'); -// tests error handling within rstStream +// tests error handling within respond // - every other NGHTTP2 error from binding (should emit stream error) const specificTestKeys = []; + const specificTests = []; const genericTests = Object.getOwnPropertyNames(constants) @@ -25,7 +27,8 @@ const genericTests = Object.getOwnPropertyNames(constants) ngError: constants[key], error: { code: 'ERR_HTTP2_ERROR', - type: Error, + type: NghttpError, + name: 'Error [ERR_HTTP2_ERROR]', message: nghttp2ErrorString(constants[key]) }, type: 'stream' @@ -36,8 +39,8 @@ const tests = specificTests.concat(genericTests); let currentError; -// mock submitRstStream because we only care about testing error handling -Http2Stream.prototype.rstStream = () => currentError.ngError; +// 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) => { @@ -50,14 +53,14 @@ server.on('stream', common.mustCall((stream, headers) => { stream.session.on('error', errorMustNotCall); stream.on('error', errorMustCall); stream.on('error', common.mustCall(() => { - stream.session.destroy(); + stream.destroy(); })); } else { stream.session.once('error', errorMustCall); stream.on('error', errorMustNotCall); } - stream.rstStream(); + stream.respond(); }, tests.length)); server.listen(0, common.mustCall(() => runTest(tests.shift()))); @@ -74,17 +77,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(); - if (currentError.type === 'stream') { - req.on('error', common.mustCall()); - } - req.on('end', common.mustCall(() => { - client.destroy(); + client.close(); if (!tests.length) { server.close(); 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-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(); + }); +})); 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-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(); +} diff --git a/test/parallel/test-http2-session-settings.js b/test/parallel/test-http2-session-settings.js index 53ff44dd9ec2fe..75fcc1942104ac 100644 --- a/test/parallel/test-http2-session-settings.js +++ b/test/parallel/test-http2-session-settings.js @@ -68,71 +68,60 @@ 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 }, common.mustCall()); + }); - // 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-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(); 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/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); +})); 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..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.destroy(); - } - }, 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.destroy(); + 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-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()); + } + })); +})); 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-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js index 14e4ba23d8fdca..6ab8bcff02866e 100644 --- a/test/parallel/test-http2-util-update-options-buffer.js +++ b/test/parallel/test-http2-util-update-options-buffer.js @@ -19,7 +19,9 @@ 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_MAX_SESSION_MEMORY = 8; +const IDX_OPTIONS_FLAGS = 9; { updateOptionsBuffer({ @@ -29,7 +31,9 @@ const IDX_OPTIONS_FLAGS = 7; peerMaxConcurrentStreams: 4, paddingStrategy: 5, maxHeaderListPairs: 6, - maxOutstandingPings: 7 + maxOutstandingPings: 7, + maxOutstandingSettings: 8, + maxSessionMemory: 9 }); strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1); @@ -39,6 +43,8 @@ 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); + strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY], 9); const flags = optionsBuffer[IDX_OPTIONS_FLAGS]; @@ -49,6 +55,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/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-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; + } + })); +} 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/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(); + }); +}); 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); -} 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; } 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-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) 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()); + } +})); 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-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-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', () => {}); +})); 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(); })); })); 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'); 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',