Skip to content

refactor(fetch): align request and response semantics with web#11

Merged
medz merged 4 commits intomainfrom
refactor/request-response-init
Mar 11, 2026
Merged

refactor(fetch): align request and response semantics with web#11
medz merged 4 commits intomainfrom
refactor/request-response-init

Conversation

@medz
Copy link
Copy Markdown
Owner

@medz medz commented Mar 11, 2026

Summary by CodeRabbit

  • Breaking Changes

    • Request/Response constructors and factories now use positional init objects (RequestInit/ResponseInit) and positional body arguments instead of previous named-parameter forms.
    • Several previously final types are now extendable.
  • New Features

    • Public types RequestInit and ResponseInit added to configure requests/responses.
  • Documentation

    • README and changelog updated with examples and message-type table.
  • Tests

    • Tests updated to the new positional/init-based API.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Introduces RequestInit and ResponseInit types and refactors Request/Response constructors to accept positional body/init arguments; removes final from several classes to allow subclassing; updates docs, examples, and tests to the new init-based API.

Changes

Cohort / File(s) Summary
Docs & Examples
CHANGELOG.md, README.md, example/main.dart
Add breaking-change notes and update examples to use RequestInit/ResponseInit and positional body/init usage in Request/Response examples.
Request core
lib/src/fetch/request.dart
Add public RequestInit; change Request constructor to Request(Uri, [RequestInit?]); update factory constructors to accept optional RequestInit; add _coerceInit and new internal creation paths; remove legacy sentinel/copyWith patterns.
Response core
lib/src/fetch/response.dart
Add public ResponseInit; change Response constructor to Response([Object? body, ResponseInit? init]); add Response._create and _internal paths; refactor factories (text, json, bytes, redirect, empty) to use ResponseInit; add status-body validation helper.
Class modifier removals
lib/src/core/http_status.dart, lib/src/core/mime_type.dart, lib/src/fetch/body.dart, lib/src/fetch/form_data.dart, lib/src/fetch/headers.dart
Remove final from several classes (HttpStatus, MimeTypeFormatException, MimeType, BodyData, MultipartBody, _HeaderEntry) to allow subclassing; no behavioral changes.
Tests
test/public_api_surface_test.dart, test/request_response_test.dart
Update tests to the positional init-based API: construct Request/Response with RequestInit/ResponseInit, adjust assertions for new defaults (e.g., statusText), and modify redirect/status tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hopped through constructors, neat and trim,
Packed method and headers into a tiny init‑prim.
Finals loosened, subclasses dance and play,
Docs and tests now hop the new fetch way. 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(fetch): align request and response semantics with web' accurately describes the main change: restructuring Request and Response APIs to align with web standards by introducing RequestInit and ResponseInit types.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/request-response-init

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/src/fetch/request.dart (1)

39-54: Consider reducing redundant header cloning in Request.json.

Headers are cloned multiple times in this flow:

  1. _coerceInit clones init?.headers (line 103)
  2. nextInit.headers?.clone() (line 41)
  3. RequestInit constructor clones again (line 48-51 → constructor at line 11)
  4. Request constructor clones again (line 22)

While this ensures complete isolation, it creates unnecessary allocations. Consider passing headers directly where cloning has already occurred, or using an internal constructor that skips redundant clones.

