GeezerVison is showing github.com — http2: add support for raw header arrays in h2Stream.respond() · nodejs/node@dafee05
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/http2.md +10-1Lines changed: 10 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1080,6 +1080,11 @@ changes: | 1080 | 1080 | pr-url: https://github.com/nodejs/node/pull/58313 | 1081 | 1081 | description: Following the deprecation of priority signaling as of RFC 9113, | 1082 | 1082 | `weight` option is deprecated. | | 1083 | + - version: | | 1084 | + - v24.0.0 | | 1085 | + - v22.17.0 | | 1086 | + pr-url: https://github.com/nodejs/node/pull/57917 | | 1087 | + description: Allow passing headers in raw array format. | 1083 | 1088 | --> | 1084 | 1089 |
| 1085 | 1090 | * `headers` {HTTP/2 Headers Object|Array} | @@ -1850,14 +1855,18 @@ and will throw an error. | 1850 | 1855 | <!-- YAML | 1851 | 1856 | added: v8.4.0 | 1852 | 1857 | changes: | | 1858 | + - version: | | 1859 | + - REPLACEME | | 1860 | + pr-url: https://github.com/nodejs/node/pull/59455 | | 1861 | + description: Allow passing headers in raw array format. | 1853 | 1862 | - version: | 1854 | 1863 | - v14.5.0 | 1855 | 1864 | - v12.19.0 | 1856 | 1865 | pr-url: https://github.com/nodejs/node/pull/33160 | 1857 | 1866 | description: Allow explicitly setting date headers. | 1858 | 1867 | --> | 1859 | 1868 |
| 1860 | | -* `headers` {HTTP/2 Headers Object} | | 1869 | +* `headers` {HTTP/2 Headers Object|Array} | 1861 | 1870 | * `options` {Object} | 1862 | 1871 | * `endStream` {boolean} Set to `true` to indicate that the response will not | 1863 | 1872 | include payload data. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/http2/core.js +82-17Lines changed: 82 additions & 17 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -2541,8 +2541,31 @@ function callStreamClose(stream) { | 2541 | 2541 | stream.close(); | 2542 | 2542 | } | 2543 | 2543 |
| 2544 | | -function processHeaders(oldHeaders, options) { | 2545 | | - assertIsObject(oldHeaders, 'headers'); | | 2544 | +function prepareResponseHeaders(stream, headersParam, options) { | | 2545 | + let headers; | | 2546 | + let statusCode; | | 2547 | + | | 2548 | + if (ArrayIsArray(headersParam)) { | | 2549 | + ({ | | 2550 | + headers, | | 2551 | + statusCode, | | 2552 | + } = prepareResponseHeadersArray(headersParam, options)); | | 2553 | + stream[kRawHeaders] = headers; | | 2554 | + } else { | | 2555 | + ({ | | 2556 | + headers, | | 2557 | + statusCode, | | 2558 | + } = prepareResponseHeadersObject(headersParam, options)); | | 2559 | + stream[kSentHeaders] = headers; | | 2560 | + } | | 2561 | + | | 2562 | + const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse); | | 2563 | + | | 2564 | + return { headers, headersList, statusCode }; | | 2565 | +} | | 2566 | + | | 2567 | +function prepareResponseHeadersObject(oldHeaders, options) { | | 2568 | + assertIsObject(oldHeaders, 'headers', ['Object', 'Array']); | 2546 | 2569 | const headers = { __proto__: null }; | 2547 | 2570 |
| 2548 | 2571 | if (oldHeaders !== null && oldHeaders !== undefined) { | @@ -2563,23 +2586,58 @@ function processHeaders(oldHeaders, options) { | 2563 | 2586 | headers[HTTP2_HEADER_DATE] ??= utcDate(); | 2564 | 2587 | } | 2565 | 2588 |
| | 2589 | + validatePreparedResponseHeaders(headers, statusCode); | | 2590 | + | | 2591 | + return { | | 2592 | + headers, | | 2593 | + statusCode: headers[HTTP2_HEADER_STATUS], | | 2594 | + }; | | 2595 | +} | | 2596 | + | | 2597 | +function prepareResponseHeadersArray(headers, options) { | | 2598 | + let statusCode; | | 2599 | + let isDateSet = false; | | 2600 | + | | 2601 | + for (let i = 0; i < headers.length; i += 2) { | | 2602 | + const header = headers[i].toLowerCase(); | | 2603 | + const value = headers[i + 1]; | | 2604 | + | | 2605 | + if (header === HTTP2_HEADER_STATUS) { | | 2606 | + statusCode = value | 0; | | 2607 | + } else if (header === HTTP2_HEADER_DATE) { | | 2608 | + isDateSet = true; | | 2609 | + } | | 2610 | + } | | 2611 | + | | 2612 | + if (!statusCode) { | | 2613 | + statusCode = HTTP_STATUS_OK; | | 2614 | + headers.unshift(HTTP2_HEADER_STATUS, statusCode); | | 2615 | + } | | 2616 | + | | 2617 | + if (!isDateSet && (options.sendDate == null || options.sendDate)) { | | 2618 | + headers.push(HTTP2_HEADER_DATE, utcDate()); | | 2619 | + } | | 2620 | + | | 2621 | + validatePreparedResponseHeaders(headers, statusCode); | | 2622 | + | | 2623 | + return { headers, statusCode }; | | 2624 | +} | | 2625 | + | | 2626 | +function validatePreparedResponseHeaders(headers, statusCode) { | 2566 | 2627 | // This is intentionally stricter than the HTTP/1 implementation, which | 2567 | 2628 | // allows values between 100 and 999 (inclusive) in order to allow for | 2568 | 2629 | // backwards compatibility with non-spec compliant code. With HTTP/2, | 2569 | 2630 | // we have the opportunity to start fresh with stricter spec compliance. | 2570 | 2631 | // This will have an impact on the compatibility layer for anyone using | 2571 | 2632 | // non-standard, non-compliant status codes. | 2572 | 2633 | if (statusCode < 200 || statusCode > 599) | 2573 | | - throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]); | | 2634 | + throw new ERR_HTTP2_STATUS_INVALID(statusCode); | 2574 | 2635 |
| 2575 | 2636 | const neverIndex = headers[kSensitiveHeaders]; | 2576 | 2637 | if (neverIndex !== undefined && !ArrayIsArray(neverIndex)) | 2577 | 2638 | throw new ERR_INVALID_ARG_VALUE('headers[http2.neverIndex]', neverIndex); | 2578 | | - | 2579 | | - return headers; | 2580 | 2639 | } | 2581 | 2640 |
| 2582 | | - | 2583 | 2641 | function onFileUnpipe() { | 2584 | 2642 | const stream = this.sink[kOwner]; | 2585 | 2643 | if (stream.ownsFd) | @@ -2882,7 +2940,7 @@ class ServerHttp2Stream extends Http2Stream { | 2882 | 2940 | } | 2883 | 2941 |
| 2884 | 2942 | // Initiate a response on this Http2Stream | 2885 | | - respond(headers, options) { | | 2943 | + respond(headersParam, options) { | 2886 | 2944 | if (this.destroyed || this.closed) | 2887 | 2945 | throw new ERR_HTTP2_INVALID_STREAM(); | 2888 | 2946 | if (this.headersSent) | @@ -2907,15 +2965,16 @@ class ServerHttp2Stream extends Http2Stream { | 2907 | 2965 | state.flags |= STREAM_FLAGS_HAS_TRAILERS; | 2908 | 2966 | } | 2909 | 2967 |
| 2910 | | - headers = processHeaders(headers, options); | 2911 | | - const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse); | 2912 | | - this[kSentHeaders] = headers; | | 2968 | + const { | | 2969 | + headers, | | 2970 | + headersList, | | 2971 | + statusCode, | | 2972 | + } = prepareResponseHeaders(this, headersParam, options); | 2913 | 2973 |
| 2914 | 2974 | state.flags |= STREAM_FLAGS_HEADERS_SENT; | 2915 | 2975 |
| 2916 | 2976 | // Close the writable side if the endStream option is set or status | 2917 | 2977 | // is one of known codes with no payload, or it's a head request | 2918 | | - const statusCode = headers[HTTP2_HEADER_STATUS] | 0; | 2919 | 2978 | if (!!options.endStream || | 2920 | 2979 | statusCode === HTTP_STATUS_NO_CONTENT || | 2921 | 2980 | statusCode === HTTP_STATUS_RESET_CONTENT || | @@ -2945,7 +3004,7 @@ class ServerHttp2Stream extends Http2Stream { | 2945 | 3004 | // regular file, here the fd is passed directly. If the underlying | 2946 | 3005 | // mechanism is not able to read from the fd, then the stream will be | 2947 | 3006 | // reset with an error code. | 2948 | | - respondWithFD(fd, headers, options) { | | 3007 | + respondWithFD(fd, headersParam, options) { | 2949 | 3008 | if (this.destroyed || this.closed) | 2950 | 3009 | throw new ERR_HTTP2_INVALID_STREAM(); | 2951 | 3010 | if (this.headersSent) | @@ -2982,8 +3041,11 @@ class ServerHttp2Stream extends Http2Stream { | 2982 | 3041 | this[kUpdateTimer](); | 2983 | 3042 | this.ownsFd = false; | 2984 | 3043 |
| 2985 | | - headers = processHeaders(headers, options); | 2986 | | - const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; | | 3044 | + const { | | 3045 | + headers, | | 3046 | + statusCode, | | 3047 | + } = prepareResponseHeadersObject(headersParam, options); | | 3048 | + | 2987 | 3049 | // Payload/DATA frames are not permitted in these cases | 2988 | 3050 | if (statusCode === HTTP_STATUS_NO_CONTENT || | 2989 | 3051 | statusCode === HTTP_STATUS_RESET_CONTENT || | @@ -3011,7 +3073,7 @@ class ServerHttp2Stream extends Http2Stream { | 3011 | 3073 | // giving the user an opportunity to verify the details and set additional | 3012 | 3074 | // headers. If statCheck returns false, the operation is aborted and no | 3013 | 3075 | // file details are sent. | 3014 | | - respondWithFile(path, headers, options) { | | 3076 | + respondWithFile(path, headersParam, options) { | 3015 | 3077 | if (this.destroyed || this.closed) | 3016 | 3078 | throw new ERR_HTTP2_INVALID_STREAM(); | 3017 | 3079 | if (this.headersSent) | @@ -3042,8 +3104,11 @@ class ServerHttp2Stream extends Http2Stream { | 3042 | 3104 | this[kUpdateTimer](); | 3043 | 3105 | this.ownsFd = true; | 3044 | 3106 |
| 3045 | | - headers = processHeaders(headers, options); | 3046 | | - const statusCode = headers[HTTP2_HEADER_STATUS] |= 0; | | 3107 | + const { | | 3108 | + headers, | | 3109 | + statusCode, | | 3110 | + } = prepareResponseHeadersObject(headersParam, options); | | 3111 | + | 3047 | 3112 | // Payload/DATA frames are not permitted in these cases | 3048 | 3113 | if (statusCode === HTTP_STATUS_NO_CONTENT || | 3049 | 3114 | statusCode === HTTP_STATUS_RESET_CONTENT || | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/http2/util.js +3-4Lines changed: 3 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -690,7 +690,6 @@ function prepareRequestHeadersArray(headers, session) { | 690 | 690 | const headersList = buildNgHeaderString( | 691 | 691 | rawHeaders, | 692 | 692 | assertValidPseudoHeader, | 693 | | - headers[kSensitiveHeaders], | 694 | 693 | ); | 695 | 694 |
| 696 | 695 | return { | @@ -755,14 +754,14 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE); | 755 | 754 | * @returns {[string, number]} | 756 | 755 | */ | 757 | 756 | function buildNgHeaderString(arrayOrMap, | 758 | | - assertValuePseudoHeader = assertValidPseudoHeader, | 759 | | - sensitiveHeaders = arrayOrMap[kSensitiveHeaders]) { | | 757 | + assertValuePseudoHeader = assertValidPseudoHeader) { | 760 | 758 | let headers = ''; | 761 | 759 | let pseudoHeaders = ''; | 762 | 760 | let count = 0; | 763 | 761 |
| 764 | 762 | const singles = new SafeSet(); | 765 | | - const neverIndex = (sensitiveHeaders || emptyArray).map((v) => v.toLowerCase()); | | 763 | + const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray; | | 764 | + const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase()); | 766 | 765 |
| 767 | 766 | function processHeader(key, value) { | 768 | 767 | key = key.toLowerCase(); | |
Collapse file Copy file name to clipboard +76Lines changed: 76 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | +if (!common.hasCrypto) | | 5 | + common.skip('missing crypto'); | | 6 | +const assert = require('assert'); | | 7 | +const http2 = require('http2'); | | 8 | + | | 9 | +{ | | 10 | + const server = http2.createServer(); | | 11 | + server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => { | | 12 | + assert.deepStrictEqual(rawHeaders, [ | | 13 | + ':method', 'GET', | | 14 | + ':authority', `localhost:${server.address().port}`, | | 15 | + ':scheme', 'http', | | 16 | + ':path', '/', | | 17 | + 'a', 'b', | | 18 | + 'x-foo', 'bar', // Lowercased as required for HTTP/2 | | 19 | + 'a', 'c', // Duplicate header order preserved | | 20 | + ]); | | 21 | + stream.respond([ | | 22 | + 'x', '1', | | 23 | + 'x-FOO', 'bar', | | 24 | + 'x', '2', | | 25 | + ]); | | 26 | + | | 27 | + assert.partialDeepStrictEqual(stream.sentHeaders, { | | 28 | + '__proto__': null, | | 29 | + ':status': 200, | | 30 | + 'x': [ '1', '2' ], | | 31 | + 'x-FOO': 'bar', | | 32 | + }); | | 33 | + | | 34 | + assert.strictEqual(typeof stream.sentHeaders.date, 'string'); | | 35 | + | | 36 | + stream.end(); | | 37 | + })); | | 38 | + | | 39 | + | | 40 | + server.listen(0, common.mustCall(() => { | | 41 | + const port = server.address().port; | | 42 | + const client = http2.connect(`http://localhost:${port}`); | | 43 | + | | 44 | + const req = client.request([ | | 45 | + 'a', 'b', | | 46 | + 'x-FOO', 'bar', | | 47 | + 'a', 'c', | | 48 | + ]).end(); | | 49 | + | | 50 | + assert.deepStrictEqual(req.sentHeaders, { | | 51 | + '__proto__': null, | | 52 | + ':path': '/', | | 53 | + ':scheme': 'http', | | 54 | + ':authority': `localhost:${server.address().port}`, | | 55 | + ':method': 'GET', | | 56 | + 'a': [ 'b', 'c' ], | | 57 | + 'x-FOO': 'bar', | | 58 | + }); | | 59 | + | | 60 | + req.on('response', common.mustCall((_headers, _flags, rawHeaders) => { | | 61 | + assert.strictEqual(rawHeaders.length, 10); | | 62 | + assert.deepStrictEqual(rawHeaders.slice(0, 8), [ | | 63 | + ':status', '200', | | 64 | + 'x', '1', | | 65 | + 'x-foo', 'bar', // Lowercased as required for HTTP/2 | | 66 | + 'x', '2', // Duplicate header order preserved | | 67 | + ]); | | 68 | + | | 69 | + assert.strictEqual(rawHeaders[8], 'date'); | | 70 | + assert.strictEqual(typeof rawHeaders[9], 'string'); | | 71 | + | | 72 | + client.close(); | | 73 | + server.close(); | | 74 | + })); | | 75 | + })); | | 76 | +} |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http2-raw-headers.js +33-13Lines changed: 33 additions & 13 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -8,19 +8,33 @@ const http2 = require('http2'); | 8 | 8 |
| 9 | 9 | { | 10 | 10 | const server = http2.createServer(); | 11 | | - server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => { | | 11 | + server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => { | 12 | 12 | assert.deepStrictEqual(rawHeaders, [ | 13 | 13 | ':path', '/foobar', | 14 | 14 | ':scheme', 'http', | 15 | | - ':authority', `localhost:${server.address().port}`, | 16 | | - ':method', 'GET', | | 15 | + ':authority', `test.invalid:${server.address().port}`, | | 16 | + ':method', 'POST', | 17 | 17 | 'a', 'b', | 18 | | - 'x-foo', 'bar', | 19 | | - 'a', 'c', | | 18 | + 'x-foo', 'bar', // Lowercased as required for HTTP/2 | | 19 | + 'a', 'c', // Duplicate header order preserved | | 20 | + ]); | | 21 | + | | 22 | + stream.respond([ | | 23 | + ':status', '404', | | 24 | + 'x', '1', | | 25 | + 'x-FOO', 'bar', | | 26 | + 'x', '2', | | 27 | + 'DATE', '0000', | 20 | 28 | ]); | 21 | | - stream.respond({ | 22 | | - ':status': 200 | | 29 | + | | 30 | + assert.deepStrictEqual(stream.sentHeaders, { | | 31 | + '__proto__': null, | | 32 | + ':status': '404', | | 33 | + 'x': [ '1', '2' ], | | 34 | + 'x-FOO': 'bar', | | 35 | + 'DATE': '0000', | 23 | 36 | }); | | 37 | + | 24 | 38 | stream.end(); | 25 | 39 | })); | 26 | 40 |
| @@ -32,8 +46,8 @@ const http2 = require('http2'); | 32 | 46 | const req = client.request([ | 33 | 47 | ':path', '/foobar', | 34 | 48 | ':scheme', 'http', | 35 | | - ':authority', `localhost:${server.address().port}`, | 36 | | - ':method', 'GET', | | 49 | + ':authority', `test.invalid:${server.address().port}`, | | 50 | + ':method', 'POST', | 37 | 51 | 'a', 'b', | 38 | 52 | 'x-FOO', 'bar', | 39 | 53 | 'a', 'c', | @@ -43,14 +57,20 @@ const http2 = require('http2'); | 43 | 57 | '__proto__': null, | 44 | 58 | ':path': '/foobar', | 45 | 59 | ':scheme': 'http', | 46 | | - ':authority': `localhost:${server.address().port}`, | 47 | | - ':method': 'GET', | | 60 | + ':authority': `test.invalid:${server.address().port}`, | | 61 | + ':method': 'POST', | 48 | 62 | 'a': [ 'b', 'c' ], | 49 | 63 | 'x-FOO': 'bar', | 50 | 64 | }); | 51 | 65 |
| 52 | | - req.on('response', common.mustCall((headers) => { | 53 | | - assert.strictEqual(headers[':status'], 200); | | 66 | + req.on('response', common.mustCall((_headers, _flags, rawHeaders) => { | | 67 | + assert.deepStrictEqual(rawHeaders, [ | | 68 | + ':status', '404', | | 69 | + 'x', '1', | | 70 | + 'x-foo', 'bar', // Lowercased as required for HTTP/2 | | 71 | + 'x', '2', // Duplicate header order preserved | | 72 | + 'date', '0000', // Server doesn't automatically set its own value | | 73 | + ]); | 54 | 74 | client.close(); | 55 | 75 | server.close(); | 56 | 76 | })); | |
|