-
-
Notifications
You must be signed in to change notification settings - Fork 35.5k
add headers impl #38986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Ethan-Arrowood
wants to merge
36
commits into
nodejs:master
from
Ethan-Arrowood:feature/fetch-headers
+1,032
−18
Closed
add headers impl #38986
Changes from 2 commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
502e14b
add headers impl
Ethan-Arrowood b0f9a75
update headers and begin porting tests
Ethan-Arrowood 499c1c6
add in progress test script
Ethan-Arrowood 357ba5d
complete test migration
Ethan-Arrowood b34a484
add docs
Ethan-Arrowood f8f2059
fix ordering
Ethan-Arrowood b585716
lint fixes
Ethan-Arrowood 865d422
Update doc/api/fetch.md
Ethan-Arrowood c96bf21
Update lib/internal/fetch/headers.js
Ethan-Arrowood e7413b1
Update lib/internal/fetch/headers.js
Ethan-Arrowood 4d96cf4
Update test/parallel/test-headers.js
Ethan-Arrowood 6b726a9
Update test/parallel/test-headers.js
Ethan-Arrowood c735d9e
use entries for iterator
Ethan-Arrowood 173ccef
lint md
Ethan-Arrowood d856bd4
fix lint again
Ethan-Arrowood bed131e
add missing character
Ethan-Arrowood a87342f
Update doc/api/fetch.md
Ethan-Arrowood 71c1aa2
Update doc/api/fetch.md
Ethan-Arrowood d5e3df3
Update lib/internal/fetch/headers.js
Ethan-Arrowood c8d156a
Update lib/internal/fetch/headers.js
Ethan-Arrowood 8fdd64c
Update lib/internal/fetch/headers.js
Ethan-Arrowood d66e313
fix lint and tests
Ethan-Arrowood 1d042f0
Update lib/internal/fetch/headers.js
Ethan-Arrowood 0a58d93
Update lib/internal/fetch/headers.js
Ethan-Arrowood a85b1c0
Update lib/internal/fetch/headers.js
Ethan-Arrowood 58da701
incorporate review and fix failing test
Ethan-Arrowood 92b9519
export api
Ethan-Arrowood ce73c08
Merge branch 'master' into feature/fetch-headers
Ethan-Arrowood 6f212c0
add inspect and docs
Ethan-Arrowood 6f06698
Update lib/fetch.js
Ethan-Arrowood ae223ca
Update lib/fetch.js
Ethan-Arrowood 1ef66a0
Update lib/internal/fetch/headers.js
Ethan-Arrowood a9a7b4d
Update lib/internal/fetch/headers.js
Ethan-Arrowood 3b902a1
incorporate review changes
Ethan-Arrowood 1568b7c
Merge branch 'master' into feature/fetch-headers
Ethan-Arrowood cd38842
lint fixes
Ethan-Arrowood File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| 'use strict'; | ||
|
|
||
| const { | ||
| codes: { ERR_INVALID_ARG_VALUE }, | ||
| } = require('internal/errors'); | ||
|
|
||
| const { | ||
| ObjectEntries, | ||
| ArrayIsArray, | ||
| StringPrototypeToLocaleLowerCase, | ||
| StringPrototypeReplace, | ||
| Symbol, | ||
| SymbolIterator, | ||
| Array, | ||
| } = primordials; | ||
|
|
||
| const { validateObject } = require('internal/validators'); | ||
|
|
||
| const { isBoxedPrimitive } = require('internal/util/types'); | ||
|
|
||
| const { validateHeaderName, validateHeaderValue } = require('_http_outgoing'); | ||
|
|
||
| const { Buffer } = require('buffer'); | ||
| const console = require('console'); | ||
|
|
||
| const kHeadersList = Symbol('headers list'); | ||
|
|
||
| /** | ||
| * This algorithm is based off of | ||
| * https://www.tbray.org/ongoing/When/200x/2003/03/22/Binary | ||
| * It only operates on the even indexes of the array (the header names) by only | ||
|
Trott marked this conversation as resolved.
|
||
| * iterating at most half the length of the input array. The search also | ||
| * assumes all entries are strings and uses String.prototype.localeCompare for | ||
| * comparison. | ||
| */ | ||
| function binarySearch(arr, val) { | ||
| let low = 0; | ||
| let high = arr.length / 2; | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
|
|
||
| while (high > low) { | ||
| const mid = (high + low) >>> 1; | ||
|
|
||
| if (val.localeCompare(arr[mid * 2]) > 0) { | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| low = mid + 1; | ||
| } else { | ||
| high = mid; | ||
| } | ||
| } | ||
|
|
||
| return low * 2; | ||
| } | ||
|
|
||
| function normalizeAndValidateHeaderName(name) { | ||
| const normalizedHeaderName = StringPrototypeToLocaleLowerCase(name); | ||
| validateHeaderName(normalizedHeaderName); | ||
| return normalizedHeaderName; | ||
| } | ||
|
|
||
| function normalizeAndValidateHeaderValue(name, value) { | ||
| // https://fetch.spec.whatwg.org/#concept-header-value-normalize | ||
| const normalizedHeaderValue = StringPrototypeReplace( | ||
| value, | ||
| /^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, | ||
| '' | ||
| ); | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| validateHeaderValue(name, normalizedHeaderValue); | ||
| return normalizedHeaderValue; | ||
| } | ||
|
|
||
| function fill(headers, object) { | ||
| if (kHeadersList in object) { | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| // Object is instance of Headers | ||
| headers[kHeadersList] = new Array(...object[kHeadersList]); | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| } else if (ArrayIsArray(object)) { | ||
| // Support both 1D and 2D arrays of header entries | ||
| if (ArrayIsArray(object[0])) { | ||
| // Array of arrays | ||
| for (const header of object) { | ||
| if (header.length !== 2) { | ||
| throw new ERR_INVALID_ARG_VALUE('init', header, 'is not of length 2'); | ||
| } | ||
| headers.append(header[0], header[1]); | ||
| } | ||
| } else if (typeof object[0] === 'string' || Buffer.isBuffer(object[0])) { | ||
|
Ethan-Arrowood marked this conversation as resolved.
|
||
| // flat array of strings or Buffers | ||
| if (object.length % 2 !== 0) { | ||
| throw new ERR_INVALID_ARG_VALUE('init', object, 'is not even in length'); | ||
| } | ||
| for (let i = 0; i < object.length; i += 2) { | ||
| headers.append( | ||
| object[i].toString('utf-8'), | ||
| object[i + 1].toString('utf-8') | ||
| ); | ||
| } | ||
| } else { | ||
| // all other array based entries | ||
| throw new ERR_INVALID_ARG_VALUE( | ||
| 'init', | ||
| object, | ||
| 'is not a valid array entry' | ||
| ); | ||
| } | ||
| } else if (!isBoxedPrimitive(object)) { | ||
| // object of key/value entries | ||
| for (const { 0: name, 1: value } of ObjectEntries(object)) { | ||
| headers.append(name, value); | ||
| } | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| class Headers { | ||
| constructor(init) { | ||
| this[kHeadersList] = []; | ||
|
|
||
| if (init && validateObject(init, 'init', { allowArray: true }) === undefined) { | ||
| fill(this, init); | ||
| } | ||
| } | ||
|
|
||
| append(name, value) { | ||
|
Ethan-Arrowood marked this conversation as resolved.
|
||
| const normalizedName = normalizeAndValidateHeaderName(name); | ||
| const normalizedValue = normalizeAndValidateHeaderValue(name, value); | ||
|
|
||
| const index = binarySearch(this[kHeadersList], normalizedName); | ||
|
|
||
| if (this[kHeadersList][index] === normalizedName) { | ||
| this[kHeadersList][index + 1] += `, ${normalizedValue}`; | ||
| } else { | ||
| this[kHeadersList].splice(index, 0, normalizedName, normalizedValue); | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| delete(name) { | ||
| const normalizedName = normalizeAndValidateHeaderName(name); | ||
|
|
||
| const index = binarySearch(this[kHeadersList], normalizedName); | ||
|
|
||
| if (this[kHeadersList][index] === normalizedName) { | ||
| this[kHeadersList].splice(index, 2); | ||
| } | ||
| } | ||
|
|
||
| get(name) { | ||
| const normalizedName = normalizeAndValidateHeaderName(name); | ||
|
|
||
| const index = binarySearch(this[kHeadersList], normalizedName); | ||
|
|
||
| if (this[kHeadersList][index] === normalizedName) { | ||
| return this[kHeadersList][index + 1]; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| has(name) { | ||
| const normalizedName = normalizeAndValidateHeaderName(name); | ||
|
|
||
| const index = binarySearch(this[kHeadersList], normalizedName); | ||
|
|
||
| return this[kHeadersList][index] === normalizedName; | ||
| } | ||
|
|
||
| set(name, value) { | ||
| const normalizedName = normalizeAndValidateHeaderName(name); | ||
| const normalizedValue = normalizeAndValidateHeaderValue(name, value); | ||
|
|
||
| const index = binarySearch(this[kHeadersList], normalizedName); | ||
| if (this[kHeadersList][index] === normalizedName) { | ||
| this[kHeadersList][index + 1] = normalizedValue; | ||
| } else { | ||
| this[kHeadersList].splice(index, 2, normalizedName, normalizedValue); | ||
| } | ||
| } | ||
|
|
||
| *keys() { | ||
| for (const header of this) { | ||
| yield header[0]; | ||
| } | ||
| } | ||
|
|
||
| *values() { | ||
| for (const header of this) { | ||
| yield header[1]; | ||
| } | ||
| } | ||
|
|
||
| *entries() { | ||
| yield* this; | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| forEach(callback, thisArg) { | ||
| for (let index = 0; index < this[kHeadersList].length; index += 2) { | ||
| callback.call( | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| thisArg, | ||
| this[kHeadersList][index + 1], | ||
| this[kHeadersList][index], | ||
| this | ||
| ); | ||
| } | ||
| } | ||
|
Ethan-Arrowood marked this conversation as resolved.
|
||
|
|
||
|
Ethan-Arrowood marked this conversation as resolved.
|
||
| *[SymbolIterator]() { | ||
| for (let index = 0; index < this[kHeadersList].length; index += 2) { | ||
| yield [this[kHeadersList][index], this[kHeadersList][index + 1]]; | ||
| } | ||
| } | ||
| } | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
|
|
||
| module.exports = { | ||
| Headers, | ||
| kHeadersList | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
|
|
||
| // Flags: --expose-internals | ||
|
|
||
| const { Headers, kHeadersList } = require('internal/fetch/headers'); | ||
|
|
||
| { | ||
| // init is undefined | ||
| assert.deepStrictEqual(new Headers()[kHeadersList], []); | ||
| // Init is flat array with one entry | ||
| assert.deepStrictEqual( | ||
| new Headers(['test-name', 'test-value'])[kHeadersList], | ||
| ['test-name', 'test-value'] | ||
| ); | ||
| // Init is flat array with multiple entries | ||
| assert.deepStrictEqual( | ||
| new Headers(['test-name-1', 'test-value-1', 'test-name-2', 'test-value-2'])[ | ||
| kHeadersList | ||
| ], | ||
| ['test-name-1', 'test-value-1', 'test-name-2', 'test-value-2'] | ||
| ); | ||
| // Init is multidimensional array with one entry | ||
| assert.deepStrictEqual( | ||
| new Headers([['test-name-1', 'test-value-1']])[kHeadersList], | ||
| ['test-name-1', 'test-value-1'] | ||
| ); | ||
| // Init is multidimensional array with multiple entries | ||
| assert.deepStrictEqual( | ||
| new Headers([ | ||
| ['test-name-1', 'test-value-1'], | ||
| ['test-name-2', 'test-value-2'], | ||
| ])[kHeadersList], | ||
| ['test-name-1', 'test-value-1', 'test-name-2', 'test-value-2'] | ||
| ); | ||
| // Throws when init length is odd | ||
| assert.throws(() => { | ||
| new Headers(['test-name-1', 'test-value', 'test-name-2']); | ||
| }, "Error: The argument 'init' is not even in length. Received [ 'test-name-1', 'test-value', 'test-name-2' ]"); | ||
| // Throws when multidimensional init entry length is not 2 | ||
| assert.throws(() => { | ||
| new Headers([['test-name-1', 'test-value-1'], ['test-name-2']]); | ||
| }, "Error: The argument 'init' is not of length 2. Received [ 'test-name-2' ]"); | ||
| // Throws when init is not valid array input | ||
| assert.throws(() => { | ||
| new Headers([0, 1]); | ||
| }, "Error: The argument 'init' is not a valid array entry. Received [ 0, 1 ]"); | ||
| } | ||
|
|
||
| { | ||
| // Init is object with single entry | ||
| const headers = new Headers({ | ||
| 'test-name-1': 'test-value-1', | ||
| }); | ||
| assert.strictEqual(headers[kHeadersList].length, 2); | ||
| } | ||
|
|
||
| { | ||
| // Init is object with multiple entries | ||
| const headers = new Headers({ | ||
| 'test-name-1': 'test-value-1', | ||
| 'test-name-2': 'test-value-2', | ||
| }); | ||
| assert.strictEqual(headers[kHeadersList].length, 4); | ||
| } | ||
|
|
||
| { | ||
| // Init fails silently when initialized with BoxedPrimitives | ||
| try { | ||
| new Headers(new Number()); | ||
| new Headers(new Boolean()); | ||
| new Headers(new String()); | ||
| } catch (error) { | ||
| common.mustNotCall(error); | ||
| } | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| { | ||
| // Init fails silently if function or primitive is passed | ||
| try { | ||
| new Headers(Function); | ||
| new Headers(function() {}); | ||
| new Headers(1); | ||
| new Headers('test'); | ||
| } catch (error) { | ||
| common.mustNotCall(error); | ||
| } | ||
|
Ethan-Arrowood marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| { | ||
| // headers append | ||
| const headers = new Headers(); | ||
| headers.append('test-name-1', 'test-value-1'); | ||
| assert.deepStrictEqual(headers[kHeadersList], [ | ||
| 'test-name-1', | ||
| 'test-value-1', | ||
| ]); | ||
| headers.append('test-name-2', 'test-value-2'); | ||
| assert.deepStrictEqual(headers[kHeadersList], [ | ||
| 'test-name-1', | ||
| 'test-value-1', | ||
| 'test-name-2', | ||
| 'test-value-2', | ||
| ]); | ||
| headers.append('test-name-1', 'test-value-3'); | ||
| assert.deepStrictEqual(headers[kHeadersList], [ | ||
| 'test-name-1', | ||
| 'test-value-1, test-value-3', | ||
| 'test-name-2', | ||
| 'test-value-2', | ||
| ]); | ||
|
|
||
| assert.throws(() => { | ||
| headers.append(); | ||
| }); | ||
|
|
||
| assert.throws(() => { | ||
| headers.append('test-name'); | ||
| }); | ||
|
|
||
| assert.throws(() => { | ||
| headers.append('invalid @ header ? name', 'test-value'); | ||
| }); | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.