♻️ Potential optimization
  factory Request.json(Uri url, Object? body, [RequestInit? init]) {
    final nextInit = _coerceInit(init, body: json.encode(body));
-   final nextHeaders = nextInit.headers?.clone() ?? Headers();
+   final nextHeaders = nextInit.headers ?? Headers();
    if (!nextHeaders.has('content-type')) {
      nextHeaders.set('content-type', 'application/json; charset=utf-8');
    }

-   return Request(
-     url,
-     RequestInit(
-       method: nextInit.method,
-       headers: nextHeaders,
-       body: nextInit.body,
-     ),
+   return Request._internal(
+     url: url,
+     method: _normalizeMethod(nextInit.method ?? 'POST'),
+     headers: nextHeaders,
+     bodyData: BodyData.fromInit(nextInit.body),
    );
  }

Note: This would require ensuring _applyDefaultBodyHeaders is called, so the full optimization may need additional adjustments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/fetch/request.dart` around lines 39 - 54, Request.json is performing
redundant Headers clones (via _coerceInit, nextInit.headers?.clone(),
RequestInit constructor, and Request constructor); to fix, avoid duplicate
allocations by passing the already-cloned headers from _coerceInit through to
the Request/RequestInit constructors without re-cloning: either use the headers
instance returned by _coerceInit (nextInit.headers) directly instead of calling
.clone() in Request.json, or add an internal/private constructor (e.g.,
RequestInit._internal or Request._internal) that accepts headers without cloning
and call that from Request.json; ensure _applyDefaultBodyHeaders still runs
against the final headers instance so behavior is unchanged.
lib/src/fetch/response.dart (1)

27-30: Use empty string for default statusText to achieve Fetch API parity.

The code explicitly claims to be "aligned with Fetch ResponseInit", but currently defaults statusText to the RFC reason phrase (e.g., 'OK', 'No Content') instead of the empty string that the Fetch API specifies. This makes constructor-created responses observably different from the web API.

Suggested fix
   }) : status = _validateStatus(init?.status ?? HttpStatus.ok),
-       statusText =
-           init?.statusText ??
-           HttpStatus.reasonPhrase(init?.status ?? HttpStatus.ok),
+       statusText = init?.statusText ?? '',
        headers = init?.headers?.clone() ?? Headers(),
        bodyData = BodyData.fromInit(body) {

Note: The test in test/request_response_test.dart that expects statusText to be 'No Content' will need to be updated to expect an empty string instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/fetch/response.dart` around lines 27 - 30, The Response constructor
currently defaults statusText to the RFC reason phrase via
HttpStatus.reasonPhrase in the initializer (see the statusText initializer in
response.dart), but to match Fetch API parity change the default to an empty
string by using init?.statusText ?? '' instead of HttpStatus.reasonPhrase(...);
update any tests (e.g., in test/request_response_test.dart) that expect 'No
Content' to expect an empty string and ensure _validateStatus(status) behavior
remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/fetch/response.dart`:
- Around line 80-85: The Response.redirect() call currently constructs a
redirected Response by invoking Response._create with url: location and
redirected: true; change it to match the Fetch Standard by creating the Response
with no response URL and not marked as redirected — call Response._create with
url: null (or omit the url) and redirected: false, while still passing the new
ResponseInit (e.g., ResponseInit(status: status, headers: nextHeaders)); update
the Response.redirect() implementation rather than changing Response._create so
the redirect helper returns a non-redirected Response with a null url.
- Around line 22-33: Response._create currently allows a non-empty body with
null-body statuses; update Response._create to validate that if
_validateStatus(init?.status ?? HttpStatus.ok) yields a null-body status (204,
205, 304) then BodyData.hasBody (from BodyData.fromInit(body) /
bodyData.hasBody) must be false and otherwise reject construction (throw/raise
an error). Modify the constructor to compute status and bodyData as now, then
check bodyData.hasBody against the status and throw a descriptive exception when
a body is present for null-body statuses; keep existing calls to _validateStatus
and _applyDefaultBodyHeaders. Add a regression test that constructs Response
with a body and ResponseInit(status: 204/205/304) and asserts that construction
fails.

---

Nitpick comments:
In `@lib/src/fetch/request.dart`:
- Around line 39-54: Request.json is performing redundant Headers clones (via
_coerceInit, nextInit.headers?.clone(), RequestInit constructor, and Request
constructor); to fix, avoid duplicate allocations by passing the already-cloned
headers from _coerceInit through to the Request/RequestInit constructors without
re-cloning: either use the headers instance returned by _coerceInit
(nextInit.headers) directly instead of calling .clone() in Request.json, or add
an internal/private constructor (e.g., RequestInit._internal or
Request._internal) that accepts headers without cloning and call that from
Request.json; ensure _applyDefaultBodyHeaders still runs against the final
headers instance so behavior is unchanged.

In `@lib/src/fetch/response.dart`:
- Around line 27-30: The Response constructor currently defaults statusText to
the RFC reason phrase via HttpStatus.reasonPhrase in the initializer (see the
statusText initializer in response.dart), but to match Fetch API parity change
the default to an empty string by using init?.statusText ?? '' instead of
HttpStatus.reasonPhrase(...); update any tests (e.g., in
test/request_response_test.dart) that expect 'No Content' to expect an empty
string and ensure _validateStatus(status) behavior remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4bc2c021-fe50-4056-b94b-c9692e06b9ef

📥 Commits

Reviewing files that changed from the base of the PR and between bc6d4ea and 8d6c375.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • README.md
  • example/main.dart
  • lib/src/core/http_status.dart
  • lib/src/core/mime_type.dart
  • lib/src/fetch/body.dart
  • lib/src/fetch/form_data.dart
  • lib/src/fetch/headers.dart
  • lib/src/fetch/request.dart
  • lib/src/fetch/response.dart
  • test/public_api_surface_test.dart
  • test/request_response_test.dart

Comment on lines +22 to 33
Response._create(
Object? body,
int status = HttpStatus.ok,
String? statusText,
Headers? headers,
this.url,
this.redirected = false,
}) : status = _validateStatus(status),
statusText = statusText ?? HttpStatus.reasonPhrase(status),
headers = headers?.clone() ?? Headers(),
ResponseInit? init, {
required this.url,
required this.redirected,
}) : status = _validateStatus(init?.status ?? HttpStatus.ok),
statusText =
init?.statusText ??
HttpStatus.reasonPhrase(init?.status ?? HttpStatus.ok),
headers = init?.headers?.clone() ?? Headers(),
bodyData = BodyData.fromInit(body) {
_applyDefaultBodyHeaders();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

According to the Fetch Response() constructor semantics, should a Response with a non-null body and status 204, 205, or 304 be rejected?

💡 Result:

Yes. The Fetch Standard defines 204, 205, and 304 as “null body status” codes [1]. In the new Response(body, init) constructor steps, if body is non-null and the response’s status is a null body status, the constructor must throw a TypeError (i.e., reject construction) [2].

Sources:
[1] Fetch Standard — “A null body status is … 204, 205, or 304” (fetch.spec.whatwg.org)
[2] Fetch Standard — Response constructor: “If body is non-null … if status is a null body status … throw a TypeError” (fetch.spec.whatwg.org)

Citations:


🏁 Script executed:

# First, check the full implementation of Response._create and related code
head -n 100 lib/src/fetch/response.dart | cat -n

Repository: medz/ht

Length of output: 3390


🏁 Script executed:

# Check if BodyData.fromInit handles null body properly
fd BodyData --type f -x grep -l "class BodyData" {}

Repository: medz/ht

Length of output: 33


🏁 Script executed:

# Search for any existing tests around null-body statuses
rg -n "204|205|304" test/ -A 2 -B 2

Repository: medz/ht

Length of output: 729


🏁 Script executed:

# Check for existing error handling in Response constructor
rg -n "ArgumentError|TypeError" lib/src/fetch/response.dart -B 2 -A 2

Repository: medz/ht

Length of output: 275


🏁 Script executed:

# Find and examine BodyData class
fd . lib/src/fetch -name "*.dart" -type f | xargs grep -l "class BodyData"

Repository: medz/ht

Length of output: 283


🏁 Script executed:

# Read the BodyData class to understand fromInit behavior
cat -n lib/src/fetch/body.dart | head -n 80

Repository: medz/ht

Length of output: 2671


🏁 Script executed:

# Search for any existing tests that might cover Response with body + null-body status
rg -n "Response.*204|Response.*205|Response.*304" test/ -B 2 -A 3

Repository: medz/ht

Length of output: 33


🏁 Script executed:

# Check what _validateStatus does
rg -n "_validateStatus" lib/src/fetch/response.dart -A 5

Repository: medz/ht

Length of output: 511


🏁 Script executed:

# Find BodyData.fromInit implementation
rg -n "fromInit" lib/src/fetch/ -A 15

Repository: medz/ht

Length of output: 2947


🏁 Script executed:

# Check if there are any tests attempting invalid Response construction
rg -n "ResponseInit.*status.*204\|ResponseInit.*status.*205\|ResponseInit.*status.*304" test/

Repository: medz/ht

Length of output: 33


🏁 Script executed:

# Look at the complete BodyData class to find hasBody or similar
cat lib/src/fetch/body.dart | grep -n "hasBody\|_present\|fromInit" -A 3 -B 1

Repository: medz/ht

Length of output: 1265


Add validation to reject bodies for 204/205/304 responses in Response._create.

The current implementation allows Response('x', ResponseInit(status: 204)) and similar invalid combinations. The Fetch Standard specifies that responses with status 204, 205, or 304 (null-body statuses) must not include a body; construction should be rejected. The shared _create constructor is the correct place to enforce this using the existing BodyData.hasBody property.

Suggested fix
   }) : status = _validateStatus(init?.status ?? HttpStatus.ok),
        statusText =
            init?.statusText ??
            HttpStatus.reasonPhrase(init?.status ?? HttpStatus.ok),
        headers = init?.headers?.clone() ?? Headers(),
        bodyData = BodyData.fromInit(body) {
+    if (bodyData.hasBody && const <int>{204, 205, 304}.contains(status)) {
+      throw ArgumentError.value(
+        status,
+        'status',
+        'Responses with status $status must not include a body',
+      );
+    }
     _applyDefaultBodyHeaders();
   }

Please add a regression test for this case alongside the fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/fetch/response.dart` around lines 22 - 33, Response._create currently
allows a non-empty body with null-body statuses; update Response._create to
validate that if _validateStatus(init?.status ?? HttpStatus.ok) yields a
null-body status (204, 205, 304) then BodyData.hasBody (from
BodyData.fromInit(body) / bodyData.hasBody) must be false and otherwise reject
construction (throw/raise an error). Modify the constructor to compute status
and bodyData as now, then check bodyData.hasBody against the status and throw a
descriptive exception when a body is present for null-body statuses; keep
existing calls to _validateStatus and _applyDefaultBodyHeaders. Add a regression
test that constructs Response with a body and ResponseInit(status: 204/205/304)
and asserts that construction fails.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
lib/src/fetch/request.dart (3)

108-114: Consider reducing header cloning in the factory path.

In the factory constructor path (e.g., Request.text), headers may be cloned up to three times:

  1. In RequestInit constructor (line 11)
  2. In _coerceInit (line 111)
  3. In Request constructor (line 24)

While correct, this is inefficient. Consider having _coerceInit pass headers through without cloning, relying on the final clone in Request.

♻️ Suggested optimization
   static RequestInit _coerceInit(RequestInit? init, {required Object? body}) {
     return RequestInit(
       method: init?.method ?? 'POST',
-      headers: init?.headers?.clone(),
+      headers: init?.headers,
       body: body,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/fetch/request.dart` around lines 108 - 114, The factory-path is
cloning headers multiple times; modify _coerceInit so it does not clone headers
(i.e., pass init?.headers through directly into the returned RequestInit) and
rely on the final clone inside the Request constructor to produce an owned
headers object; update the _coerceInit implementation to avoid a headers.clone()
call while preserving null-safety and existing behavior for RequestInit, leaving
RequestInit and Request constructors unchanged.

20-26: Minor: Headers may be cloned twice.

When a RequestInit is passed with headers, they're cloned in RequestInit's constructor (line 11) and again here (line 24). This is safe but slightly inefficient. Consider accepting already-cloned headers from RequestInit without re-cloning.

♻️ Suggested optimization
-  Request(Uri url, [RequestInit? init])
-    : this._create(
-        url: url,
-        method: init?.method ?? 'GET',
-        headers: init?.headers?.clone() ?? Headers(),
-        bodyData: BodyData.fromInit(init?.body),
-      );
+  Request(Uri url, [RequestInit? init])
+    : this._create(
+        url: url,
+        method: init?.method ?? 'GET',
+        headers: init?.headers ?? Headers(),
+        bodyData: BodyData.fromInit(init?.body),
+      );

Since RequestInit already clones headers on construction, re-cloning is unnecessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/fetch/request.dart` around lines 20 - 26, The Request constructor is
cloning headers redundantly; update Request(Uri url, [RequestInit? init]) to
pass init?.headers directly to the internal _create when present (instead of
calling .clone() again) so already-cloned headers from RequestInit are reused;
specifically modify the Request constructor call that currently does headers:
init?.headers?.clone() ?? Headers() to accept init?.headers ?? Headers(),
leaving Headers() fallback and leaving RequestInit and Headers.clone() behavior
unchanged.

49-62: Minor redundancy in Request.json.

At line 58, nextInit.method ?? 'POST' is redundant because _coerceInit already defaults method to 'POST' (line 110). The null-coalescing fallback will never trigger.

♻️ Simplified version
     return Request._create(
       url: url,
-      method: nextInit.method ?? 'POST',
+      method: nextInit.method!,
       headers: nextHeaders,
       bodyData: BodyData.fromInit(nextInit.body),
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/fetch/request.dart` around lines 49 - 62, The Request.json factory
redundantly uses nextInit.method ?? 'POST' even though _coerceInit already
defaults method to 'POST'; remove the unreachable null-coalescing fallback and
pass nextInit.method directly to Request._create (i.e., change the method
argument in Request.json to use nextInit.method) so the defaulting is
centralized in _coerceInit; update any relevant tests if they relied on the
previous fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/src/fetch/request.dart`:
- Around line 108-114: The factory-path is cloning headers multiple times;
modify _coerceInit so it does not clone headers (i.e., pass init?.headers
through directly into the returned RequestInit) and rely on the final clone
inside the Request constructor to produce an owned headers object; update the
_coerceInit implementation to avoid a headers.clone() call while preserving
null-safety and existing behavior for RequestInit, leaving RequestInit and
Request constructors unchanged.
- Around line 20-26: The Request constructor is cloning headers redundantly;
update Request(Uri url, [RequestInit? init]) to pass init?.headers directly to
the internal _create when present (instead of calling .clone() again) so
already-cloned headers from RequestInit are reused; specifically modify the
Request constructor call that currently does headers: init?.headers?.clone() ??
Headers() to accept init?.headers ?? Headers(), leaving Headers() fallback and
leaving RequestInit and Headers.clone() behavior unchanged.
- Around line 49-62: The Request.json factory redundantly uses nextInit.method
?? 'POST' even though _coerceInit already defaults method to 'POST'; remove the
unreachable null-coalescing fallback and pass nextInit.method directly to
Request._create (i.e., change the method argument in Request.json to use
nextInit.method) so the defaulting is centralized in _coerceInit; update any
relevant tests if they relied on the previous fallback.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 701a202a-0cc1-48d3-a60d-2daaad8c4f75

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6c375 and 38ffff3.

📒 Files selected for processing (3)
  • lib/src/fetch/request.dart
  • lib/src/fetch/response.dart
  • test/request_response_test.dart

@medz medz merged commit 819a5ca into main Mar 11, 2026
2 checks passed
@medz medz deleted the refactor/request-response-init branch March 11, 2026 19:32
Sign up for free to 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.

1 participant