From ae9bbab0e7c67b04c74dea797b45f22f7cc65bb0 Mon Sep 17 00:00:00 2001 From: divybot Date: Mon, 27 Apr 2026 22:46:56 +0530 Subject: [PATCH] fix(ext/node): plumb llhttp error code, reason, and bytes parsed through HTTPParser Enables tests/node_compat/runner/suite/test/parallel/test-http-client-error-rawbytes.js Co-authored-by: Divy Srivastava --- ext/node/ops/llhttp/binding.rs | 55 ++++++++++++++++++- .../polyfills/internal_binding/http_parser.ts | 11 +++- tests/node_compat/config.jsonc | 1 + 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/ext/node/ops/llhttp/binding.rs b/ext/node/ops/llhttp/binding.rs index b9c0f22c0eba76..04d24617449a6d 100644 --- a/ext/node/ops/llhttp/binding.rs +++ b/ext/node/ops/llhttp/binding.rs @@ -81,6 +81,15 @@ struct Inner { header_overflow: bool, initialized: bool, + /// Bytes consumed up to (but not including) the parse error position + /// from the most recent execute() call. Set to 0 on success, valid + /// only after execute() returns -1. + last_bytes_parsed: u32, + /// llhttp errno from the most recent execute() / finish() call. + /// Used to surface specific error codes (e.g. HPE_INVALID_TRANSFER_ENCODING) + /// to JS instead of the generic HPE_ERROR. + last_errno: i32, + /// The stream being consumed (for parser.consume optimization). /// When set, the parser reads directly from the stream handle /// via a ReadInterceptor, bypassing the JS readable stream. @@ -137,6 +146,8 @@ impl HTTPParser { header_nread: 0, header_overflow: false, initialized: false, + last_bytes_parsed: 0, + last_errno: 0, consumed_stream: None, consume_callbacks: None, consume_isolate: v8::UnsafeRawIsolatePtr::null(), @@ -220,6 +231,9 @@ impl Inner { self.got_exception = false; self.pending_pause = false; self.header_nread = 0; + self.last_bytes_parsed = 0; + self.last_errno = sys::HPE_OK; + self.header_overflow = false; self.initialized = true; } @@ -819,6 +833,8 @@ unsafe fn consume_read_callback( } } } + inner.last_bytes_parsed = nread_result.max(0) as u32; + inner.last_errno = err as i32; if inner.pending_pause { inner.pending_pause = false; @@ -842,7 +858,7 @@ unsafe fn consume_read_callback( let is_error = inner.got_exception || (inner.parser.upgrade == 0 && err != sys::HPE_OK); let result_val: v8::Local = if is_error { - let bytes_parsed = data.len() as i32; + let bytes_parsed = nread_result.max(0); let parse_err = if inner.header_overflow { ParseError::HeaderOverflow { bytes_parsed } } else { @@ -974,6 +990,8 @@ impl HTTPParser { } } } + inner.last_bytes_parsed = nread as u32; + inner.last_errno = err as i32; if inner.pending_pause { inner.pending_pause = false; @@ -1063,6 +1081,41 @@ impl HTTPParser { self.inner().header_overflow } + /// Bytes consumed before the parse error. Set by the most recent + /// execute() call; only meaningful when execute() returned -1. + #[fast] + fn get_last_bytes_parsed(&self) -> u32 { + self.inner().last_bytes_parsed + } + + /// llhttp errno name (e.g. "HPE_INVALID_TRANSFER_ENCODING") for the + /// most recent execute() / finish() call. Returns "HPE_OK" when no + /// error is recorded. + #[string] + fn get_last_error_code(&self) -> String { + let errno = self.inner().last_errno; + let name_ptr = unsafe { sys::llhttp_errno_name(errno) }; + if name_ptr.is_null() { + return String::from("HPE_ERROR"); + } + let cstr = unsafe { CStr::from_ptr(name_ptr) }; + cstr.to_string_lossy().into_owned() + } + + /// Human-readable reason for the most recent parse error + /// (e.g. "Transfer-Encoding can't be present with Content-Length"). + /// Returns the empty string if no reason is set. + #[string] + fn get_last_error_reason(&self) -> String { + let inner = self.inner(); + let reason_ptr = unsafe { sys::llhttp_get_error_reason(&inner.parser) }; + if reason_ptr.is_null() { + return String::new(); + } + let cstr = unsafe { CStr::from_ptr(reason_ptr) }; + cstr.to_string_lossy().into_owned() + } + /// Get the current buffer being parsed (for error reporting). #[buffer] fn get_current_buffer(&self) -> Box<[u8]> { diff --git a/ext/node/polyfills/internal_binding/http_parser.ts b/ext/node/polyfills/internal_binding/http_parser.ts index bb5664111bb99a..80a54f7004aa6c 100644 --- a/ext/node/polyfills/internal_binding/http_parser.ts +++ b/ext/node/polyfills/internal_binding/http_parser.ts @@ -237,10 +237,15 @@ HTTPParser.prototype.execute = function ( err.reason = "Header overflow"; err.message = "Parse Error: Header overflow"; } else { - err.code = "HPE_ERROR"; - err.reason = "Parse Error"; + const code = this._native.getLastErrorCode(); + const reason = this._native.getLastErrorReason(); + err.code = code && code !== "HPE_OK" ? code : "HPE_ERROR"; + err.reason = reason || "Parse Error"; + if (reason) { + err.message = `Parse Error: ${reason}`; + } } - err.bytesParsed = data.byteLength; + err.bytesParsed = this._native.getLastBytesParsed(); return err; } diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 93f8ba78f88c57..50cc5068b110c7 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -1310,6 +1310,7 @@ "parallel/test-http-client-default-headers-exist.js": {}, "parallel/test-http-client-defaults.js": {}, "parallel/test-http-client-encoding.js": {}, + "parallel/test-http-client-error-rawbytes.js": {}, "parallel/test-http-client-finished.js": {}, "parallel/test-http-client-get-url.js": {}, "parallel/test-http-client-headers-array.js": {},