Skip to content

fetch: stop replacing unrecognized HTTP methods with GET - #33469

Closed
robobun wants to merge 7 commits into
mainfrom
farm/abc564cf/fetch-custom-methods
Closed

robobun wants to merge 7 commits into
mainfrom
farm/abc564cf/fetch-custom-methods

Conversation

@robobun

@robobun robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

Any method string that is not an exact-case match of Bun's internal 36-verb
table is silently sent as GET. Against a raw socket:

import net from "node:net";
const server = net.createServer(s =>
  s.once("data", b => {
    console.log(b.toString().split("\r\n")[0]);
    s.end("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
  }),
);
await new Promise(r => server.listen(0, "127.0.0.1", r));
const url = `http://127.0.0.1:${server.address().port}/`;

await fetch(url, { method: "Delete" });   // wire: GET / HTTP/1.1
await fetch(url, { method: "Propfind" }); // wire: GET / HTTP/1.1
await fetch(url, { method: "BREW" });     // wire: GET / HTTP/1.1
await fetch(url, { method: "put" });      // wire: PUT / HTTP/1.1

The request "succeeds" with a 200, so a DELETE that never deleted looks
like it worked. Methods usually come from a variable (an HTTP client wrapper,
a proxy forwarding req.method, a REST SDK with a per-call method option),
so any casing the table does not hold turns a mutation into a read. Node and
browsers send the token as given.

Cause

bun_http_types::Method is a closed enum looked up through a
comptime_string_map! keyed on exact bytes (only the all-upper and all-lower
spellings are present). fetch() and new Request() both did

method_jsc::from_js(global_this, method_)?   // -> Option<Method>
    .unwrap_or(Method::GET)

so a miss became GET rather than an error, and the enum had nowhere to put
a verb the table does not know.

Fix

Follow the fetch spec:

  • case-normalize only DELETE, GET, HEAD, OPTIONS, POST, PUT
  • forward any other valid RFC 9110 token byte-for-byte
  • TypeError on a token that is not a token, and on the forbidden CONNECT,
    TRACE, TRACK

Method keeps its role as the closed enum used for routing, S3 signing and
the EnumSet route tables. Two new types carry a user-supplied verb:

  • MethodRef<'a> = Known(Method) | Custom(&'a [u8]), Copy, stored in
    HTTPClient/AsyncHTTP so the HTTP thread's bitwise copy of AsyncHTTP
    and the picohttp::Request<'static> borrow keep working. The custom token
    borrows FetchTasklet-owned storage, the same contract headers_buf,
    request_body and hostname already use.
  • MethodBuf = owning counterpart, stored in Request, Response's init
    and FetchOptions.

A custom verb is treated conservatively: it may carry a request body, it is
never assumed idempotent (so a keep-alive reset does not replay it), and it
is rewritten to GET only on a 303 redirect, never on 301/302.

Errors come back the way the surrounding code already reports them:
new Request() throws, fetch() and server.fetch() reject.

init["method"] is also read with get/fast_get instead of
get_truthy/fast_get_truthy, because the spec keys on WebIDL presence:
only undefined falls through to the default. That lines the three entry
points (fetch, new Request, server.fetch) up with each other and with
Node:

init.method before after (and Node)
absent / undefined GET GET
"" GET TypeError
null GET "null"
0 / false "0" / "false" "0" / "false"

Response::Init is the parser for both shapes, so it is split: the three
Response entry points (new Response, Response.json, Response.redirect)
stay permissive, because WebIDL's ResponseInit has no method member and
Response never exposes one — Bun reads it only so new Request(url, response)
can inherit a method. Only new Request(url, init) applies the rules above.

Behavior changes worth calling out

  • fetch(url, { method: "patch" }) now sends patch, not PATCH. Only the
    six spec-listed methods are normalized; this matches undici and browsers.
    node:http keeps upper-casing, which also matches Node.
  • fetch(url, { method: "CONNECT" }) (and TRACE/TRACK) now rejects
    instead of going out on the wire.
  • fetch(url, { method: "" }) now rejects, and { method: null } sends
    null, per the table above.
  • fetch("s3://...", { method: <custom token> }) rejects, because the S3
    request signer takes a known Method.

Not in this PR: the Bun.serve receive side

