fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12) - #35

Merged
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes
May 15, 2026
Merged

fix(interceptor): surgical fixes for Request body bytes + http.request overload edges (#10, #12)#35
AndresL230 merged 9 commits into
mainfrom
feat/10-12-interceptor-fixes

Conversation

@AndresL230

@AndresL230AndresL230 commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Wave 3 — two narrow correctness bugs in src/core/interceptor.ts, plus the leftover roadmap docs maintenance from Wave 2.

  • fetch(new Request(url, { body })) reports requestBytes: 0 #12fetch(new Request(url, { body })) reported requestBytes: 0 because estimateRequestBytes only inspected init.body. The body on a constructed Request is a ReadableStream by then, and Request.headers.get("content-length") is unreliable on Node's undici fetch (empirically verified — undici sets the header on the wire but not on Request.headers). Fix: estimateRequestBytes becomes async, clones the Request, and reads cloned.arrayBuffer() for the byte count. The clone tees the body stream, so the original Request still feeds the actual outgoing HTTP request. Deliberate contract change: stream-bodied Requests now report actual bytes (the issue's intent) at the cost of materializing the body in memory (~2× peak for the body).
  • #10ahttp.request(URL, { path }) silently dropped options.path. Fix: extractUrl gains an optional pathOverride; the wrapper computes it from second-arg options.path when the first arg is a URL/string.
  • #10bopts.host containing an embedded :port plus a separate opts.port produced an unparseable URL (host:port:port), silently skipping instrumentation. Fix: strip any embedded port from opts.host before appending opts.port.

No public API changes. No new exports. No changes to the install/uninstall lifecycle or the double-count guard.

Roadmap

First commit also marks Waves 1 + 2 as done (links to merged PRs #33 and #34) and flips Wave 3 to in-progress — leftover docs maintenance bundled into this PR.

Commits

  1. docs: mark waves 1+2 done; add wave 3 surgical fixes plan (#10, #12) — roadmap + plan doc.
  2. docs(plans): revise #12 approach to async clone+arrayBuffer — plan revision after empirically discovering the original sync content-length-header approach didn't work on undici-backed fetch.
  3. fix(interceptor): measure Request body via clone+arrayBuffer for fetch (#12) — Task 2 initial fix.
  4. fix(interceptor): skip body measurement on parse failure; expand #12 comments+tests — Task 2 review fix-up (avoid orphaning the clone-and-measure promise on URL parse failure; tighten worst-case-cost comment; add regression test for already-consumed Request).
  5. fix(interceptor): honor options.path when first arg is URL (#10) — Task 3 (#10a impl + #10b impl bundled, regression tests for 10a only).
  6. test(interceptor): strip embedded port from opts.host (#10) — Task 4 (#10b regression tests).

Test plan

  • npm run lint clean
  • npm run build emits ESM + CJS + DTS
  • npm run test240/240 vitest (baseline 230 + 10 new; total includes the 7 dist smoke tests; test:dist re-runs those 7 separately after build)

Plan

See docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md.

Closes#10, closes#12.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved request body byte estimation for both fetch and HTTP requests.
    • Enhanced URL and path parsing for more accurate request routing.
    • Fixed hostname normalization when embedded ports are present.
  • Tests

    • Added comprehensive test coverage for request body measurement and URL/path parsing scenarios.
  • Documentation

    • Added detailed Wave 3 implementation plan with specifications and roadmap updates.

Review Change Stack

AndresL230and others added 6 commits May 15, 2026 02:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirical testing on Node 26 showed Request.headers.get("content-length")
returns null for undici-backed fetch — undici sets the header on the wire
but never on the Request.headers object. The sync recovery path the plan
originally specified was rejected.
Switching to async Request.clone() + arrayBuffer() (user-approved Option A).
Deliberately changes the contract for stream-bodied Requests: was 0, now
actual bytes — which is what the issue explicitly asks for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@AndresL230 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 6 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 842f15d0-6778-443e-b317-241c1fc55586

📥 Commits

Reviewing files that changed from the base of the PR and between b40b061 and 6065521.

📒 Files selected for processing (3)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts
📝 Walkthrough

Walkthrough

This PR implements Wave 3 of the interceptor roadmap: three targeted correctness fixes to request body byte estimation for fetch with Request objects, support for options.path overrides in http.request with URL first arguments, and defensive hostname normalization to strip embedded ports.

Changes

Interceptor Surgical Fixes

Layer / File(s)Summary
Implementation Plan and Wave Status
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md, docs/superpowers/roadmap-2026-05-13-issue-waves.md
Detailed Wave 3 implementation plan with task breakdown, TDD test specs, and exact commit instructions; roadmap updated to mark Waves 1–2 done and Wave 3 in-progress.
URL and Path Extraction Refinements
src/core/interceptor.ts (extractUrl, http.request wrapper), tests/interceptor.test.ts
extractUrl now strips embedded ports from hostname/host fields and supports pathOverride to replace parsed pathname; http.request wrapper detects URL-like first arguments and passes options.path as override; tests validate path override behavior and host/port normalization edge cases.
Async Request Body Measurement for Fetch
src/core/interceptor.ts (estimateRequestBytes, patchedFetch, error handling), tests/interceptor.test.ts
estimateRequestBytes clones Request objects and reads arrayBuffer asynchronously to measure body bytes without consuming the original; fetch wrapper launches measurement early and awaits before emitting telemetry on both success and error; tests cover Request body variants, init.body overrides, zero-byte cases, streaming bodies, and already-consumed bodies.

Sequence Diagrams

sequenceDiagram
participant HttpRequestWrapper as http.request wrapper
participant ExtractUrl as extractUrl
participant Telemetry as Telemetry event
HttpRequestWrapper->>HttpRequestWrapper: Detect URL-like first arg<br/>and RequestOptions.path
HttpRequestWrapper->>ExtractUrl: Call with url and pathOverride
ExtractUrl->>ExtractUrl: Parse URL, normalize hostname<br/>(strip embedded port)
ExtractUrl->>ExtractUrl: Apply pathOverride to pathname<br/>if provided
ExtractUrl->>HttpRequestWrapper: Return normalized url/path
HttpRequestWrapper->>Telemetry: Record with corrected path
Loading
sequenceDiagram
participant PatchedFetch as patchedFetch
participant EstimateRequestBytes as estimateRequestBytes
participant Telemetry as Telemetry event
PatchedFetch->>PatchedFetch: Validate instrumentation<br/>will be recorded
PatchedFetch->>EstimateRequestBytes: Start requestBytesPromise<br/>(async)
PatchedFetch->>PatchedFetch: Call fetch(input, init)
alt Success or HTTP error
PatchedFetch->>EstimateRequestBytes: Await promise
EstimateRequestBytes-->>PatchedFetch: Return requestBytes
PatchedFetch->>Telemetry: Emit success event<br/>with requestBytes
else Fetch fails
PatchedFetch->>EstimateRequestBytes: Await promise in catch
EstimateRequestBytes-->>PatchedFetch: Return requestBytes (0 if clone failed)
PatchedFetch->>Telemetry: Emit error event<br/>with requestBytes
end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Three surgical strikes we've made with care,
Request bytes now measured everywhere,
Path overrides honored, ports no longer double,
The interceptor's steady—no more hidden trouble! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 66.67% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main changes: surgical fixes for Request body bytes and http.request overload edges, with specific issue references (#10, #12).
Linked Issues check✅ PassedThe PR directly addresses both #10 objectives (options.path handling, embedded port stripping) and #12 objective (Request body byte estimation via async clone+arrayBuffer), with corresponding code changes and tests.
Out of Scope Changes check✅ PassedAll changes align with issue objectives: interceptor fixes, related tests, roadmap updates marking Waves 1-3 progress, and implementation documentation; no extraneous modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/10-12-interceptor-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…t count
Final-review pass found three stale references inside the plan doc that
still described the abandoned content-length-header approach (Task 2 Step 7
commit-message template, Task 5 Step 2 expected-log block, Task 5 Step 4
PR-body template). Updates them to match the async clone+arrayBuffer
approach that actually shipped, and corrects the test count from 239 to 240
(one extra test landed during Task 2's review fix-up).
No code change. PR description (#35) was already correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md (1)

946-946: 💤 Low value

Clarify or remove stale line-number reference.

"caller at line 151 updated" appears to reference the original source file, but the plan doesn't mention line 151 elsewhere. If this refers to patchedFetch in src/core/interceptor.ts, consider clarifying or removing the stale reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md` at line 946,
The plan contains a stale line-number reference ("caller at line 151 updated")
that likely refers to the patchedFetch caller; update the text to either remove
the line-number mention or replace it with a clear symbol-based reference such
as "the caller in patchedFetch" and note that estimateRequestBytes' signature
was changed and that patchedFetch was updated accordingly; ensure you reference
the function names estimateRequestBytes and patchedFetch (not a line number) so
readers can find the change.
tests/interceptor.test.ts (1)

251-321: ⚡ Quick win

Add a regression for prompt resolve/reject with slow Request streams.

These cases validate byte accounting, but they do not protect against the bigger risk here: fetch(req) resolving or rejecting only after the cloned body finishes draining. A slow-stream + early-failure/early-response case would lock that down. As per coding guidelines, "tests/**/*.test.ts: Use vitest for unit testing with a target of comprehensive coverage across all core modules".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/interceptor.test.ts` around lines 251 - 321, Add a regression test in
interceptor.test.ts that verifies fetch(req) resolves/rejects without waiting
for a slow Request body clone to finish draining: create a Request with a
ReadableStream that yields data slowly (use async delays in
start/controller.enqueue), send it to a server route that responds immediately
(e.g., "/early-response" or "/early-error"), call fetch(req) and assert that the
Promise resolves/rejects promptly (don’t await the full stream drain) and that
the interceptor still records an event with a sane requestBytes (use the
existing events array and estimateRequestBytes behavior). Reference Request,
fetch, the ReadableStream start/controller.enqueue, the server route handler,
and the interceptor/event capture to locate where to add the test; implement the
delay with a small await/timeout and use vitest assertions/timers consistent
with the other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Around line 891-923: The PR body incorrectly states the rejected approach for
`#12` (saying the fix reads Request.headers.get("content-length")); update the PR
body text for issue `#12` to describe the actual implementation: state that
estimateRequestBytes became async, and when init.body is absent and input is a
Request with a body it clones the Request (input.clone()) and awaits the clone's
arrayBuffer() (cloned.arrayBuffer()) to measure bytes so the original
Request/body stream is preserved; replace the existing one-line claim about
content-length with this corrected explanation.
- Line 5: The plan text miscounts fixes: it claims "two narrow correctness bugs"
but the interceptor changes actually address three issues—estimateRequestBytes
failing to read Request bodies, the http.request overload behavior that drops
options.path when the first argument is a URL, and the host-with-embedded-port
vs opts.port collision that strips the port; update the description and PR title
to list all three fixes and adjust any changelog/commit messages accordingly,
and ensure references to src/core/interceptor.ts and the functions/variables
estimateRequestBytes, fetch handling for new Request(url, { body }), and the
http.request code path that uses options.path, opts.host, and opts.port are
mentioned so reviewers can verify each fix is covered.
In `@src/core/interceptor.ts`:
- Around line 218-230: The code currently awaits requestBytesPromise (variables
requestBytesPromise and requestBytes) on the caller-visible fetch path which can
block resolution for slow/streaming Request bodies; instead remove the await
from the success and error return paths and handle request-byte measurement
asynchronously by attaching a background continuation (e.g.
requestBytesPromise.then(bytes => { requestBytes = bytes; emitTelemetry(...);
}).catch(() => {/* swallow or log safe default */})); ensure
estimateRequestBytes is invoked as before but do not await it on either the
normal return or rethrow path so the fetch resolution is not delayed.
- Around line 73-77: The hostname extraction currently truncates bracketed IPv6
addresses by using indexOf(":"), so update the logic that computes hostname from
hostRaw (derived from opts.hostname ?? opts.host ?? "localhost") to be
bracket-aware: if hostRaw starts with "[" find the matching "]" and take the
substring through that bracket (so "[::1]:8080" -> "[::1]"); otherwise, strip a
trailing numeric port by looking for the last ":" and only slicing there if the
substring after the last ":" is all digits (so "host:8080" -> "host" but
"user@host:abc" stays unchanged). Apply this change where hostname is defined
(and used by extractUrl()/related URL construction) so IPv6 bracketed hosts and
non-port colons are handled correctly.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md`:
- Line 946: The plan contains a stale line-number reference ("caller at line 151
updated") that likely refers to the patchedFetch caller; update the text to
either remove the line-number mention or replace it with a clear symbol-based
reference such as "the caller in patchedFetch" and note that
estimateRequestBytes' signature was changed and that patchedFetch was updated
accordingly; ensure you reference the function names estimateRequestBytes and
patchedFetch (not a line number) so readers can find the change.
In `@tests/interceptor.test.ts`:
- Around line 251-321: Add a regression test in interceptor.test.ts that
verifies fetch(req) resolves/rejects without waiting for a slow Request body
clone to finish draining: create a Request with a ReadableStream that yields
data slowly (use async delays in start/controller.enqueue), send it to a server
route that responds immediately (e.g., "/early-response" or "/early-error"),
call fetch(req) and assert that the Promise resolves/rejects promptly (don’t
await the full stream drain) and that the interceptor still records an event
with a sane requestBytes (use the existing events array and estimateRequestBytes
behavior). Reference Request, fetch, the ReadableStream
start/controller.enqueue, the server route handler, and the interceptor/event
capture to locate where to add the test; implement the delay with a small
await/timeout and use vitest assertions/timers consistent with the other tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c01bf62-6531-4de7-a46c-6beec1ccf437

📥 Commits

Reviewing files that changed from the base of the PR and between 51040ec and b40b061.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-05-15-interceptor-surgical-fixes.md
  • docs/superpowers/roadmap-2026-05-13-issue-waves.md
  • src/core/interceptor.ts
  • tests/interceptor.test.ts

Comment threadsrc/core/interceptor.ts Outdated
Comment threadsrc/core/interceptor.ts Outdated
AndresL230and others added 2 commits May 15, 2026 03:36
The original 10b strip used first-colon split on the host string, which
broke IPv6:
- `[::1]:8080` → first colon at index 1 (inside `[::1]`) → "["
- `::1` → first colon at index 0 → ""
Rewrite the strip to be bracket-aware:
- Bracketed IPv6 ("[::1]:8080"): strip ":port" after the closing "]"
- Bare IPv6 ("::1"): preserve as-is (multi-colon → no strip). URL
reconstruction still fails downstream for unbracketed forms (URL
spec requires brackets), but that's a graceful null-return rather
than silent data corruption.
- Regular host/IPv4 ("host" or "host:port"): strip when exactly one
colon, leave alone otherwise.
Test: bracketed IPv6 with collision (`[::1]:PORT` + `port: PORT`) now
captures a correctly-attributed event via the error-path callback.
WHATWG `URL.hostname` retains brackets for IPv6 literals, so the
captured `event.host` is `[::1]` (matches Node's URL semantics).
CodeRabbit comment 3246546663 on PR #35 — flagged Major. Pre-Wave-3,
this was a silent null-return; post-Wave-3 the strip introduced silent
wrong data. This commit restores graceful behavior for IPv6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt doesn't block fetch caller (#12)
Move the `requestBytesPromise` await off the caller-visible fetch path.
Previously, `patchedFetch` awaited the cloned-body materialization
before returning the response (success path) or rethrowing
(error path), which could delay large/streaming uploads' fetch
resolution by the body-materialization time.
Now:
- Bodyless response path: schedule a deferred IIFE that awaits the
request-byte measurement, then fires telemetry. Return the response
immediately.
- Streaming response path: `fireTelemetry` is async and awaits the
request-byte measurement inside the existing body-counter IIFE,
which only delays the eventual `_callback`, not the caller's
fetch resolution or response stream consumption.
- Error path: schedule a deferred IIFE for the error event after
awaiting the request-byte measurement; rethrow immediately.
`latencyMs` is computed synchronously at response-arrival (or error)
time, so latency reflects server response time rather than request-body
materialization time. Event contents and ordering are unchanged.
Tests: added `flushDeferred()` helper (one `setImmediate` cycle) before
event-array assertions in the affected interceptor tests so the
deferred IIFE has fired before assertions. No assertion weakened.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230 merged commit c2ad485 into mainMay 15, 2026
1 check passed
@AndresL230
AndresL230 deleted the feat/10-12-interceptor-fixes branch May 21, 2026 04:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fetch(new Request(url, { body })) reports requestBytes: 0 http.request overload edges: options.path dropped, opts.host with port double-appended

1 participant

@AndresL230