253 lines
10 KiB
JavaScript
253 lines
10 KiB
JavaScript
const require_getRawRequest = require('./getRawRequest-zx8rUJL2.cjs');
|
|
const require_createRequestId = require('./createRequestId-Cs4oXfa1.cjs');
|
|
const require_hasConfigurableGlobal = require('./hasConfigurableGlobal-BvCTG97d.cjs');
|
|
const require_handleRequest = require('./handleRequest-CvX2G-Lz.cjs');
|
|
let _open_draft_deferred_promise = require("@open-draft/deferred-promise");
|
|
let outvariant = require("outvariant");
|
|
let _open_draft_until = require("@open-draft/until");
|
|
|
|
//#region src/interceptors/fetch/utils/createNetworkError.ts
|
|
function createNetworkError(cause) {
|
|
return Object.assign(/* @__PURE__ */ new TypeError("Failed to fetch"), { cause });
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/fetch/utils/followRedirect.ts
|
|
const REQUEST_BODY_HEADERS = [
|
|
"content-encoding",
|
|
"content-language",
|
|
"content-location",
|
|
"content-type",
|
|
"content-length"
|
|
];
|
|
const kRedirectCount = Symbol("kRedirectCount");
|
|
/**
|
|
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1210
|
|
*/
|
|
async function followFetchRedirect(request, response) {
|
|
if (response.status !== 303 && request.body != null) return Promise.reject(createNetworkError());
|
|
const requestUrl = new URL(request.url);
|
|
let locationUrl;
|
|
try {
|
|
locationUrl = new URL(response.headers.get("location"), request.url);
|
|
} catch (error) {
|
|
return Promise.reject(createNetworkError(error));
|
|
}
|
|
if (!(locationUrl.protocol === "http:" || locationUrl.protocol === "https:")) return Promise.reject(createNetworkError("URL scheme must be a HTTP(S) scheme"));
|
|
if (Reflect.get(request, kRedirectCount) > 20) return Promise.reject(createNetworkError("redirect count exceeded"));
|
|
Object.defineProperty(request, kRedirectCount, { value: (Reflect.get(request, kRedirectCount) || 0) + 1 });
|
|
if (request.mode === "cors" && (locationUrl.username || locationUrl.password) && !sameOrigin(requestUrl, locationUrl)) return Promise.reject(createNetworkError("cross origin not allowed for request mode \"cors\""));
|
|
const requestInit = {};
|
|
if ([301, 302].includes(response.status) && request.method === "POST" || response.status === 303 && !["HEAD", "GET"].includes(request.method)) {
|
|
requestInit.method = "GET";
|
|
requestInit.body = null;
|
|
REQUEST_BODY_HEADERS.forEach((headerName) => {
|
|
request.headers.delete(headerName);
|
|
});
|
|
}
|
|
if (!sameOrigin(requestUrl, locationUrl)) {
|
|
request.headers.delete("authorization");
|
|
request.headers.delete("proxy-authorization");
|
|
request.headers.delete("cookie");
|
|
request.headers.delete("host");
|
|
}
|
|
/**
|
|
* @note Undici "safely" extracts the request body.
|
|
* I suspect we cannot dispatch this request again
|
|
* since its body has been read and the stream is locked.
|
|
*/
|
|
requestInit.headers = request.headers;
|
|
const finalResponse = await fetch(new Request(locationUrl, requestInit));
|
|
Object.defineProperty(finalResponse, "redirected", {
|
|
value: true,
|
|
configurable: true
|
|
});
|
|
return finalResponse;
|
|
}
|
|
/**
|
|
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/util.js#L761
|
|
*/
|
|
function sameOrigin(left, right) {
|
|
if (left.origin === right.origin && left.origin === "null") return true;
|
|
if (left.protocol === right.protocol && left.hostname === right.hostname && left.port === right.port) return true;
|
|
return false;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/fetch/utils/brotli-decompress.browser.ts
|
|
var BrotliDecompressionStream = class extends TransformStream {
|
|
constructor() {
|
|
console.warn("[Interceptors]: Brotli decompression of response streams is not supported in the browser");
|
|
super({ transform(chunk, controller) {
|
|
controller.enqueue(chunk);
|
|
} });
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/interceptors/fetch/utils/decompression.ts
|
|
var PipelineStream = class extends TransformStream {
|
|
constructor(transformStreams, ...strategies) {
|
|
super({}, ...strategies);
|
|
const readable = [super.readable, ...transformStreams].reduce((readable$1, transform) => readable$1.pipeThrough(transform));
|
|
Object.defineProperty(this, "readable", { get() {
|
|
return readable;
|
|
} });
|
|
}
|
|
};
|
|
function parseContentEncoding(contentEncoding) {
|
|
return contentEncoding.toLowerCase().split(",").map((coding) => coding.trim());
|
|
}
|
|
function createDecompressionStream(contentEncoding) {
|
|
if (contentEncoding === "") return null;
|
|
const codings = parseContentEncoding(contentEncoding);
|
|
if (codings.length === 0) return null;
|
|
return new PipelineStream(codings.reduceRight((transformers, coding) => {
|
|
if (coding === "gzip" || coding === "x-gzip") return transformers.concat(new DecompressionStream("gzip"));
|
|
else if (coding === "deflate") return transformers.concat(new DecompressionStream("deflate"));
|
|
else if (coding === "br") return transformers.concat(new BrotliDecompressionStream());
|
|
else transformers.length = 0;
|
|
return transformers;
|
|
}, []));
|
|
}
|
|
function decompressResponse(response) {
|
|
if (response.body === null) return null;
|
|
const decompressionStream = createDecompressionStream(response.headers.get("content-encoding") || "");
|
|
if (!decompressionStream) return null;
|
|
response.body.pipeTo(decompressionStream.writable);
|
|
return decompressionStream.readable;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/fetch/index.ts
|
|
var FetchInterceptor = class FetchInterceptor extends require_createRequestId.Interceptor {
|
|
static {
|
|
this.symbol = Symbol("fetch");
|
|
}
|
|
constructor() {
|
|
super(FetchInterceptor.symbol);
|
|
}
|
|
checkEnvironment() {
|
|
return require_hasConfigurableGlobal.hasConfigurableGlobal("fetch");
|
|
}
|
|
async setup() {
|
|
const pureFetch = globalThis.fetch;
|
|
(0, outvariant.invariant)(!pureFetch[require_getRawRequest.IS_PATCHED_MODULE], "Failed to patch the \"fetch\" module: already patched.");
|
|
globalThis.fetch = async (input, init) => {
|
|
const requestId = require_createRequestId.createRequestId();
|
|
/**
|
|
* @note Resolve potentially relative request URL
|
|
* against the present `location`. This is mainly
|
|
* for native `fetch` in JSDOM.
|
|
* @see https://github.com/mswjs/msw/issues/1625
|
|
*/
|
|
const resolvedInput = typeof input === "string" && typeof location !== "undefined" && !require_getRawRequest.canParseUrl(input) ? new URL(input, location.href) : input;
|
|
const request = new Request(resolvedInput, init);
|
|
/**
|
|
* @note Set the raw request only if a Request instance was provided to fetch.
|
|
*/
|
|
if (input instanceof Request) require_getRawRequest.setRawRequest(request, input);
|
|
const responsePromise = new _open_draft_deferred_promise.DeferredPromise();
|
|
const controller = new require_getRawRequest.RequestController(request, {
|
|
passthrough: async () => {
|
|
this.logger.info("request has not been handled, passthrough...");
|
|
/**
|
|
* @note Clone the request instance right before performing it.
|
|
* This preserves any modifications made to the intercepted request
|
|
* in the "request" listener. This also allows the user to read the
|
|
* request body in the "response" listener (otherwise "unusable").
|
|
*/
|
|
const requestCloneForResponseEvent = request.clone();
|
|
const { error: responseError, data: originalResponse } = await (0, _open_draft_until.until)(() => pureFetch(request));
|
|
if (responseError) return responsePromise.reject(responseError);
|
|
this.logger.info("original fetch performed", originalResponse);
|
|
if (this.emitter.listenerCount("response") > 0) {
|
|
this.logger.info("emitting the \"response\" event...");
|
|
const responseClone = originalResponse.clone();
|
|
await require_hasConfigurableGlobal.emitAsync(this.emitter, "response", {
|
|
response: responseClone,
|
|
isMockedResponse: false,
|
|
request: requestCloneForResponseEvent,
|
|
requestId
|
|
});
|
|
}
|
|
responsePromise.resolve(originalResponse);
|
|
},
|
|
respondWith: async (rawResponse) => {
|
|
if (require_handleRequest.isResponseError(rawResponse)) {
|
|
this.logger.info("request has errored!", { response: rawResponse });
|
|
responsePromise.reject(createNetworkError(rawResponse));
|
|
return;
|
|
}
|
|
this.logger.info("received mocked response!", { rawResponse });
|
|
const decompressedStream = decompressResponse(rawResponse);
|
|
const response = decompressedStream === null ? rawResponse : new require_getRawRequest.FetchResponse(decompressedStream, rawResponse);
|
|
require_getRawRequest.FetchResponse.setUrl(request.url, response);
|
|
/**
|
|
* Undici's handling of following redirect responses.
|
|
* Treat the "manual" redirect mode as a regular mocked response.
|
|
* This way, the client can manually follow the redirect it receives.
|
|
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1173
|
|
*/
|
|
if (require_getRawRequest.FetchResponse.isRedirectResponse(response.status)) {
|
|
if (request.redirect === "error") {
|
|
responsePromise.reject(createNetworkError("unexpected redirect"));
|
|
return;
|
|
}
|
|
if (request.redirect === "follow") {
|
|
followFetchRedirect(request, response).then((response$1) => {
|
|
responsePromise.resolve(response$1);
|
|
}, (reason) => {
|
|
responsePromise.reject(reason);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
if (this.emitter.listenerCount("response") > 0) {
|
|
this.logger.info("emitting the \"response\" event...");
|
|
await require_hasConfigurableGlobal.emitAsync(this.emitter, "response", {
|
|
response: response.clone(),
|
|
isMockedResponse: true,
|
|
request,
|
|
requestId
|
|
});
|
|
}
|
|
responsePromise.resolve(response);
|
|
},
|
|
errorWith: (reason) => {
|
|
this.logger.info("request has been aborted!", { reason });
|
|
responsePromise.reject(reason);
|
|
}
|
|
});
|
|
this.logger.info("[%s] %s", request.method, request.url);
|
|
this.logger.info("awaiting for the mocked response...");
|
|
this.logger.info("emitting the \"request\" event for %s listener(s)...", this.emitter.listenerCount("request"));
|
|
await require_handleRequest.handleRequest({
|
|
request,
|
|
requestId,
|
|
emitter: this.emitter,
|
|
controller
|
|
});
|
|
return responsePromise;
|
|
};
|
|
Object.defineProperty(globalThis.fetch, require_getRawRequest.IS_PATCHED_MODULE, {
|
|
enumerable: true,
|
|
configurable: true,
|
|
value: true
|
|
});
|
|
this.subscriptions.push(() => {
|
|
Object.defineProperty(globalThis.fetch, require_getRawRequest.IS_PATCHED_MODULE, { value: void 0 });
|
|
globalThis.fetch = pureFetch;
|
|
this.logger.info("restored native \"globalThis.fetch\"!", globalThis.fetch.name);
|
|
});
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
Object.defineProperty(exports, 'FetchInterceptor', {
|
|
enumerable: true,
|
|
get: function () {
|
|
return FetchInterceptor;
|
|
}
|
|
});
|
|
//# sourceMappingURL=fetch-U3v3Y4ap.cjs.map
|