Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 118 additions & 1 deletion src/http/lshpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,79 @@ pub enum HpackError {
UnableToEncode,
}

/// The fields of one outbound header block, staged for [`HPACK::encode_block`].
#[derive(Default)]
pub struct HeaderBlock {
/// Each name followed by its value, in field order.
bytes: Vec<u8>,
/// Two entries per field: the name length with `never_index` in bit 31, then the value length.
fields: Vec<u32>,
}

impl HeaderBlock {
const NEVER_INDEX: u32 = 1 << 31;

pub fn push(
&mut self,
name: &[u8],
value: &[u8],
never_index: bool,
) -> Result<(), bun_alloc::AllocError> {
// A length that does not fit its entry fails like a reservation of that size.
let (Ok(name_len), Ok(value_len)) = (u32::try_from(name.len()), u32::try_from(value.len()))
else {
return Err(bun_alloc::AllocError);
};
if name_len >= Self::NEVER_INDEX {
return Err(bun_alloc::AllocError);
}
self.bytes
.try_reserve(name.len() + value.len())
.map_err(|_| bun_alloc::AllocError)?;
self.fields
.try_reserve(2)
.map_err(|_| bun_alloc::AllocError)?;
self.bytes.extend_from_slice(name);
self.bytes.extend_from_slice(value);
self.fields.push(if never_index {
name_len | Self::NEVER_INDEX
} else {
name_len
});
self.fields.push(value_len);
Ok(())
}

pub fn clear(&mut self) {
self.bytes.clear();
self.fields.clear();
}

/// Number of fields.
pub fn len(&self) -> usize {
self.fields.len() / 2
}

pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}

/// Heap bytes the block keeps while it is empty.
pub fn capacity(&self) -> usize {
self.bytes.capacity() + self.fields.capacity() * core::mem::size_of::<u32>()
}

/// https://github.com/nodejs/node/blob/v26.3.0/deps/nghttp2/lib/nghttp2_hd.c#L1578-L1603
pub fn deflate_bound(&self) -> usize {
12 + 12 * self.len() + self.bytes.len()
}

/// Output space [`HPACK::encode_block`] asks for.
pub fn encode_bound(&self) -> usize {
self.bytes.len() + 32 * self.len()
}
}

impl HPACK {
/// `name` and `value` point into a thread-local buffer that every `HPACK` on this thread decodes and encodes
/// through. The borrow only stops this instance from overwriting them.
Expand Down Expand Up @@ -85,9 +158,44 @@ impl HPACK {
})
}

/// Encodes all of `block` into `dst`'s spare capacity, or none: `Err` leaves the table as is.
pub fn encode_block(
&mut self,
block: &HeaderBlock,
dst: &mut Vec<u8>,
) -> Result<(), HpackError> {
if block.is_empty() {
return Ok(());
}
let spare = dst.spare_capacity_mut();
// SAFETY: `HeaderBlock::push` is the only writer of `bytes` and `fields`, so the lengths
// in `fields` add up to `bytes`. The C side writes at most `spare.len()` bytes at `spare`.
let written = unsafe {
lshpack_wrapper_encode_block(
self,
block.bytes.as_ptr(),
block.fields.as_ptr(),
block.len(),
spare.as_mut_ptr().cast::<u8>(),
spare.len(),
)
};
match written {
0 => Err(HpackError::UnableToEncode),
// lshpack could not allocate a table entry after it inserted earlier fields.
usize::MAX => bun_core::out_of_memory(),
written => {
// SAFETY: the C side initialized `written <= spare.len()` bytes of the spare capacity.
unsafe { dst.set_len(dst.len() + written) };
Ok(())
}
}
}

/// encode name, value with never_index option into dst_buffer
/// if name + value length is greater than LSHPACK_MAX_HEADER_SIZE this will return UnableToEncode
pub fn encode(
/// A later field can fail after earlier ones are in the table: see [`Self::encode_block`].
pub(crate) fn encode(
&mut self,
name: &[u8],
value: &[u8],
Expand Down Expand Up @@ -235,4 +343,13 @@ unsafe extern "C" {
buffer_len: usize,
buffer_offset: usize,
) -> usize;
// `fields` holds `field_count` pairs of u32 that describe `bytes` (see `HeaderBlock`).
fn lshpack_wrapper_encode_block(
self_: &mut HPACK,
bytes: *const u8,
fields: *const u32,
field_count: usize,
dst: *mut u8,
dst_len: usize,
) -> usize;
}
18 changes: 4 additions & 14 deletions src/js/node/http2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,18 +449,6 @@ function emitSessionCloseNT(self: Http2Session, frame) {
runInFrame(frame, self.emit, self, "close");
}
}
function emitErrorNT(self: any, error: any, destroy: boolean) {
if (destroy) {
if (self.listenerCount("error") > 0) {
self.destroy(error);
} else {
self.destroy();
}
} else if (self.listenerCount("error") > 0) {
self.emit("error", error);
}
}

function emitOutofStreamErrorNT(self: any) {
self.destroy($ERR_HTTP2_OUT_OF_STREAMS());
}
Expand Down Expand Up @@ -3545,7 +3533,8 @@ class ServerHttp2Stream extends Http2Stream {
ArrayPrototypePush.$call(this[kInfoHeaders], headers);
}

