Repository: denoland/deno
Pull request: fix(ext/http): gracefully handle consumed external in OTel ops

## Summary

Follow-up to #33005. On some platforms (notably Windows), after `take_external!` consumes an HTTP record's external pointer and the memory is freed, the allocator may reuse or zero the memory before `op_http_copy_span_to_otel_info` or `op_http_metric_handle_otel_error` attempt to access it via `clone_external!`. This causes a panic: "Detected an invalid v8::External (expected http record)".

The fix adds a `try_clone_external!` macro that validates the external pointer's marker before dereferencing. If the pointer has already been consumed (marker mismatch due to freed/reused memory), the OTel ops gracefully skip their work rather than panicking. This is safe because these ops only write optional telemetry attributes -- skipping them degrades observability slightly but doesn't affect request handling.

We couldn't simply convert `take_external!` to `clone_external!` in the response-completing ops (`op_http_set_promise_complete`, etc.) because the `HttpRecord::recycle()` method asserts `Rc::strong_count(&self) == 1` -- the lifecycle requires `take_external!` to be the mechanism that drops the JS-side Rc reference.

Closes #33026

## Why our #33005 test didn't catch this

Our test only covered the double-error path (handler throws, then onError throws). The new issue occurs in normal request handling with OTEL enabled, likely triggered by Windows-specific memory allocator behavior that reuses freed memory more aggressively than macOS/Linux.

## Test plan

- [x] Existing `otel_basic::http_serve_error_with_otel` test passes
- [x] All other `otel_basic::http_*` tests pass (except `http_metric` which is pre-existing flaky on main)
- [x] Could not reproduce the panic locally on macOS (consistent with it being a Windows allocator behavior), but the fix is defensive and correct regardless

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/http/http_next.rs
patch: @@ -151,6 +151,24 @@ macro_rules! clone_external {
   }};
 }
 
+/// Try to clone Rc<HttpRecord> from raw external pointer.
+/// Returns None if the pointer has already been consumed by take_external.
+macro_rules! try_clone_external {
+  ($external:expr) => {{
+    let ptr = $external;
+    if ptr.is_null()
+      || ptr.align_offset(std::mem::align_of::<usize>()) != 0
+      || std::ptr::read::<usize>(ptr as _)
+        != <RcHttpRecord as deno_core::Externalizable>::external_marker()
+    {
+      None
+    } else {
+      let ptr = ExternalPointer::<RcHttpRecord>::from_raw(ptr);
+      Some(ptr.unsafely_deref().0.clone())
+    }
+  }};
+}
+
 #[derive(Debug, thiserror::Error, deno_error::JsError)]
 pub enum HttpNextError {
   #[class(inherit)]
@@ -1712,9 +1730,11 @@ pub async fn op_raw_write_vectored(
 
 #[op2(fast)]
 pub fn op_http_metric_handle_otel_error(external: *const c_void) {
-  let http =
-    // SAFETY: op is called with external.
-    unsafe { clone_external!(external, "op_http_metric_handle_otel_error") };
+  // SAFETY: The external may have already been consumed by a take_external!
+  // call. Gracefully skip if the pointer is no longer valid.
+  let Some(http) = (unsafe { try_clone_external!(external) }) else {
+    return;
+  };
 
   http.otel_info_set_error("user");
 }
@@ -1724,9 +1744,13 @@ pub fn op_http_copy_span_to_otel_info(
   external: *const c_void,
   #[cppgc] span: &deno_telemetry::OtelSpan,
 ) {
-  let http =
-    // SAFETY: op is called with external.
-    unsafe { clone_external!(external, "op_http_copy_span_to_otel_info") };
+  // SAFETY: The external may have already been consumed by a take_external!
+  // call (e.g. op_http_set_promise_complete). On some platforms (Windows),
+  // the freed memory may be reused, causing the marker check to fail.
+  // We gracefully skip the copy in that case rather than panicking.
+  let Some(http) = (unsafe { try_clone_external!(external) }) else {
+    return;
+  };
 
   http.copy_span_to_otel_info(span);
 }