feat(http): add size limits to server connections and listen lifecycle hooks

This commit is contained in:
2026-05-19 19:39:01 +00:00
parent d4b448071e
commit 6f571c943b
6 changed files with 162 additions and 36 deletions
+52 -4
View File
@@ -11,6 +11,13 @@ export interface BodyReader extends Reader {
): void
}
export type BodyReaderOptions = {
/**
* @defaultValue Infinity
*/
maxBodyLength?: number
}
abstract class BaseBodyReader implements Pick<BodyReader, "onFinish"> {
#finishOkHandlers: (() => void | Promise<void>)[]
#finishKoHandlers: ((err: unknown) => void | Promise<void>)[]
@@ -105,11 +112,17 @@ export class ChunkedBodyReader extends BaseBodyReader implements BodyReader {
#chunks: Uint8Array[]
#finished: boolean
constructor(buffer: ReadBuffer) {
#bodyLength: number
#maxBodyLength: number
constructor(buffer: ReadBuffer, { maxBodyLength = Infinity }: BodyReaderOptions = {}) {
super()
this.#buffer = buffer
this.#chunks = []
this.#finished = false
this.#bodyLength = 0
this.#maxBodyLength = maxBodyLength
}
get closed(): boolean {
@@ -124,6 +137,9 @@ export class ChunkedBodyReader extends BaseBodyReader implements BodyReader {
const len = Number.parseInt(chunkLine, 16)
if (!Number.isInteger(len) || len < 0)
throw new Error(`Invalid chunk size: ${JSON.stringify(chunkLine)}`)
this.#bodyLength += len
if (this.#bodyLength > this.#maxBodyLength)
throw new Error(`Body too large: ${this.#bodyLength} (max: ${this.#maxBodyLength})`)
if (!len) {
this.#finished = true
await this.#buffer.read(2)
@@ -145,13 +161,39 @@ export class ChunkedBodyReader extends BaseBodyReader implements BodyReader {
}
}
export async function readHeaders(buffer: ReadBuffer): Promise<Headers> {
export type ReadHeadersOptions = {
/**
* @defaultValue 256
*/
maxHeaderCount?: number
/**
* @defaultValue Infinity
*/
maxTotalHeaderSize?: number
}
export async function readHeaders(
buffer: ReadBuffer,
{ maxHeaderCount = 256, maxTotalHeaderSize = Infinity }: ReadHeadersOptions = {},
): Promise<Headers> {
const rawHeaders: Record<string, string[]> = {}
let headerCount = 0
let totalHeaderSize = 0
while (true) {
const headerLine = await buffer.readLine()
if (!headerLine.length) {
break
}
headerCount++
if (headerCount > maxHeaderCount)
throw new Error(`Too many headers: ${headerCount} (max: ${maxHeaderCount})`)
totalHeaderSize += headerLine.length
if (totalHeaderSize > maxTotalHeaderSize)
throw new Error(
`Total max header size exceeded: ${totalHeaderSize} (max: ${maxTotalHeaderSize})`,
)
const colon = headerLine.indexOf(":")
if (colon === -1) throw new Error(`Invalid header line (no colon): ${headerLine.slice(0, 80)}`)
const key = headerLine.slice(0, colon)
@@ -164,13 +206,19 @@ export async function readHeaders(buffer: ReadBuffer): Promise<Headers> {
)
}
export function bodyReader(buffer: ReadBuffer, headers: Headers): BodyReader | null {
export function bodyReader(
buffer: ReadBuffer,
headers: Headers,
{ maxBodyLength = Infinity }: BodyReaderOptions = {},
): BodyReader | null {
if (headers.get("Transfer-Encoding") === "chunked") {
return new ChunkedBodyReader(buffer)
return new ChunkedBodyReader(buffer, { maxBodyLength })
} else if (typeof headers.get("Content-Length") === "string") {
const len = +(headers.get("Content-Length") as string)
if (!Number.isInteger(len) || len < 0)
throw new Error(`Content-Length is invalid: ${headers.get("Content-Length")}`)
if (len > maxBodyLength)
throw new Error(`Content-Length is too big: ${len} (max: ${maxBodyLength})`)
return new BasicBodyReader(buffer, len)
} else {
return null