diff --git a/ext/node/polyfills/http.ts b/ext/node/polyfills/http.ts index b35c336a..1757e2d0 100644 --- a/ext/node/polyfills/http.ts +++ b/ext/node/polyfills/http.ts @@ -496,14 +496,18 @@ class ClientRequest extends OutgoingMessage { const traceId = internals.getRequestTraceId?.(); const isTraced = traceId !== null && traceId !== undefined; + if (isTraced) { + (headers as [string, string][]).push([internals.FETCH_TRACE_ID_HEADER, traceId]); + } const rlKey = isTraced ? traceId : ""; + const effectivelyTraced = isTraced; const allowed = op_check_outbound_rate_limit( parsedUrl.href, rlKey, - isTraced, + effectivelyTraced, ); if (!allowed) { - const msg = isTraced + const msg = effectivelyTraced ? `Rate limit exceeded for trace ${rlKey}` : `Rate limit exceeded for function`; throw new Deno.errors.RateLimitError(msg); diff --git a/ext/runtime/js/http.js b/ext/runtime/js/http.js index 5c19aa2f..539cdab9 100644 --- a/ext/runtime/js/http.js +++ b/ext/runtime/js/http.js @@ -1,7 +1,10 @@ import "ext:deno_http/01_http.js"; import { core, internals, primordials } from "ext:core/mod.js"; -import { enterRequestContext } from "ext:runtime/request_context.js"; +import { + enterRequestContext, + FETCH_TRACE_ID_HEADER, +} from "ext:runtime/request_context.js"; import { RequestPrototype } from "ext:deno_fetch/23_request.js"; import { fromInnerResponse, @@ -226,13 +229,20 @@ async function respond(requestEvent, httpConn, options, snapshot) { /** @type {Response} */ let response; - const traceParent = requestEvent.request.headers.get("traceparent"); - let traceId = null; - if (traceParent) { - // traceparent format: 00-{trace-id}-{parent-id}-{flags} - const parts = traceParent.split("-"); - if (parts.length >= 4 && parts[1].length === 32) { - traceId = parts[1]; + // x-er-fetch-trace-id takes priority: injected by the runtime's fetch / + // node:http into every outbound request, so it is tamper-proof once set. + // On the very first hop it won't be present, so we fall back to parsing + // the W3C traceparent header. + let traceId = + requestEvent.request.headers.get(FETCH_TRACE_ID_HEADER) ?? null; + if (traceId === null) { + const traceParent = requestEvent.request.headers.get("traceparent"); + if (traceParent) { + // traceparent format: 00-{trace-id}-{parent-id}-{flags} + const parts = traceParent.split("-"); + if (parts.length >= 4 && parts[1].length === 32) { + traceId = parts[1]; + } } } const prevCtx = enterRequestContext(traceId); diff --git a/ext/runtime/js/request_context.js b/ext/runtime/js/request_context.js index 4fdda590..7105f09b 100644 --- a/ext/runtime/js/request_context.js +++ b/ext/runtime/js/request_context.js @@ -4,6 +4,11 @@ const { AsyncVariable } = core; const requestTraceIdVar = new AsyncVariable(); +// Header injected by the runtime's fetch / node:http into every outbound +// request to propagate the request-local trace ID for rate limiting. +// Defined once here and shared via internals to avoid scattered string literals. +const FETCH_TRACE_ID_HEADER = "x-er-fetch-trace-id"; + function enterRequestContext(traceId) { return requestTraceIdVar.enter(traceId); } @@ -14,5 +19,6 @@ function getRequestTraceId() { internals.enterRequestContext = enterRequestContext; internals.getRequestTraceId = getRequestTraceId; +internals.FETCH_TRACE_ID_HEADER = FETCH_TRACE_ID_HEADER; -export { enterRequestContext, getRequestTraceId }; +export { enterRequestContext, FETCH_TRACE_ID_HEADER, getRequestTraceId }; diff --git a/vendor/deno_fetch/26_fetch.js b/vendor/deno_fetch/26_fetch.js index 7ddd5b45..baa4115c 100644 --- a/vendor/deno_fetch/26_fetch.js +++ b/vendor/deno_fetch/26_fetch.js @@ -396,16 +396,22 @@ function fetch(input, init = { __proto__: null }) { } // Rate limit check. + // Inject x-er-fetch-trace-id so the next hop can read the rate-limit + // key independently of the OTel traceparent (which user code can alter). const traceId = internals.getRequestTraceId?.(); const isTraced = traceId !== null && traceId !== undefined; + if (isTraced) { + requestObject.headers.set(internals.FETCH_TRACE_ID_HEADER, traceId); + } const rlKey = isTraced ? traceId : ""; + const effectivelyTraced = isTraced; const allowed = op_check_outbound_rate_limit( requestObject.url, rlKey, - isTraced, + effectivelyTraced, ); if (!allowed) { - const msg = isTraced + const msg = effectivelyTraced ? `Rate limit exceeded for trace ${rlKey}` : `Rate limit exceeded for function`; reject(new Deno.errors.RateLimitError(msg));