-
-
Notifications
You must be signed in to change notification settings - Fork 127
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support non-configurable responses (#677)
- Loading branch information
1 parent
21c095a
commit 1d1cc09
Showing
10 changed files
with
326 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
export interface FetchResponseInit extends ResponseInit { | ||
url?: string | ||
} | ||
|
||
export class FetchResponse extends Response { | ||
/** | ||
* Response status codes for responses that cannot have body. | ||
* @see https://fetch.spec.whatwg.org/#statuses | ||
*/ | ||
static readonly STATUS_CODES_WITHOUT_BODY = [101, 103, 204, 205, 304] | ||
|
||
static readonly STATUS_CODES_WITH_REDIRECT = [301, 302, 303, 307, 308] | ||
|
||
static isConfigurableStatusCode(status: number): boolean { | ||
return status >= 200 && status <= 599 | ||
} | ||
|
||
static isRedirectResponse(status: number): boolean { | ||
return FetchResponse.STATUS_CODES_WITH_REDIRECT.includes(status) | ||
} | ||
|
||
/** | ||
* Returns a boolean indicating whether the given response status | ||
* code represents a response that can have a body. | ||
*/ | ||
static isResponseWithBody(status: number): boolean { | ||
return !FetchResponse.STATUS_CODES_WITHOUT_BODY.includes(status) | ||
} | ||
|
||
static setUrl(url: string | undefined, response: Response): void { | ||
if (!url) { | ||
return | ||
} | ||
|
||
if (response.url != '') { | ||
return | ||
} | ||
|
||
Object.defineProperty(response, 'url', { | ||
value: url, | ||
enumerable: true, | ||
configurable: true, | ||
writable: false, | ||
}) | ||
} | ||
|
||
constructor(body?: BodyInit | null, init: FetchResponseInit = {}) { | ||
const status = init.status ?? 200 | ||
const safeStatus = FetchResponse.isConfigurableStatusCode(status) | ||
? status | ||
: 200 | ||
const finalBody = FetchResponse.isResponseWithBody(status) ? body : null | ||
|
||
super(finalBody, { | ||
...init, | ||
status: safeStatus, | ||
}) | ||
|
||
if (status !== safeStatus) { | ||
/** | ||
* @note Undici keeps an internal "Symbol(state)" that holds | ||
* the actual value of response status. Update that in Node.js. | ||
*/ | ||
const stateSymbol = Object.getOwnPropertySymbols(this).find( | ||
(symbol) => symbol.description === 'state' | ||
) | ||
if (stateSymbol) { | ||
const state = Reflect.get(this, stateSymbol) as object | ||
Reflect.set(state, 'status', status) | ||
} else { | ||
Object.defineProperty(this, 'status', { | ||
value: status, | ||
enumerable: true, | ||
configurable: true, | ||
writable: false, | ||
}) | ||
} | ||
} | ||
|
||
FetchResponse.setUrl(init.url, this) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
test/modules/XMLHttpRequest/compliance/xhr-response-non-configurable.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// @vitest-environment jsdom | ||
/** | ||
* @see https://github.com/mswjs/msw/issues/2307 | ||
*/ | ||
import { it, expect, beforeAll, afterEach, afterAll } from 'vitest' | ||
import { HttpServer } from '@open-draft/test-server/http' | ||
import { XMLHttpRequestInterceptor } from '../../../../src/interceptors/XMLHttpRequest' | ||
import { FetchResponse } from '../../../../src/utils/fetchUtils' | ||
import { createXMLHttpRequest, useCors } from '../../../helpers' | ||
import { DeferredPromise } from '@open-draft/deferred-promise' | ||
|
||
const interceptor = new XMLHttpRequestInterceptor() | ||
|
||
const httpServer = new HttpServer((app) => { | ||
app.use(useCors) | ||
app.get('/resource', (_req, res) => { | ||
res.writeHead(101, 'Switching Protocols') | ||
res.end() | ||
}) | ||
}) | ||
|
||
beforeAll(async () => { | ||
interceptor.apply() | ||
await httpServer.listen() | ||
}) | ||
|
||
afterEach(() => { | ||
interceptor.removeAllListeners() | ||
}) | ||
|
||
afterAll(async () => { | ||
interceptor.dispose() | ||
await httpServer.close() | ||
}) | ||
|
||
it('handles non-configurable responses from the actual server', async () => { | ||
const responsePromise = new DeferredPromise<Response>() | ||
interceptor.on('response', ({ response }) => { | ||
responsePromise.resolve(response) | ||
}) | ||
|
||
const request = await createXMLHttpRequest((request) => { | ||
request.open('GET', httpServer.http.url('/resource')) | ||
request.send() | ||
}) | ||
|
||
expect(request.status).toBe(101) | ||
expect(request.statusText).toBe('Switching Protocols') | ||
expect(request.responseText).toBe('') | ||
|
||
// Must expose the exact response in the listener. | ||
await expect(responsePromise).resolves.toHaveProperty('status', 101) | ||
}) | ||
|
||
it('supports mocking non-configurable responses', async () => { | ||
interceptor.on('request', ({ controller }) => { | ||
/** | ||
* @note The Fetch API `Response` will still error on | ||
* non-configurable status codes. Instead, use this helper class. | ||
*/ | ||
controller.respondWith(new FetchResponse(null, { status: 101 })) | ||
}) | ||
|
||
const responsePromise = new DeferredPromise<Response>() | ||
interceptor.on('response', ({ response }) => { | ||
responsePromise.resolve(response) | ||
}) | ||
|
||
const request = await createXMLHttpRequest((request) => { | ||
request.open('GET', httpServer.http.url('/resource')) | ||
request.send() | ||
}) | ||
|
||
expect(request.status).toBe(101) | ||
expect(request.responseText).toBe('') | ||
|
||
// Must expose the exact response in the listener. | ||
await expect(responsePromise).resolves.toHaveProperty('status', 101) | ||
}) |
66 changes: 66 additions & 0 deletions
66
test/modules/fetch/compliance/fetch-response-non-configurable.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// @vitest-environment node | ||
import { vi, it, expect, beforeAll, afterEach, afterAll } from 'vitest' | ||
import { HttpServer } from '@open-draft/test-server/http' | ||
import { DeferredPromise } from '@open-draft/deferred-promise' | ||
import { FetchInterceptor } from '../../../../src/interceptors/fetch' | ||
import { FetchResponse } from '../../../../src/utils/fetchUtils' | ||
|
||
const interceptor = new FetchInterceptor() | ||
|
||
const httpServer = new HttpServer((app) => { | ||
app.get('/resource', (_req, res) => { | ||
res.writeHead(101, 'Switching Protocols') | ||
res.set('connection', 'upgrade') | ||
res.set('upgrade', 'HTTP/2.0') | ||
res.end() | ||
}) | ||
}) | ||
|
||
beforeAll(async () => { | ||
interceptor.apply() | ||
await httpServer.listen() | ||
}) | ||
|
||
afterEach(() => { | ||
interceptor.removeAllListeners() | ||
}) | ||
|
||
afterAll(async () => { | ||
interceptor.dispose() | ||
await httpServer.close() | ||
}) | ||
|
||
it('handles non-configurable responses from the actual server', async () => { | ||
const responseListener = vi.fn() | ||
interceptor.on('response', responseListener) | ||
|
||
// Fetch doesn't handle 101 responses by spec. | ||
await expect(fetch(httpServer.http.url('/resource'))).rejects.toThrow( | ||
'fetch failed' | ||
) | ||
|
||
// Must not call the response listner. Fetch failed. | ||
expect(responseListener).not.toHaveBeenCalled() | ||
}) | ||
|
||
it('supports mocking non-configurable responses', async () => { | ||
interceptor.on('request', ({ controller }) => { | ||
/** | ||
* @note The Fetch API `Response` will still error on | ||
* non-configurable status codes. Instead, use this helper class. | ||
*/ | ||
controller.respondWith(new FetchResponse(null, { status: 101 })) | ||
}) | ||
|
||
const responsePromise = new DeferredPromise<Response>() | ||
interceptor.on('response', ({ response }) => { | ||
responsePromise.resolve(response) | ||
}) | ||
|
||
const response = await fetch('http://localhost/irrelevant') | ||
|
||
expect(response.status).toBe(101) | ||
|
||
// Must expose the exact response in the listener. | ||
await expect(responsePromise).resolves.toHaveProperty('status', 101) | ||
}) |
Oops, something went wrong.