-
Notifications
You must be signed in to change notification settings - Fork 570
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
"preprocessed" headers #3998
Comments
I would call it ImmutableRequestHeaders to not confuse it with the Headers object from the fetch spec. |
What do we need to solve this problem?
It turns out that all the request data will be prepared in advance and all that remains is to write them to the socket without unnecessary checks. I want to hear your opinion on this import { isValidHeaderName, isValidHeaderValue } from './utils.ts';
interface IHTTPHeader {
name: string;
values: string[];
};
interface IHTTPHeaders {
// TODO:
}
const kHeadersMap = Symbol('headers map');
const kCachable = Symbol('cachable');
const cacheWMap: WeakMap<HTTPHeaders, string> = new WeakMap();
export class HTTPHeader implements IHTTPHeader {
name: string;
values: string[];
constructor(name: string, value: string | string[]) {
this.name = name;
this.values = typeof value === 'string' ? [value] : value.slice();
}
append(value: string | string[]) {
const { values } = this;
if (typeof value === 'string') {
values.push(value);
} else {
values.length = (values.length + value.length);
for (let i = 0; i < value.length; i++) {
values.push(value[i]);
}
}
}
clone() {
return new HTTPHeader(this.name, this.values.slice());
}
* entries() {
const { name: rawName, values } = this;
const name = rawName.toLowerCase();
if (values.length === 1) {
yield [rawName, values[0]];
}
else if (name === 'set-cookie') {
for (let index = 0; index < values.length; index++) {
yield [rawName, values[index]];
}
}
else {
const separator = name === 'cookie' ? '; ' : ', ';
yield [rawName, values.join(separator)];
}
}
*[Symbol.iterator]() {
return this.entries();
}
toString() {
let raw = ``;
for (const [name, value] of this.entries()) {
raw += `${name}: ${value}\r\n`;
}
return raw;
}
}
type HTTPHeadersInit = [string, string][] | Record<string, string> | Iterable<[string, string]> | HTTPHeaders | Headers;
export class HTTPHeaders implements IHTTPHeaders {
[kCachable] = false;
[kHeadersMap]: Map<string, IHTTPHeader> = new Map();
static extend(h: HTTPHeaders, init: HTTPHeadersInit) {
for (const [name, value] of new HTTPHeaders(init)) {
h.append(name, value);
}
return h;
}
constructor(init?: HTTPHeadersInit) {
if (!init || typeof init !== 'object') return;
const map = this[kHeadersMap];
if (init instanceof HTTPHeaders) {
for (const [name, header] of init[kHeadersMap].entries()) {
map.set(name, copyHeader(header));
}
}
else if (Symbol.iterator in init) {
for (const [name, value] of init) {
this.append(name, value);
}
}
else {
for (const [name, value] of Object.entries(init)) {
this.append(name, value);
}
}
}
clone() {
return new HTTPHeaders(this);
}
extend(init: HTTPHeadersInit = {}) {
const h = this.clone();
for (const [name, value] of new HTTPHeaders(init)) {
h.append(name, value);
}
return h;
}
assign(...init: HTTPHeadersInit[]) {
const h = this.clone();
for (const entry of init) {
for (const [name, value] of new HTTPHeaders(entry)) {
h.append(name, value);
}
}
return h;
}
set(name: string, value: string, raw: boolean = true) {
name = name.trim();
if (!isValidHeaderName(name)) {
throw new Error(`"${name}" is an invalid header name.`);
}
value = value.trim();
if (!isValidHeaderValue(value)) {
throw new Error(`"${value}" is an invalid header value.`);
}
const lower = name.toLowerCase();
const map = this[kHeadersMap];
map.set(lower, new HTTPHeader(raw ? name : lower, value));
return this[kCachable] ? this.cache() : this;
}
append(name: string, value: string, raw: boolean = true) {
const lower = name.toLowerCase();
const map = this[kHeadersMap];
// Проверяем наличие
if (map.has(lower)) {
value = value.trim();
if (!isValidHeaderValue(value)) {
throw new Error(`"${value}" is an invalid header value.`);
}
appendHeaderValues(map.get(lower)!, value);
} else {
name = name.trim();
if (!isValidHeaderName(name)) {
throw new Error(`"${name}" is an invalid header name.`);
}
value = value.trim();
if (!isValidHeaderValue(value)) {
throw new Error(`"${value}" is an invalid header value.`);
}
map.set(lower, new HTTPHeader(raw ? name : lower, value));
}
return this[kCachable] ? this.cache() : this;
}
has(name: string) {
return this[kHeadersMap].has(name.toLowerCase());
}
get(name: string) {
return this[kHeadersMap].get(name.toLowerCase());
}
delete(name: string) {
const deleted = this[kHeadersMap].delete(name.toLowerCase());
if (this[kCachable]) this.cache();
return deleted;
}
*entries(raw: boolean = true) {
for (const [name, header] of this[kHeadersMap]) {
const { name: rawName, values } = header;
const headerName = raw ? rawName : name;
if (values.length === 1) {
yield [headerName, values[0]];
}
else if (name === 'set-cookie') {
for (let index = 0; index < values.length; index++) {
yield [headerName, values[index]];
}
}
else {
const separator = name === 'cookie' ? '; ' : ', ';
yield [headerName, values.join(separator)];
}
}
}
keys() {
return this[kHeadersMap].keys();
}
values(name?: string) {
if (!name) {
return this[kHeadersMap].values().map(h => h.values)!;
} else {
return this[kHeadersMap].get(name)?.values ?? [];
}
}
getSetCookie() {
return (this.values('set-cookie') ?? []) as string[];
}
[Symbol.iterator]() {
return this.entries();
}
cache() {
this[kCachable] = true;
cacheWMap.set(this, this.build());
return this;
}
build() {
let raw = ``;
for (const [name, value] of this.entries()) {
raw += `${name}: ${value}\r\n`;
}
return raw;
}
toString() {
return cacheWMap.has(this) ? cacheWMap.get(this)! : this.build();
}
}
export function createHeader(name: string, value: string | string[]): IHTTPHeader {
return new HTTPHeader(name, value);
}
export function appendHeaderValues(h: IHTTPHeader, value: string | string[]) {
const { values } = h;
if (typeof value === 'string') {
values.push(value);
} else {
values.length = (values.length + value.length);
for (let i = 0; i < value.length; i++) {
values.push(value[i]);
}
}
}
export function copyHeader(h: IHTTPHeader) {
const { name, values } = h;
return new HTTPHeader(name, values.slice());
} |
Caching the finished header string greatly reduces the load import { bench, run, summary } from 'mitata';
import { HTTPHeaders } from './HTTPHeaders.ts';
const arr = Array.from({ length: 20 }, (_, i) => [`x-user-${i}`, `${i}`]);
const h = new HTTPHeaders(arr);
const h2 = h.clone().cache();
summary(() => {
bench('without cache', () => {
h.toString();
});
bench('with cache', () => {
h2.toString();
});
bench('forof', () => {
let a = '';
for (const [name, value] of h) {
a += `${name}: ${value}\r\n`;
}
});
bench('forof noop', () => {
for (const [name, value] of h) {}
});
});
run();
|
That's the approach I use in https://github.com/mcollina/autocannon. |
we need a similar approach, only write the request body separately, since there may be large buffer, streams, generators, and so on that do not need to be dumped into memory by default. |
Like the idea/approach for the immutable headers concept; one thing that grasped my attention was the each header being represented with a class. Do we need such fine grain representation? Overall, we can target first |
I totally agree with you, we can just add caching to the current headers implementation. And we won't have to change much. When assembling a request, it would be good for us to bring the headers to the same type (HTTP Headers) and then use the methods of this structure, adding system headers (host, connection, content-length, ...) and so on. |
When using the same headers for multiple requests it would be nice to bypass much of the header validation and transformation logic we do:
i.e. an API like this should be possible:
Where
undici.Headers
guarantee that the headers are already in a valid state and we don't need to do anything further. Could even be pre computed into a headers string.To start with I would make
undici.Headers
immutable. Later we could add immutable composition, e.g:A quick start (just move logic from core/request constructor and make request access the state through symbols):
Refs: #3994
The text was updated successfully, but these errors were encountered: