Skip to content
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

add headers impl #38986

Closed
Closed
Show file tree
Hide file tree
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 Jun 10, 2021
b0f9a75
update headers and begin porting tests
Ethan-Arrowood Jun 10, 2021
499c1c6
add in progress test script
Ethan-Arrowood Jun 15, 2021
357ba5d
complete test migration
Ethan-Arrowood Jun 21, 2021
b34a484
add docs
Ethan-Arrowood Jun 21, 2021
f8f2059
fix ordering
Ethan-Arrowood Jun 21, 2021
b585716
lint fixes
Ethan-Arrowood Jun 21, 2021
865d422
Update doc/api/fetch.md
Ethan-Arrowood Jun 21, 2021
c96bf21
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
e7413b1
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
4d96cf4
Update test/parallel/test-headers.js
Ethan-Arrowood Jun 21, 2021
6b726a9
Update test/parallel/test-headers.js
Ethan-Arrowood Jun 21, 2021
c735d9e
use entries for iterator
Ethan-Arrowood Jun 21, 2021
173ccef
lint md
Ethan-Arrowood Jun 21, 2021
d856bd4
fix lint again
Ethan-Arrowood Jun 21, 2021
bed131e
add missing character
Ethan-Arrowood Jun 21, 2021
a87342f
Update doc/api/fetch.md
Ethan-Arrowood Jun 21, 2021
71c1aa2
Update doc/api/fetch.md
Ethan-Arrowood Jun 21, 2021
d5e3df3
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
c8d156a
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
8fdd64c
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
d66e313
fix lint and tests
Ethan-Arrowood Jun 21, 2021
1d042f0
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
0a58d93
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jun 21, 2021
a85b1c0
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jul 22, 2021
58da701
incorporate review and fix failing test
Ethan-Arrowood Jul 22, 2021
92b9519
export api
Ethan-Arrowood Jul 22, 2021
ce73c08
Merge branch 'master' into feature/fetch-headers
Ethan-Arrowood Jul 22, 2021
6f212c0
add inspect and docs
Ethan-Arrowood Jul 22, 2021
6f06698
Update lib/fetch.js
Ethan-Arrowood Jul 26, 2021
ae223ca
Update lib/fetch.js
Ethan-Arrowood Jul 26, 2021
1ef66a0
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jul 26, 2021
a9a7b4d
Update lib/internal/fetch/headers.js
Ethan-Arrowood Jul 26, 2021
3b902a1
incorporate review changes
Ethan-Arrowood Jul 26, 2021
1568b7c
Merge branch 'master' into feature/fetch-headers
Ethan-Arrowood Jul 26, 2021
cd38842
lint fixes
Ethan-Arrowood Jul 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions lib/internal/fetch/headers.js
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,
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
} = 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.
Show resolved Hide 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.
Show resolved Hide resolved

while (high > low) {
const mid = (high + low) >>> 1;

if (val.localeCompare(arr[mid * 2]) > 0) {
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
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.
Show resolved Hide resolved
validateHeaderValue(name, normalizedHeaderValue);
return normalizedHeaderValue;
}

function fill(headers, object) {
if (kHeadersList in object) {
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
// Object is instance of Headers
headers[kHeadersList] = new Array(...object[kHeadersList]);
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
} 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.
Show resolved Hide 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.
Show resolved Hide resolved
}
}

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.
Show resolved Hide 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.
Show resolved Hide resolved
}
}

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.
Show resolved Hide resolved
}

forEach(callback, thisArg) {
for (let index = 0; index < this[kHeadersList].length; index += 2) {
callback.call(
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved
thisArg,
this[kHeadersList][index + 1],
this[kHeadersList][index],
this
);
}
}
Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide resolved

Ethan-Arrowood marked this conversation as resolved.
Show resolved Hide 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.
Show resolved Hide resolved

module.exports = {
Headers,
kHeadersList
};
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
'lib/internal/errors.js',
'lib/internal/error_serdes.js',
'lib/internal/event_target.js',
'lib/internal/fetch/headers.js',
'lib/internal/fixed_queue.js',
'lib/internal/freelist.js',
'lib/internal/freeze_intrinsics.js',
Expand Down
126 changes: 126 additions & 0 deletions test/parallel/test-headers.js
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.
Show resolved Hide resolved
}

{
// 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.
Show resolved Hide resolved
}

{
// 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');
});
}