Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions openspec/specs/http-client/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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" }) })`
Expand All @@ -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>("/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
Loading