Bun.serve drops the connection for any method outside the same 36-verb
table. Raw request lines against Bun.serve({ fetch: req => new Response(req.method) }),
identical on main and on this branch:

GET       -> HTTP/1.1 200 OK, body "GET"
PROPFIND  -> HTTP/1.1 200 OK, body "PROPFIND"
CUSTOM    -> (connection closed, no response)
propfind  -> (connection closed, no response)

fetch() now stops masking that: fetch(bunServer, { method: "CUSTOM" }) used
to arrive as a silent GET with a 200, and now surfaces as a connection error.
Fixing the server needs a case-sensitive accessor through uWS (whose
getMethod() lower-cases the request line in place), relaxing
isValidMethod's strict mode, and the 9-verb routing dispatch in
uws_sys::App/uws_sys::h3 — a separate change, left alone deliberately.

That means #6021 and #21566 are only half addressed here (the fetch half);
I have not marked them as fixed.

Verification

test/js/web/fetch/fetch-method.test.ts asserts the bytes on the wire via a
raw socket, not what Bun echoes back in request.method.

$ bun bd test test/js/web/fetch/fetch-method.test.ts
 68 pass
 0 fail

The same file against a debug build of main: 20 pass, 48 fail.

It also pins the lifetime of a custom method's borrowed token across redirect
hops (302/307 keep it, 303 rewrites it to GET), which the debug build's ASAN
exercises.

Unit coverage for the spec predicates lives in src/http_types/Method.rs
(cargo test -p bun_http_types).

Fixes #12014

Suites re-run against this build

test/js/web/fetch/, test/js/bun/http/serve.test.ts,
test/js/node/http/node-http.test.ts, test/js/bun/s3/s3.test.ts. The
remaining failures (RSS-threshold leak tests, IPv6 requestIP, localhost
resolution, 5s timeouts under ASAN) reproduce identically on a debug build of
main in the same container.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 572f94fa-ac87-4d02-a8be-1b2850a5435e

📥 Commits

Reviewing files that changed from the base of the PR and between b812ef7 and b70b59a.

📒 Files selected for processing (1)
  • test/js/web/fetch/fetch-method.test.ts

Walkthrough

This PR replaces the fixed Method enum used for HTTP request methods with new MethodRef/MethodBuf types that support both known and arbitrary custom method tokens. It adds token validation/normalization, a JSC bridge for JS string conversion, and threads these types through AsyncHTTP, HTTPClient, Request, Response, fetch, FetchTasklet, and server request handling, updating numerous call sites accordingly.

Changes

Custom HTTP method support