session[bunHTTP2Native]?.request(this.id, undefined, headers, sensitiveNames);
// The last argument marks the block as informational: a refusal leaves the stream open for respond().
session[bunHTTP2Native]?.request(this.id, undefined, headers, sensitiveNames, undefined, true);
}
respond(headers?: HeadersObject | any[] | null, options?: any) {
if (this.destroyed || this.session === undefined) {
Expand Down Expand Up @@ -6313,9 +6302,10 @@ class ClientHttp2Session extends Http2Session {
process.nextTick(emitEventNT, req, "ready");
return req;
} catch (e: any) {
// Nothing reached the wire, so the session stays usable and the throw is the only error channel.
if (connectionsCounted) {
this.#connections--;
process.nextTick(emitErrorNT, this, e, this.#connections === 0 && this.#closed);
if (this.#connections === 0 && this.#closed) setImmediate(destroyIfNotDestroyedNT, this);
}
throw e;
}
Expand Down
35 changes: 35 additions & 0 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,41 @@ size_t lshpack_wrapper_encode(lshpack_wrapper* self,
return ptr - start;
}

// Returns the bytes written, 0 if the block is refused (encoder untouched), or SIZE_MAX if lshpack ran out of memory mid-block.
size_t lshpack_wrapper_encode_block(lshpack_wrapper* self,
const unsigned char* bytes, const uint32_t* fields, size_t field_count,
unsigned char* dst, size_t dst_len)
{
size_t needed = 0;
for (size_t i = 0; i < field_count; i++) {
size_t name_len = fields[i * 2] & 0x7fffffffu;
size_t val_len = fields[i * 2 + 1];
if (name_len > LSXPACK_MAX_STRLEN || val_len > LSXPACK_MAX_STRLEN || name_len + val_len > LSHPACK_MAX_HEADER_SIZE)
return 0;
needed += name_len + val_len + 32;
}
if (dst_len < needed)
return 0;

unsigned char* cursor = dst;
unsigned char* const end = dst + dst_len;
for (size_t i = 0; i < field_count; i++) {
size_t name_len = fields[i * 2] & 0x7fffffffu;
size_t val_len = fields[i * 2 + 1];
lsxpack_header_t hdr;
lsxpack_header_set_offset2(&hdr, reinterpret_cast<const char*>(bytes), 0, name_len, name_len, val_len);
if (fields[i * 2] >> 31) {
hdr.indexed_type = 2;
}
auto* next = lshpack_enc_encode(&self->enc, cursor, end, &hdr);
if (next == cursor)
return SIZE_MAX;
cursor = next;
bytes += name_len + val_len;
}
return cursor - dst;
}

size_t lshpack_wrapper_decode(lshpack_wrapper* self,
const unsigned char* src, size_t src_len,
lshpack_header* output)
Expand Down
28 changes: 28 additions & 0 deletions src/jsc/rare_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ pub struct RareData {

/// `node:http2` PADDED DATA scratch; see [`Self::take_h2_padded_frame_buffer`].
h2_padded_frame_buffer: Option<Box<H2PaddedFrameBuffer>>,
/// `node:http2` outbound header block scratch; see [`Self::take_h2_header_scratch`].
h2_header_scratch: Option<H2HeaderScratch>,
/// Output scratch for one JS-thread `CompressionStream` step; see [`Self::take_compression_scratch`].
compression_scratch: Option<Vec<u8>>,
/// Inflated payload of one `new WebSocket()` client message; see [`Self::take_websocket_inflate_scratch`].
Expand Down Expand Up @@ -303,6 +305,7 @@ impl Default for RareData {
listening_sockets_for_watch_mode: Mutex::new(Vec::new()),
pipe_read_scratch: Box::new(bun_event_loop::PipeReadScratch::new()),
h2_padded_frame_buffer: None,
h2_header_scratch: None,
compression_scratch: None,
websocket_inflate_scratch: None,
libdeflate_decompressor: None,
Expand Down Expand Up @@ -356,6 +359,13 @@ impl PathBuf {
/// One max-size HTTP/2 PADDED DATA frame payload (pad-length byte + data + padding).
pub type H2PaddedFrameBuffer = [u8; 16384];

/// One outbound `node:http2` header block: the staged fields and their HPACK encoding.
#[derive(Default)]
pub struct H2HeaderScratch {
pub block: bun_http::lshpack::HeaderBlock,
pub encoded: Vec<u8>,
}

// ──────────────────────────────────────────────────────────────────────────
// ProxyEnvStorage
// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -744,6 +754,24 @@ impl RareData {
self.h2_padded_frame_buffer.get_or_insert(buffer);
}

/// By value, like [`Self::take_h2_padded_frame_buffer`]: a value's `toString` can re-enter.
pub fn take_h2_header_scratch(&mut self) -> H2HeaderScratch {
self.h2_header_scratch.take().unwrap_or_default()
}

/// Hand a taken scratch back; the slot keeps the first one returned and lets an oversized one go.
pub fn put_back_h2_header_scratch(&mut self, mut scratch: H2HeaderScratch) {
const KEEP: usize = 64 * 1024;
if self.h2_header_scratch.is_none()
&& scratch.block.capacity() <= KEEP
&& scratch.encoded.capacity() <= KEEP
{
scratch.block.clear();
scratch.encoded.clear();
self.h2_header_scratch = Some(scratch);
}
}

/// Empty `Vec` with whatever capacity the last step left behind.
pub fn take_compression_scratch(&mut self) -> Vec<u8> {
self.compression_scratch.take().unwrap_or_default()
Expand Down
Loading
Loading