Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adopt Fetch API Request and Response #292

Merged
merged 27 commits into from
Nov 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
195d243
feat(ClientRequest): support fetch api Response
kettanaito Oct 11, 2022
acc3f7a
feat(XMLHttpRequest): support fetch api Response
kettanaito Oct 11, 2022
2141929
test(fetch): support fetch api Response
kettanaito Oct 11, 2022
303e218
feat(XMLHttpRequest): support fetch api Response in the browser
kettanaito Oct 11, 2022
00effc6
feat: prevent nested intercepted requests
kettanaito Oct 12, 2022
157c41d
chore: continue with tests
kettanaito Oct 12, 2022
4b88d6c
Merge branch 'main' into feat/standard-api
kettanaito Oct 12, 2022
764c298
chore: regenerate yarn.lock
kettanaito Oct 12, 2022
57def2d
docs: update the readme
kettanaito Oct 12, 2022
6825755
feat: include "lib.dom" to annotate Response
kettanaito Oct 12, 2022
d36aa5e
fix: remove anything isomorphic response
kettanaito Oct 12, 2022
7e88618
feat(ClientRequest): support fetch api Request
kettanaito Oct 12, 2022
e7a3b44
chore: support fetch api Request everywhere else
kettanaito Oct 13, 2022
ae613c6
docs: update readme with fetch api Request
kettanaito Oct 13, 2022
2ffb9ad
test(ClientRequest): add "createRequest" unit test
kettanaito Oct 13, 2022
3034074
chore: remove unused code
kettanaito Oct 13, 2022
3890766
chore(ClientRequest): add the "passthrough" method
kettanaito Oct 13, 2022
f106377
chore: fix browser tests
kettanaito Oct 13, 2022
d52d71d
fix(XMLHttpRequest): return ArrayBuffer for "arraybuffer" response types
kettanaito Oct 13, 2022
d886833
fix(XMLHttpRequest): keep "status" as 0 for unresolved requests
kettanaito Oct 13, 2022
7b1935e
chore: fix credentials helpers
kettanaito Oct 13, 2022
80b1465
feat: include "requestId" in emitted events
kettanaito Oct 14, 2022
eba1aaf
chore(ClientRequest): remove unnecessary request header abstraction
kettanaito Oct 18, 2022
d8a4728
feat: support modifying outgoing request headers
kettanaito Oct 18, 2022
3bb4265
chore: use "createRequestWithCredentials"
kettanaito Nov 7, 2022
0da86fc
test(xhr): fix raw headers test
kettanaito Nov 7, 2022
f8c8096
Merge branch 'main' into feat/standard-api
kettanaito Nov 7, 2022
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
96 changes: 49 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class NodeClientRequest extends ClientRequest {
async end(...args) {
// Check if there's a mocked response for this request.
// You control this in the "resolver" function.
const mockedResponse = await resolver(isomorphicRequest)
const mockedResponse = await resolver(request)

// If there is a mocked response, use it to respond to this
// request, finalizing it afterward as if it received that
Expand Down Expand Up @@ -81,9 +81,9 @@ This library extends (or patches, where applicable) the following native modules
- `XMLHttpRequest`
- `fetch`

Once extended, it intercepts and normalizes all requests to the _isomorphic request instances_. The isomorphic request is an abstract representation of the request coming from different sources (`ClientRequest`, `XMLHttpRequest`, `window.Request`, etc.) that allows us to handle such requests in the same, unified manner.
Once extended, it intercepts and normalizes all requests to the Fetch API `Request` instances. This way, no matter the request source (`http.ClientRequest`, `XMLHttpRequest`, `window.Request`, etc), you always get a specification-compliant request instance to work with.

You can respond to an isomorphic request using an _isomorphic response_. In a similar way, the isomorphic response is a representation of the response to use for different requests. Responding to requests differs substantially when using modules like `http` or `XMLHttpRequest`. This library takes the responsibility for coercing isomorphic responses into appropriate responses depending on the request module automatically.
You can respond to the intercepted request by constructing a Fetch API Response instance. Instead of designing custom abstractions, this library respects the Fetch API specification and takes the responsibility to coerce a single response declaration to the appropriate response formats based on the request-issuing modules (like `http.OutgoingMessage` to respond to `http.ClientRequest`, or updating `XMLHttpRequest` response-related properties).

## What this library doesn't do

Expand Down Expand Up @@ -116,19 +116,14 @@ interceptor.apply()

// Listen to any "http.ClientRequest" being dispatched,
// and log its method and full URL.
interceptor.on('request', (request) => {
console.log(request.method, request.url.href)
interceptor.on('request', (request, requestId) => {
console.log(request.method, request.url)
})

// Listen to any responses sent to "http.ClientRequest".
// Note that this listener is read-only and cannot affect responses.
interceptor.on('response', (response, request) => {
console.log(
'response to %s %s was:',
request.method,
request.url.href,
response
)
console.log('response to %s %s was:', request.method, request.url, response)
})
```

Expand Down Expand Up @@ -203,71 +198,78 @@ interceptor.on('request', listener)

## Introspecting requests

All HTTP request interceptors emit a "request" event. In the listener to this event, they expose an isomorphic `request` instance—a normalized representation of the captured request.
All HTTP request interceptors emit a "request" event. In the listener to this event, they expose a `request` reference, which is a [Fetch API Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) instance.

> There are many ways to describe a request in Node.js, that's why this library exposes you a custom request instance that abstracts those details away from you, making request listeners uniform.
> There are many ways to describe a request in Node.js but this library coerces different request definitions to a single specification-compliant `Request` instance to make the handling consistent.

```js
interceptor.on('reqest', (request) => {})
interceptor.on('reqest', (request, requestId) => {
console.log(request.method, request.url)
})
```

The exposed `request` partially implements Fetch API [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) specification, containing the following properties and methods:
Since the exposed `request` instance implements the Fetch API specification, you can operate with it just as you do with the regular browser request. For example, this is how you would read the request body as JSON:

```ts
interface IsomorphicRequest {
id: string
url: URL
method: string
headers: Headers
credentials: 'omit' | 'same-origin' | 'include'
bodyUsed: boolean
clone(): IsomorphicRequest
arrayBuffer(): Promise<ArrayBuffer>
text(): Promise<string>
json(): Promise<Record<string, unknown>>
}
```js
interceptor.on('request', async (request, requestId) => {
const json = await request.clone().json()
})
```

For example, this is how you would read a JSON request body:
> **Do not forget to clone the request before reading its body!**

## Modifying requests

Request representations are readonly. You can, however, mutate the intercepted request's headers in the "request" listener:

```js
interceptor.on('request', async (request) => {
const json = await request.json()
interceptor.on('request', (request) => {
request.headers.set('X-My-Header', 'true')
})
```

> This restriction is done so that the library wouldn't have to unnecessarily synchronize the actual request instance and its Fetch API request representation. As of now, this library is not meant to be used as a full-scale proxy.

## Mocking responses

Although this library can be used purely for request introspection purposes, you can also affect request resolution by responding to any intercepted request within the "request" event.

Use the `request.respondWith()` method to respond to a request with a mocked response:

```js
interceptor.on('request', (request) => {
request.respondWith({
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: 'John',
lastName: 'Maverick',
}),
})
interceptor.on('request', (request, requestId) => {
request.respondWith(
new Response(
JSON.stringify({
firstName: 'John',
lastName: 'Maverick',
}),
{
status: 201,
statusText: 'Created',
headers: {
'Content-Type': 'application/json',
},
}
)
)
})
```

> We use Fetch API `Response` class as the middleground for mocked response definition. This library then coerces the response instance to the appropriate response format (e.g. to `http.OutgoingMessage` in the case of `http.ClientRequest`).

**The `Response` class is built-in in since Node.js 18. Use a Fetch API-compatible polyfill, like `node-fetch`, for older versions of Node.js.`**

Note that a single request _can only be handled once_. You may want to introduce conditional logic, like routing, in your request listener but it's generally advised to use a higher-level library like [Mock Service Worker](https://github.com/mswjs/msw) that does request matching for you.

Requests must be responded to within the same tick as the request listener. This means you cannot respond to a request using `setTimeout`, as this will delegate the callback to the next tick. If you wish to introduce asynchronous side-effects in the listener, consider making it an `async` function, awaiting any side-effects you need.

```js
// Respond to all requests with a 500 response
// delayed by 500ms.
interceptor.on('request', async (request) => {
interceptor.on('request', async (request, requestId) => {
await sleep(500)
request.respondWith({ status: 500 })
request.respondWith(new Response(null, { status: 500 }))
})
```

Expand Down Expand Up @@ -310,7 +312,7 @@ const interceptor = new BatchInterceptor({

interceptor.apply()

interceptor.on('request', (request) => {
interceptor.on('request', (request, requestId) => {
// Inspect the intercepted "request".
// Optionally, return a mocked response.
})
Expand Down Expand Up @@ -358,7 +360,7 @@ const resolver = new RemoteHttpResolver({
process: appProcess,
})

resolver.on('request', (request) => {
resolver.on('request', (request, requestId) => {
// Optionally, return a mocked response
// for a request that occurred in the "appProcess".
})
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@
"superagent": "^6.1.0",
"supertest": "^6.1.6",
"ts-jest": "^27.1.1",
"typescript": "4.3.5",
"typescript": "4.4.4",
"wait-for-expect": "^3.0.2"
},
"dependencies": {
"@open-draft/until": "^1.0.3",
"@remix-run/web-fetch": "^4.3.1",
"@types/debug": "^4.1.7",
"@xmldom/xmldom": "^0.8.3",
"debug": "^4.3.3",
Expand Down
24 changes: 0 additions & 24 deletions src/InteractiveIsomorphicRequest.ts

This file was deleted.

106 changes: 0 additions & 106 deletions src/IsomorphicRequest.test.ts

This file was deleted.

Loading