39 lines
1.5 KiB
TypeScript
39 lines
1.5 KiB
TypeScript
import type { RequestHeaders } from '../../routes/types.ts';
|
|
import { ApplicationError } from '../../errors/index.ts';
|
|
import { HTTP_STATUS_CODE_BAD_REQUEST } from '../../constants.ts';
|
|
|
|
/** RFC 9110 field-name token grammar. */
|
|
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
|
|
/**
|
|
* Validate and normalize request headers at a transport boundary.
|
|
*
|
|
* Lowercase names give HTTP and WebSocket routes identical lookup semantics.
|
|
* Case-insensitive duplicates are rejected instead of selecting an ambiguous
|
|
* authentication value. Header values may not contain line breaks.
|
|
*/
|
|
export const normalizeRequestHeaders = (headers: Readonly<Record<string, string>>): RequestHeaders => {
|
|
const normalizedEntries: Array<[string, string]> = [];
|
|
const names = new Set<string>();
|
|
|
|
for (const [ name, value ] of Object.entries(headers)) {
|
|
if (!HEADER_NAME_PATTERN.test(name)) {
|
|
throw new ApplicationError(HTTP_STATUS_CODE_BAD_REQUEST, 'Invalid request header name');
|
|
}
|
|
|
|
if (value.includes('\r') || value.includes('\n')) {
|
|
throw new ApplicationError(HTTP_STATUS_CODE_BAD_REQUEST, 'Invalid request header value');
|
|
}
|
|
|
|
const normalizedName = name.toLowerCase();
|
|
if (names.has(normalizedName)) {
|
|
throw new ApplicationError(HTTP_STATUS_CODE_BAD_REQUEST, 'Duplicate request header name');
|
|
}
|
|
|
|
names.add(normalizedName);
|
|
normalizedEntries.push([ normalizedName, value ]);
|
|
}
|
|
|
|
return Object.freeze(Object.fromEntries(normalizedEntries));
|
|
};
|