diff --git a/openspec/changes/custom-response-object/.openspec.yaml b/openspec/changes/archive/2026-08-02-custom-response-object/.openspec.yaml similarity index 100% rename from openspec/changes/custom-response-object/.openspec.yaml rename to openspec/changes/archive/2026-08-02-custom-response-object/.openspec.yaml diff --git a/openspec/changes/custom-response-object/design.md b/openspec/changes/archive/2026-08-02-custom-response-object/design.md similarity index 100% rename from openspec/changes/custom-response-object/design.md rename to openspec/changes/archive/2026-08-02-custom-response-object/design.md diff --git a/openspec/changes/custom-response-object/proposal.md b/openspec/changes/archive/2026-08-02-custom-response-object/proposal.md similarity index 100% rename from openspec/changes/custom-response-object/proposal.md rename to openspec/changes/archive/2026-08-02-custom-response-object/proposal.md diff --git a/openspec/changes/custom-response-object/specs/http-client/spec.md b/openspec/changes/archive/2026-08-02-custom-response-object/specs/http-client/spec.md similarity index 100% rename from openspec/changes/custom-response-object/specs/http-client/spec.md rename to openspec/changes/archive/2026-08-02-custom-response-object/specs/http-client/spec.md diff --git a/openspec/changes/custom-response-object/tasks.md b/openspec/changes/archive/2026-08-02-custom-response-object/tasks.md similarity index 100% rename from openspec/changes/custom-response-object/tasks.md rename to openspec/changes/archive/2026-08-02-custom-response-object/tasks.md diff --git a/openspec/specs/http-client/spec.md b/openspec/specs/http-client/spec.md index 9b4fd63..1e52b0d 100644 --- a/openspec/specs/http-client/spec.md +++ b/openspec/specs/http-client/spec.md @@ -48,7 +48,9 @@ Each HTTP method on the returned client SHALL resolve a relative request path ag - **THEN** the request fails the same way a bare `fetch("/users")` would fail in that environment, since there is no base to resolve against ### Requirement: HTTP method wrappers -Each of `get`, `post`, `put`, `patch`, `delete`, `head`, and `options` SHALL issue a `fetch` request using its corresponding uppercase HTTP method (e.g. `get` issues a request with method `GET`), forwarding any caller-supplied request-init options (headers, body, signal, etc.) other than `method`, and SHALL return the resulting `Response`. +Each of `get`, `post`, `put`, `patch`, `delete`, `head`, and `options` SHALL issue a `fetch` request using its corresponding uppercase HTTP method (e.g. `get` issues a request with method `GET`), forwarding any caller-supplied request-init options (headers, body, signal, etc.) other than `method`, and SHALL be generic over an optional type parameter `T` (defaulting to `string`), resolving to a `FetchifyResponse` object: `{ data: T | null; response: Response }`. + +`data` SHALL be populated only when the underlying `Response`'s `ok` is `true`: the body SHALL be read as text and `JSON.parse`d; if parsing succeeds, `data` SHALL be the parsed value, and if the body is not valid JSON, `data` SHALL be the raw text. When the underlying response is not `ok`, or the body cannot be read, `data` SHALL be `null`. `response` SHALL be a clone of the original `fetch` `Response`, taken before any body is read for `data`, so it remains fully unread and usable by the caller (`.json()`, `.blob()`, `.text()`, `status`, `headers`, `ok`, etc.). #### Scenario: Method sets the correct HTTP verb - **WHEN** a client calls `client.post("/users", { body: JSON.stringify({ name: "a" }) })` @@ -58,6 +60,30 @@ Each of `get`, `post`, `put`, `patch`, `delete`, `head`, and `options` SHALL iss - **WHEN** a client calls `client.get("/users", { method: "POST" })` - **THEN** the underlying request is still sent with HTTP method `GET` -#### Scenario: Returned value is the fetch Response +#### Scenario: ok JSON response yields parsed data +- **WHEN** any HTTP method wrapper resolves, the underlying response's `ok` is `true`, and the body is valid JSON +- **THEN** the resolved value's `data` is the parsed JSON value + +#### Scenario: ok non-JSON response yields text data +- **WHEN** any HTTP method wrapper resolves, the underlying response's `ok` is `true`, and the body is not valid JSON +- **THEN** the resolved value's `data` is the response body read as text + +#### Scenario: Non-ok response yields null data +- **WHEN** any HTTP method wrapper resolves and the underlying response's `ok` is `false` +- **THEN** the resolved value's `data` is `null` + +#### Scenario: Body read failure yields null data +- **WHEN** the underlying response's `ok` is `true` but reading its body fails +- **THEN** the resolved value's `data` is `null` + +#### Scenario: response is an unread clone of the original Response - **WHEN** any HTTP method wrapper resolves -- **THEN** it resolves to the `Response` object produced by the underlying `fetch` call, unmodified +- **THEN** the resolved value's `response` is a clone of the `Response` produced by the underlying `fetch` call, with its body not yet consumed, so the caller can independently call `.text()`, `.json()`, or read `status`/`headers`/`ok` on it + +#### Scenario: Caller specifies a type parameter +- **WHEN** a consumer calls `client.get("/user")` +- **THEN** the resolved value's `data` has static type `User | null`, and at runtime holds the JSON-parsed response body + +#### Scenario: Caller omits the type parameter +- **WHEN** a consumer calls `client.get("/user")` without a type parameter +- **THEN** the resolved value's `data` has static type `string | null`, matching the default parse-then-fallback behavior