Skip to content

Commit

Permalink
Merge pull request #824 from wheresrhys/rhys/body-types
Browse files Browse the repository at this point in the history
fix: handle all types of BodyInit correctly
  • Loading branch information
wheresrhys authored Aug 30, 2024
2 parents 7c74168 + dde5e6b commit b661ba2
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 146 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ references:

node18: &node18
docker:
- image: cimg/node:18.0
- image: cimg/node:18.11
nodelts: &nodelts
docker:
- image: cimg/node:lts
Expand Down
8 changes: 1 addition & 7 deletions docs/docs/@fetch-mock/core/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,11 @@ fetchMock.config.sendAsJson = false;

Options marked with a `` can also be overridden for individual calls to `.mock(matcher, response, options)` by setting as properties on the `options` parameter

### sendAsJson<sup>†</sup>

`{Boolean}` default: `true`

Always convert objects passed to `.mock()` to JSON strings before building reponses. Can be useful to set to `false` globally if e.g. dealing with a lot of `ArrayBuffer`s. When `true` the `Content-Type: application/json` header will also be set on each response.

### includeContentLength<sup>†</sup>

`{Boolean}` default: `true`

Sets a `Content-Length` header on each response.
Sets a `Content-Length` header on each response, with the exception of responses whose body is a `FormData` or `ReadableStream` instance as these are hard/impossible to calculate up front.

### matchPartialBody

Expand Down
18 changes: 14 additions & 4 deletions docs/docs/@fetch-mock/core/route/response.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ If the object _only_ contains properties from among those listed below it is use

### body

`{String|Object}`
`{String|Object|BodyInit}`

Set the `Response` body, e.g. `"Server responded ok"`, `{ token: 'abcdef' }`. See the `Object` section of the docs below for behaviour when passed an `Object`.
Set the `Response` body. This could be

- a string e.g. `"Server responded ok"`, `{ token: 'abcdef' }`.
- an object literal (see the `Object` section of the docs below).
- Anything else that satisfies the specification for the [body parameter of new Response()](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#body). This currently allows instances of Blob, ArrayBuffer, TypedArray, DataView, FormData, ReadableStream, URLSearchParams, and String.

### status

Expand Down Expand Up @@ -62,9 +66,15 @@ Forces `fetch` to return a `Promise` rejected with the value of `throws` e.g. `n

## Object

`{Object|ArrayBuffer|...`
`{Object}`

Any object literal that does not match the schema for a response config will be converted to a `JSON` string and set as the response `body`.

The `Content-Type: application/json` header will also be set on each response. To send JSON responses that do not set this header (e.g. to mock a poorly configured server) manually convert the object to a string first e.g.

If the `sendAsJson` option is set to `true`, any object that does not match the schema for a response config will be converted to a `JSON` string and set as the response `body`. Otherwise, the object will be set as the response `body` (useful for `ArrayBuffer`s etc.)
```js
fetchMock.route('http://a.com', JSON.stringify({ prop: 'value' }));
```

## Promise

Expand Down
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"types": "./dist/esm/index.d.ts",
"type": "module",
"engines": {
"node": ">=18.0.0"
"node": ">=18.11.0"
},
"dependencies": {
"@types/glob-to-regexp": "^0.4.4",
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/FetchMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import CallHistory from './CallHistory.js';
import * as requestUtils from './RequestUtils.js';

export type FetchMockGlobalConfig = {
sendAsJson?: boolean;
includeContentLength?: boolean;
matchPartialBody?: boolean;
allowRelativeUrls?: boolean;
Expand All @@ -20,7 +19,6 @@ export type FetchMockConfig = FetchMockGlobalConfig & FetchImplementations;

export const defaultFetchMockConfig: FetchMockConfig = {
includeContentLength: true,
sendAsJson: true,
matchPartialBody: false,
Request: globalThis.Request,
Response: globalThis.Response,
Expand Down
63 changes: 48 additions & 15 deletions packages/core/src/Route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type RouteConfig = UserRouteConfig &
FetchImplementations &
InternalRouteConfig;
export type RouteResponseConfig = {
body?: string | object;
body?: BodyInit | object;
status?: number;
headers?: {
[key: string]: string;
Expand Down Expand Up @@ -72,6 +72,22 @@ export type RouteResponse =
| RouteResponseFunction;
export type RouteName = string;

function isBodyInit(body: BodyInit | object): body is BodyInit {
return (
body instanceof Blob ||
body instanceof ArrayBuffer ||
// checks for TypedArray
ArrayBuffer.isView(body) ||
body instanceof DataView ||
body instanceof FormData ||
body instanceof ReadableStream ||
body instanceof URLSearchParams ||
body instanceof String ||
typeof body === 'string' ||
body === null
);
}

function sanitizeStatus(status?: number): number {
if (!status) {
return 200;
Expand Down Expand Up @@ -194,31 +210,48 @@ class Route {
constructResponseBody(
responseInput: RouteResponseConfig,
responseOptions: ResponseInitUsingHeaders,
): string | null {
// start to construct the body
): BodyInit {
let body = responseInput.body;
// convert to json if we need to
if (typeof body === 'object') {
if (this.config.sendAsJson && responseInput.body != null) {
const bodyIsBodyInit = isBodyInit(body);

if (!bodyIsBodyInit) {
if (typeof body === 'undefined') {
body = null;
} else if (typeof body === 'object') {
// convert to json if we need to
body = JSON.stringify(body);
if (!responseOptions.headers.has('Content-Type')) {
responseOptions.headers.set('Content-Type', 'application/json');
}
} else {
throw new TypeError('Invalid body provided to construct response');
}
}

if (typeof body === 'string') {
// add a Content-Length header if we need to
if (
this.config.includeContentLength &&
!responseOptions.headers.has('Content-Length')
// add a Content-Length header if we need to
if (
this.config.includeContentLength &&
!responseOptions.headers.has('Content-Length') &&
!(body instanceof ReadableStream) &&
!(body instanceof FormData)
) {
let length = 0;
if (body instanceof Blob) {
length = body.size;
} else if (
body instanceof ArrayBuffer ||
ArrayBuffer.isView(body) ||
body instanceof DataView
) {
responseOptions.headers.set('Content-Length', body.length.toString());
length = body.byteLength;
} else if (body instanceof URLSearchParams) {
length = body.toString().length;
} else if (typeof body === 'string' || body instanceof String) {
length = body.length;
}
return body;
responseOptions.headers.set('Content-Length', length.toString());
}
// @ts-expect-error TODO need to implement handling of non-string bodies properlyy
return body || null;
return body as BodyInit;
}

static defineMatcher(matcher: MatcherDefinition) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,8 @@ export default class Router {
if (typeof response[name] === 'function') {
//@ts-expect-error TODO probably make use of generics here
return new Proxy(response[name], {
apply: (matcherFunction, thisArg, args) => {
const result = matcherFunction.apply(response, args);
apply: (func, thisArg, args) => {
const result = func.apply(response, args);
if (result.then) {
pendingPromises.push(
//@ts-expect-error TODO probably make use of generics here
Expand Down
Loading

0 comments on commit b661ba2

Please sign in to comment.