Layer / File(s) Summary
Method type foundation
src/http_types/Method.rs
Adds is_token, is_forbidden, normalize, and new MethodRef/MethodBuf enums with accessors for known/custom methods, plus tests.
JS-to-Method bridge
src/http_jsc/method_jsc.rs
Adds request_method_from_js to parse/validate JS method strings into MethodBuf or TypeError, and MethodRefJsc::to_js to serialize methods back to JS.
AsyncHTTP/HTTPClient plumbing
src/http/AsyncHTTP.rs, src/http/lib.rs
Switches AsyncHTTP/HTTPClient method storage and constructor parameters to MethodRef, detaches method byte lifetime in build_request, and updates redirect logic to use MethodRef predicates.
Request/Response method storage
src/runtime/webcore/Request.rs, src/runtime/webcore/Response.rs
Changes Request/Response/Init method fields to MethodBuf, updates constructors, get_method, and refactors Response init parsing to validate methods differently for RequestInit vs ResponseInit.
fetch()/FetchTasklet method handling
src/runtime/webcore/fetch.rs, src/runtime/webcore/fetch/FetchTasklet.rs
Parses fetch() method options via the new JSC bridge into MethodBuf, updates S3 upload method checks/signing, and updates FetchTasklet/FetchOptions to carry MethodBuf while deriving MethodRef for AsyncHTTP::init.
Server request method parsing
src/runtime/server/server_body.rs, src/runtime/server/mod.rs, src/runtime/bake/DevServer.rs
Updates on_fetch to parse and validate request methods via the JSC bridge, and adjusts Request::init call sites and DevServer saved-request method resolution.
Call-site conversions
src/install/NetworkTask.rs, src/install/npm.rs, src/runtime/cli/*, src/runtime/webcore/s3/*, src/standalone_graph/StandaloneModuleGraph.rs
Updates numerous AsyncHTTP::init/init_sync call sites to convert Method::GET/POST/PUT via .into() for the new parameter type.
Method behavior tests
test/js/web/fetch/fetch-method.test.ts, test/js/web/request/request-clone-leak.test.ts
Adds tests covering method normalization, custom method forwarding, redirect rewriting, invalid/forbidden method rejection, and adjusts leak-test RSS thresholds/wiring.

Sequence Diagram(s)

sequenceDiagram
  participant JS as JS caller
  participant RequestInit as Init::parse
  participant JSCBridge as request_method_from_js
  participant Request
  participant AsyncHTTP

  JS->>RequestInit: new Request(url, {method})
  RequestInit->>JSCBridge: request_method_from_js(method)
  alt valid known/custom token
    JSCBridge-->>RequestInit: Ok(MethodBuf)
  else invalid or forbidden
    JSCBridge-->>RequestInit: TypeError
  end
  RequestInit->>Request: store MethodBuf
  Request->>AsyncHTTP: init(MethodRef derived from MethodBuf)
  AsyncHTTP->>AsyncHTTP: build_request using method bytes
Loading

Related Issues: #12014 (Request coerces non-standard method to GET instead of preserving/validating it)

Suggested labels: rust, http, webcore, needs-review

Suggested reviewers: Jarred-Sumner, cirospaciari

🐰 A method once fixed, now free to roam,
Custom tokens carried safely home,
Through fetch and server, byte by byte,
Validated tokens shining bright.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: preserving unrecognized HTTP methods instead of coercing them to GET.
Description check ✅ Passed The description covers the change and includes a verification section with concrete test results and commands.
Linked Issues check ✅ Passed The changes satisfy #12014 by keeping nonstandard Request methods instead of silently converting them to GET.
Out of Scope Changes check ✅ Passed The extra code and test updates appear necessary to propagate the method-type change and validate behavior, not unrelated scope.

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

@github-actions github-actions Bot added the claude label Jul 6, 2026
@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:06 PM PT - Jul 6th, 2026

❌ @robobun, your commit b70b59a has 2 failures in Build #69121 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33469

That installs a local version of the PR into your bun-33469 executable, so you can run:

bun-33469 --bun

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Support arbitrary method in fetch #6021 - fetch() silently replaces custom methods (e.g. CUSTOM) with GET; this PR now forwards them as-is
  2. Non-standard and broken handling of HTTP methods in both fetch and Bun.serve #21566 - Comprehensive issue documenting non-standard HTTP method handling in fetch and Bun.serve; this PR fixes the fetch() side (custom methods forwarded, forbidden methods rejected)
  3. Request class coherese method if the method is not standard #12014 - new Request(url, { method: 'BAD' }) silently coerces to GET; this PR now properly validates and either normalizes or forwards the method

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #6021
Fixes #21566
Fixes #12014

🤖 Generated with Claude Code

Comment thread src/runtime/webcore/fetch.rs
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

I checked each of the three issues the bot suggested against this branch. Only one of them is actually closed by it.

#12014 — fixed. new Request(url, { method: "BAD" }).method was "GET", now "BAD".

#6021 — half. Its repro round-trips through Bun.serve. fetch() now puts CUSTOM on the wire, but the server still drops the connection, so the test in that issue does not pass yet.

#21566 — half. It tracks three bullets; this PR is the first one (fetch silently replacing the method). The two Bun.serve bullets are untouched.

Raw request lines against Bun.serve({ fetch: req => new Response(req.method) }), identical on main and on this branch:

GET       -> HTTP/1.1 200 OK, body "GET"
PROPFIND  -> HTTP/1.1 200 OK, body "PROPFIND"
CUSTOM    -> (connection closed, no response)
propfind  -> (connection closed, no response)

So fetch() used to hide the server bug: fetch(bunServer, { method: "CUSTOM" }) arrived as a silent GET and got a 200. Now it surfaces as a connection error, which is the honest answer but still not a working request.

The server side is a separate change: uWS getMethod() lower-cases the request line in place (so even propfind fails above), isValidMethod’s strict mode rejects anything outside the same 36-verb table, and route dispatch in uws_sys::App/uws_sys::h3 only knows 9 verbs. I would rather do that on its own than bolt it onto this diff — happy to follow up with it.

Only Fixes #12014 is in the description.

Also addressed the init["method"] presence review in d70b19b: fetch, new Request and server.fetch all read it with get/fast_get now, so { method: "" } throws instead of defaulting to GET.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing the empty-string presence check — that's now covered end-to-end. I didn't find further issues, but given the new MethodRef/MethodBuf types threaded through HTTPClient/AsyncHTTP, the cross-thread borrowed-token lifetime, and the user-visible behavior changes (patch no longer upper-cased, forbidden methods now reject), this warrants a human look.

Extended reasoning...

Overview

This PR fixes fetch() and new Request() silently replacing unrecognized HTTP methods with GET. It introduces MethodRef<'a> (borrowing, Copy) and MethodBuf (owning) alongside the existing closed Method enum, and threads them through 23 files: the HTTP client core (HTTPClient, AsyncHTTP, FetchTasklet), the webcore Request/Response/fetch entry points, and a dozen mechanical .into() updates at CLI/install/S3 call sites. New spec predicates (is_token, is_forbidden, normalize) live in Method.rs with unit tests, and fetch-method.test.ts asserts wire bytes via a raw socket.

Since my last review, commit d70b19b1 addressed the get_truthy → get/fast_get presence-check gap in both fetch.rs and Response.rs, added "" to the invalid-token test tables, and added a test that only undefined falls through to the default.

Security risks

The RFC 9110 token validator (is_token) rejects control characters, whitespace, and non-ASCII before any custom method reaches the request line, which prevents request-line injection via method. The forbidden-method check is case-insensitive per spec. I don't see auth/crypto/permissions surface touched. The main risk area is memory safety, not security per se.

Level of scrutiny

High. This touches the per-request hot path of the HTTP client and introduces a lifetime-erased borrow (MethodRef::Custom(&'a [u8]) stored as MethodRef<'static> in AsyncHTTP) whose backing storage lives in FetchTasklet.method: MethodBuf. Correctness relies on field declaration order (method after http so drop order frees the borrower first) and on the same Interned::assume contract that headers_buf/hostname already use — plausible, and the PR documents it, but this is exactly the kind of cross-thread borrowed-slice invariant that Bun's review guidelines flag for careful human review. The bitwise-copy path in on_async_http_callback also copies original_method back into the client; a reviewer familiar with the HTTP-thread copy lifecycle should confirm the custom-token slice remains valid through redirects and retries.

Other factors

  • User-visible behavior changes a maintainer should sign off on: fetch(url, {method: "patch"}) now sends lowercase patch (spec-correct but a change from prior Bun behavior), and CONNECT/TRACE/TRACK now reject where they previously went out on the wire.
  • DevServer.rs now does .known().unwrap_or(Method::POST) / .unwrap_or(Method::GET) on saved requests — a reasonable fallback but worth a glance from someone who knows that code path.
  • Test coverage is thorough (wire-level assertions, clone/copy paths, forbidden methods, token edge cases, falsy-value handling), and the PR description is exemplary.
  • No outstanding reviewer comments; my prior feedback is fully resolved.

@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Took the lifetime concern seriously rather than just asserting it, since that was the right thing to poke at.

fefecba adds three wire-level redirect cases to fetch-method.test.ts, because a custom method is exactly the case where the HTTP thread re-reads the borrowed token for the next hop:

302 + BREW              -> ["BREW", "BREW"]
307 + Propfind + body   -> ["Propfind", "Propfind"]
303 + BREW              -> ["BREW", "GET"]
302 + POST + body       -> ["POST", "GET"]

They pass under bun bd (ASAN on), and all four fail on an unpatched build. The last two are the spec's redirect rule (303 rewrites anything that is not GET/HEAD, 301/302 rewrite only POST), which is why MethodRef::is() compares against a known verb rather than bytes — a custom token never matches POST, so 301/302 leave it alone.

On retries: MethodRef::is_idempotent() returns false for Custom, so a custom verb is never replayed on a keep-alive reset. The token itself lives in FetchTasklet.method, declared after http, so drop order frees the AsyncHTTP that borrows it first.

Also fixed the undocumented_unsafe_blocks clippy failure in the same commit. bun run rust:clippy is clean locally now.

Agreed the patch casing and the CONNECT/TRACE/TRACK rejection want a maintainer's sign-off — both are spelled out in the description.

Comment thread src/runtime/webcore/Response.rs
Comment thread test/js/web/fetch/fetch-method.test.ts Outdated
robobun added 4 commits July 6, 2026 14:35
fetch() and new Request() looked the method up in a case-sensitive table of
36 verbs and fell back to GET when the lookup missed, so "Delete", "Put",
"Propfind" and any custom token were silently sent as GET.

Follow the fetch spec instead: case-normalize only DELETE, GET, HEAD,
OPTIONS, POST and PUT, forward any other valid RFC 9110 token byte-for-byte,
and raise a TypeError for invalid tokens and the forbidden CONNECT, TRACE
and TRACK.

Method becomes MethodRef<'a> (a known verb or a borrowed custom token) in the
HTTP client, which keeps it Copy, and MethodBuf (owning) in Request, Response
init and FetchOptions. Custom verbs are treated as body-carrying and
non-idempotent: they may have a request body, are never retried on a
keep-alive reset, and are rewritten to GET only on a 303 redirect.
get_truthy/fast_get_truthy drop "" and null before the method reaches the
validator, so fetch(url, {method: ""}) still defaulted to GET while
server.fetch() rejected it. The fetch spec checks WebIDL presence: only
undefined falls through to the default.
The custom method token is borrowed from the FetchTasklet and re-read on the
HTTP thread for the next hop, so assert it survives 302/307 and that 303 still
rewrites it to GET. Also move the SAFETY comment onto the line clippy's
undocumented_unsafe_blocks wants it on.
Response::Init is the shared RequestInit/ResponseInit parser. WebIDL's
ResponseInit has no method member and Response never exposes one; Bun reads it
only so new Request(url, response) can inherit a method. Validating it there
made new Response(body, {method: "CONNECT"}) and Response.json/redirect throw
for a field that does not exist.

Split the parser: the Request path rejects invalid and forbidden tokens, the
Response paths ignore a method they cannot represent.

Also answer each raw-socket connection exactly once in the test helpers, so a
request body arriving in its own data event cannot consume the next hop.
@robobun
robobun force-pushed the farm/abc564cf/fetch-custom-methods branch from c74bf22 to 7349a54 Compare July 6, 2026 14:53
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Worked through the red lanes on build #69019. Rebased onto 48ff9eb and pushed 7349a54.

test/js/bun/cookie/cookie-map.test.ts (4 lanes) — my base predated #33425, which updated those Expires assertions to IMF-fixdate. The rebase clears it; the file now matches main and the suite is 33/33 locally.

test/js/web/websocket/websocket-syscall-fault.test.ts (x64-asan) — LeakSanitizer reported a 128-byte Request leaked from on_web_socket_upgrade. That is pre-existing, not this PR. Running the same fixture against a build of main, with detect_leaks=1:

Direct leak of 112 byte(s) in 1 object(s) allocated from:
  #9  ...::Request>::new                 src/runtime/webcore/Request.rs:145:9
  #10 ...::on_web_socket_upgrade         src/runtime/server/server_body.rs:3312:34

Same allocation site, same stack. It reports 128 bytes on this branch only because Request grew by 16. The fixture calls process.exit(0) from onclose, so the upgrade Request is still live when LSAN runs; worth a separate look, but it is not something this PR introduces.

test/js/web/request/request-clone-leak.test.ts (x64-asan) — new Request(test #1) hit 64 MB against a toBeLessThan(64) bound. That bound has no headroom: the same case measures 65 MB under ASAN with mains src/, and run-to-run variance is a few MB. Request growing 16 bytes here (a MethodBuf rather than a one-byte Method) does not help. Raised the ASAN bound to 80 with the measurement in a comment — the leak it guards against presents as 100+ MB, so the guard is intact. The request.clone bound is untouched; it measures 5-10 MB.

test/cli/install/*, postgres-simple-query-pipeline, grpc-js/test-pick-first, verify-baseline agent creation — none touch the method path. My diff in src/install/ is Method::GET → Method::GET.into(), the same wire bytes.

new Request(test #1) measures 65 MB with no leak present, against a bound of
64, so the assertion rides the boundary. Request also grew 16 bytes here (a
MethodBuf in place of a one-byte Method). The leak this guards against
presents as 100+ MB, so 80 still catches it.

These cases have also never run under a debug build: ~100k Requests per case
takes a debug JSC ~30s, past the default timeout. Shrinking the workload to
fit would leave the RSS bounds measuring nothing, so skip there and say so.
@robobun
robobun force-pushed the farm/abc564cf/fetch-custom-methods branch from 7349a54 to d036bdc Compare July 6, 2026 15:32
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Two changes to test/js/web/request/request-clone-leak.test.ts in d036bdc, both about this PR making Request 16 bytes bigger (a MethodBuf instead of a one-byte Method):

The ASAN bound had no headroom. new Request(test #1) measures 65 MB with no leak present, against toBeLessThan(64). Raised to 80; the leak it guards against presents as 100+ MB, so the guard is intact. The request.clone bound is untouched (it measures 5-10 MB).

These cases have never run under a debug build. Each builds ~100k Requests, which a debug JSC takes ~30s to do — well past the default test timeout, so they time out on main too:

$ bun bd test test/js/web/request/request-clone-leak.test.ts   # on main
(fail) new Request(test #1) [22525.06ms]
  ^ this test timed out after 5000ms.

Shrinking the workload enough to fit would leave the RSS bounds measuring nothing, so they are now explicitly skipped in debug rather than silently timing out. Release and release-ASAN, where this guard actually earns its keep, are unaffected.

Happy to pull either out if you would rather they land separately.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@test/js/web/fetch/fetch-method.test.ts`:
- Around line 75-153: The tests in fetch-method.test.ts are all network-bound
and independent, so this suite should run concurrently instead of sequentially.
Update the surrounding describe block and the related sibling network test
blocks to use describe.concurrent or test.concurrent, and keep the existing
helper functions like wireMethod, requestLine, and redirectedMethods unchanged
so each case still owns its own ephemeral server and socket I/O without sharing
state.
- Around line 7-73: The TCP server setup and teardown logic is duplicated in
requestLine and redirectedMethods, including the buffered data handling and
finally server.close() cleanup. Extract the shared
net.createServer/listen/error-handling/request-capture flow into a reusable
helper, then let requestLine and redirectedMethods build on it with their
specific response behavior and parsing. Use the existing requestLine and
redirectedMethods helpers as the main entry points to refactor around.

In `@test/js/web/request/request-clone-leak.test.ts`:
- Around line 89-93: The RSS assertion in request-clone-leak.test.ts is too
tight for the measured no-leak baseline, especially under ASAN, and needs more
headroom. Update the threshold used in the delta expectation in the request
clone leak test to give roughly 2x margin over the observed baseline (for
example, re-measure and raise the ASAN bound closer to ~130 instead of 80),
keeping the existing isASAN conditional in place. Ensure the change is made in
the test case around the delta check so the threshold remains stable across CI
variance.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 61a96771-d76a-4b2b-8568-524c738bceb1

📥 Commits

Reviewing files that changed from the base of the PR and between 48ff9eb and b812ef7.

📒 Files selected for processing (24)
  • src/http/AsyncHTTP.rs
  • src/http/lib.rs
  • src/http_jsc/method_jsc.rs
  • src/http_types/Method.rs
  • src/install/NetworkTask.rs
  • src/install/npm.rs
  • src/runtime/bake/DevServer.rs
  • src/runtime/cli/audit_command.rs
  • src/runtime/cli/create_command.rs
  • src/runtime/cli/pm_view_command.rs
  • src/runtime/cli/publish_command.rs
  • src/runtime/cli/run_command.rs
  • src/runtime/cli/upgrade_command.rs
  • src/runtime/server/mod.rs
  • src/runtime/server/server_body.rs
  • src/runtime/webcore/Request.rs
  • src/runtime/webcore/Response.rs
  • src/runtime/webcore/fetch.rs
  • src/runtime/webcore/fetch/FetchTasklet.rs
  • src/runtime/webcore/s3/client.rs
  • src/runtime/webcore/s3/simple_request.rs
  • src/standalone_graph/StandaloneModuleGraph.rs
  • test/js/web/fetch/fetch-method.test.ts
  • test/js/web/request/request-clone-leak.test.ts

Comment thread test/js/web/fetch/fetch-method.test.ts Outdated
Comment thread test/js/web/fetch/fetch-method.test.ts Outdated
Comment thread test/js/web/request/request-clone-leak.test.ts
requestLine and redirectedMethods repeated the same server bootstrap, buffered
read and teardown; both now build on one requestLines helper that answers the
nth request however the caller asks. Each case still owns its own ephemeral
port, so the network-bound block runs concurrently.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No bugs found on this pass and the earlier findings are all addressed, but this touches enough surface — new MethodRef/MethodBuf types threaded through HTTPClient/AsyncHTTP/FetchTasklet with lifetime-erased borrows, plus the user-facing behavior changes called out in the description (patch no longer normalized, CONNECT/TRACE/TRACK now reject, method: null → "null") — that a human should sign off on the design and the compat trade-offs.

Extended reasoning...

Overview

24 files. The core change introduces MethodRef<'a> (borrowing, Copy) and MethodBuf (owning) alongside the existing closed Method enum, then threads them through the HTTP stack: HTTPClient.method and AsyncHTTP.method become MethodRef<'a>; Request.method, Response::Init.method, and FetchOptions.method become MethodBuf; FetchTasklet gains a method: MethodBuf field that owns the custom token the AsyncHTTP borrows via Interned::assume + detach_lifetime. request_method_from_js implements the fetch-spec normalize/token/forbidden rules, and Response::Init::init is split (const-generic) so only the RequestInit path validates. About half the touched files are mechanical Method::X → Method::X.into() updates at AsyncHTTP::init call sites.

Security risks

Low. Token validation (is_token) follows RFC 9110 §5.6.2 and rejects non-ASCII / whitespace / separators, which prevents request-line injection via the method. Forbidden-method rejection (CONNECT/TRACE/TRACK) tightens rather than loosens. No auth, crypto, or permission surfaces touched.

Level of scrutiny

High. This is production-critical HTTP client code with:

  • Several explicitly-called-out user-facing behavior changes (non-normalized patch, rejected forbidden methods, {method: ""} now throws, {method: null} → "null", S3 rejects custom methods) that a maintainer should agree are the right compat trade-offs.
  • Lifetime-sensitive unsafe: the MethodRef::Custom token is stored in FetchTasklet and lent to the HTTP-thread AsyncHTTP copy via bun_ptr::Interned::assume + detach_lifetime, relying on field-declaration order for drop sequencing and on the same self-borrow contract as hostname/headers_buf. The redirect test exercises this across hops under ASAN, which is reassuring, but the pattern still deserves a human eye.
  • A new type pair in bun_http_types that becomes public API for the crate.

Other factors

All three of my earlier inline findings (truthy vs presence read, ResponseInit collateral validation, per-socket test guard) were addressed and resolved, as were CodeRabbit's dedupe/concurrency nits. One CodeRabbit comment remains open (RSS-threshold headroom in request-clone-leak.test.ts — 80 vs the ~2× guideline); minor, but worth a decision. Test coverage is thorough (68 wire-level assertions + Rust unit tests for the spec predicates), and the DevServer .known().unwrap_or(...) fallbacks are reachable only from Bun.serve-received requests, which never carry a Custom method. The bug-hunting system found nothing on this revision.

@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Both review bots have gone quiet with nothing outstanding, so here is the short version of what actually wants a human call. Everything below is already in the description; collecting it so it is one read rather than six.

The compat trade-offs. Each is a deliberate deviation from today's Bun, and each matches Node/undici and browsers:

input today this PR Node
method: "Delete" wire GET wire DELETE DELETE
method: "BREW" wire GET wire BREW BREW
method: "patch" wire PATCH wire patch patch
method: "CONNECT" wire CONNECT TypeError TypeError
method: "" GET TypeError TypeError
method: null GET "null" "null"

The first two are the bug. The last four are the spec rules that come with fixing it properly, and they are the ones worth disagreeing with if you are going to.

The lifetime. MethodRef::Custom is a &'a [u8] borrowed from FetchTasklet.method, lent to the HTTP thread's AsyncHTTP through the same Interned::assume contract headers_buf / hostname already use. method is declared after http so drop order frees the borrower first. MethodRef stays Copy, so the bitwise ptr::read of AsyncHTTP onto the HTTP thread and the client.method = original_method restore in teardown are untouched. Three redirect tests read the token back on the second hop under ASAN.

One thing I checked after review. DevServer's .known().unwrap_or(Method::POST) looked like a place a custom verb could silently degrade — the exact bug class this fixes. It cannot: the only producer of SavedRequest.request is prepare_js_request_context, which builds the Request from RequestContext.method: Method (the closed enum, parsed off the wire), so a Custom never reaches it. The fallback also mirrors its sibling arm, Method::which(req.method()).unwrap_or(Method::POST), exactly.

Scope I left out, on purpose. Bun.serve's receive side still drops the connection for any verb outside the same 36-entry table — CUSTOM, and even lowercase propfind. Unchanged by this PR, and fixing it needs a case-sensitive accessor through uWS (whose getMethod() lower-cases the request line in place) plus the 9-verb route dispatch. Happy to follow up with it separately. That is why only #12014 is marked fixed, and not #6021 / #21566.

@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for the current head (b70b59a, build #69121). The diff's own tests pass; the red lanes are pre-existing or unrelated flake.

  • fetch-method.test.ts, request-clone-leak.test.ts — pass. The two files this PR adds/touches are green now (the rebase, the debug-skip, and the threshold bump did their job).
  • websocket-syscall-fault.test.ts (x64-asan) — pre-existing, not this PR. LeakSanitizer flags the upgrade Request (the fixture calls process.exit(0) while it is still live). Same allocation site (Request::new → on_web_socket_upgrade) reproduces on a build of main with detect_leaks=1; it reports 112 bytes there vs 128 here only because Request grew 16 bytes. Deterministic, so this lane stays red until that separate leak is fixed.
  • node-http-connect.test.ts (Windows x64) — the partial writes and buffering case, a tunnel-data buffering assertion. node:http resolves its method through _http_client.ts (checkIsHttpToken + toUpperCase), not the fetch/Request path this PR changes, so the method work doesn't reach it.
  • install/*, napi (Windows), terminal, webview, net-mongodb-pattern-leak, hot, 20144 — unrelated lanes, no HTTP-method surface.

Not re-triggering: the x64-asan leak is deterministic and pre-existing, so a re-roll can't turn the build green. Flagging for a maintainer rather than pushing empty commits.

ahliweb added a commit to ahliweb/awcms-micro that referenced this pull request Jul 25, 2026
…e ujung (#364)

Sambungan terakhir yang belum teruji: rute publikasi sungguhan →
PostgreSQL sungguhan → Varnish sungguhan.

Suite transport membuktikan purgeEdgeCache() mengosongkan cache nyata;
unit test membuktikan rute memanggil pembungkusnya. Tidak satu pun
membuktikan keduanya TERSAMBUNG — resolusi hostname dari
awcms_micro_tenant_domains duduk di antaranya, dan tenant yang
hostname-nya tidak resolve tidak meng-invalidasi apa pun sementara
seluruh test komponen tetap hijau. Di staging celah itu hanya pernah
terlihat dengan menerbitkan artikel sungguhan.

Yang ikut terkunci: setiap hostname aktif tenant di-purge (bukan hanya
primary), hostname non-aktif tidak, publikasi tenant lain tidak
mengosongkan cache tenant ini, dan publikasi yang GAGAL tidak
mengosongkan apa pun — yang terakhir menutup cara murah bagi pemanggil
tak berwenang untuk membuang cache sebuah situs.

Kedua CLI operator dijalankan sebagai proses sungguhan sehingga exit
code yang dibaca pipeline deploy ikut jadi assertion. Fixture Varnish
diekstrak ke tests/integration/varnish-fixture.ts.

Diuji balik dengan dua mutasi: melepas pemanggilan invalidasi dari rute
publish menggagalkan 3 dari 6; mengubah filter status resolver hostname
menggagalkan 5 dari 6.

Ditambah gate http:methods:check. Aturan Bun ternyata lebih luas dari
"metode kustom" — yang menentukan kecocokan huruf per huruf dengan tabel
verb internalnya, sehingga Post/Delete/Patch juga terkirim sebagai GET.
Sudah dilaporkan ke hulu (oven-sh/bun#33469), jadi tidak dibuat laporan
baru; gate ini menutup sisi kita.

bun run check hijau dengan database nyata: 4829 pass, 0 fail.

Co-authored-by: AWCMS-Micro Security <security@awcms-micro>
@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Issue #42497 reports the same defect: new Request(url, { method: "custom" }).method returns "GET", and fetch(url, { method: "PatCh" }) sends GET on the wire. Confirmed on bun 1.4.3. This PR covers both cases.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-06, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request class coherese method if the method is not standard

1 participant