Skip to content

Commit

Permalink
feat: [1502] Add the possibility to intercept sync requests
Browse files Browse the repository at this point in the history
  • Loading branch information
OlaviSau committed Jan 4, 2025
1 parent a9ec9d8 commit ea4f90f
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 72 deletions.
6 changes: 2 additions & 4 deletions packages/happy-dom/src/browser/types/IBrowserSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js';
import IAsyncRequestInterceptor from '../../fetch/types/IAsyncRequestInterceptor.js';
import IFetchInterceptor from '../../fetch/types/IFetchInterceptor.js';

/**
* Browser settings.
Expand Down Expand Up @@ -42,9 +42,7 @@ export default interface IBrowserSettings {
*/
disableSameOriginPolicy: boolean;

intercept?: {
asyncFetch?: IAsyncRequestInterceptor;
};
interceptor?: IFetchInterceptor;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js';
import IAsyncRequestInterceptor from '../../fetch/types/IAsyncRequestInterceptor.js';
import IFetchInterceptor from '../../fetch/types/IFetchInterceptor.js';

export default interface IOptionalBrowserSettings {
/** Disables JavaScript evaluation. */
Expand Down Expand Up @@ -36,9 +36,7 @@ export default interface IOptionalBrowserSettings {
*/
disableSameOriginPolicy?: boolean;

intercept?: {
asyncFetch?: IAsyncRequestInterceptor;
};
interceptor?: IFetchInterceptor;
};

/**
Expand Down
18 changes: 10 additions & 8 deletions packages/happy-dom/src/fetch/Fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import FetchResponseHeaderUtility from './utilities/FetchResponseHeaderUtility.j
import FetchHTTPSCertificate from './certificate/FetchHTTPSCertificate.js';
import { Buffer } from 'buffer';
import FetchBodyUtility from './utilities/FetchBodyUtility.js';
import IAsyncRequestInterceptor from './types/IAsyncRequestInterceptor.js';
import IFetchInterceptor from './types/IFetchInterceptor.js';

const LAST_CHUNK = Buffer.from('0\r\n\r\n');

Expand All @@ -51,7 +51,7 @@ export default class Fetch {
private nodeResponse: IncomingMessage | null = null;
private response: Response | null = null;
private responseHeaders: Headers | null = null;
private requestInterceptor?: IAsyncRequestInterceptor;
private interceptor?: IFetchInterceptor;
private request: Request;
private redirectCount = 0;
private disableCache: boolean;
Expand Down Expand Up @@ -101,8 +101,7 @@ export default class Fetch {
options.disableSameOriginPolicy ??
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
false;
this.requestInterceptor =
this.#browserFrame.page.context.browser.settings.fetch.intercept?.asyncFetch;
this.interceptor = this.#browserFrame.page.context.browser.settings.fetch.interceptor;
}

/**
Expand All @@ -112,11 +111,14 @@ export default class Fetch {
*/
public async send(): Promise<Response> {
FetchRequestReferrerUtility.prepareRequest(new URL(this.#window.location.href), this.request);
const beforeSendResponse = this.requestInterceptor?.beforeSend
? await this.requestInterceptor?.beforeSend(this.request, this.#window)
const beforeRequestResponse = this.interceptor?.beforeAsyncRequest
? await this.interceptor?.beforeAsyncRequest({
request: this.request,
window: this.#window
})
: undefined;
if (beforeSendResponse instanceof Response) {
return beforeSendResponse;
if (beforeRequestResponse instanceof Response) {
return beforeRequestResponse;
}
FetchRequestValidationUtility.validateSchema(this.request);

Expand Down
12 changes: 12 additions & 0 deletions packages/happy-dom/src/fetch/SyncFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Zlib from 'zlib';
import FetchResponseRedirectUtility from './utilities/FetchResponseRedirectUtility.js';
import FetchCORSUtility from './utilities/FetchCORSUtility.js';
import Fetch from './Fetch.js';
import IFetchInterceptor from "./types/IFetchInterceptor.js";

Check warning on line 23 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `"./types/IFetchInterceptor.js"` with `'./types/IFetchInterceptor.js'`

Check warning on line 23 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `"./types/IFetchInterceptor.js"` with `'./types/IFetchInterceptor.js'`

Check warning on line 23 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (22)

Replace `"./types/IFetchInterceptor.js"` with `'./types/IFetchInterceptor.js'`

interface ISyncHTTPResponse {
error: string;
Expand All @@ -39,6 +40,7 @@ export default class SyncFetch {
private redirectCount = 0;
private disableCache: boolean;
private disableSameOriginPolicy: boolean;
private interceptor?: IFetchInterceptor;
#browserFrame: IBrowserFrame;
#window: BrowserWindow;
#unfilteredHeaders: Headers | null = null;
Expand Down Expand Up @@ -84,6 +86,7 @@ export default class SyncFetch {
options.disableSameOriginPolicy ??
this.#browserFrame.page.context.browser.settings.fetch.disableSameOriginPolicy ??
false;
this.interceptor = this.#browserFrame.page.context.browser.settings.fetch.interceptor;
}

/**
Expand All @@ -93,6 +96,15 @@ export default class SyncFetch {
*/
public send(): ISyncResponse {
FetchRequestReferrerUtility.prepareRequest(new URL(this.#window.location.href), this.request);
const beforeRequestResponse = this.interceptor?.beforeSyncRequest
? this.interceptor?.beforeSyncRequest({
request: this.request,
window: this.#window
})

Check warning on line 103 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (18)

Insert `↹`

Check warning on line 103 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (20)

Insert `↹`

Check warning on line 103 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (22)

Insert `↹`
: undefined;
if (typeof beforeRequestResponse === 'object') {
return beforeRequestResponse;
}
FetchRequestValidationUtility.validateSchema(this.request);

if (this.request.signal.aborted) {
Expand Down
16 changes: 0 additions & 16 deletions packages/happy-dom/src/fetch/types/IAsyncRequestInterceptor.ts

This file was deleted.

60 changes: 60 additions & 0 deletions packages/happy-dom/src/fetch/types/IFetchInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Request from '../Request.js';
import BrowserWindow from '../../window/BrowserWindow.js';
import Response from '../Response.js';
import ISyncResponse from './ISyncResponse.js';

export default interface IFetchInterceptor {
/**
* Hook dispatched before making an async request.
* It can be used for modifying the request, providing a response without making a request or for logging.
*
* @param context Contains the request and the window from where the request was made.
*
* @returns Promise that can resolve to a response to be used instead of sending out the response.
*/
beforeAsyncRequest?: (context: {
request: Request;
window: BrowserWindow;
}) => Promise<Response | void>;

/**
* Hook dispatched before making an sync request.
* It can be used for modifying the request, providing a response without making a request or for logging.
*
* @param context Contains the request and the window from where the request was made.
*
* @returns Promise that can resolve to a response to be used instead of sending out the response.
*/
beforeSyncRequest?: (context: {
request: Request;
window: BrowserWindow;
}) => ISyncResponse | void;

/**
* Hook dispatched after receiving an async response.
* It can be used for modifying or replacing the response and logging.
*
* @param context Contains the request, response and the window from where the request was made.
*
* @returns Promise that can resolve to a response to be used instead of sending out the response.
*/
afterAsyncResponse?: (context: {
request: Request;
response: Response;
window: BrowserWindow;
}) => Promise<Response | void>;

/**
* Hook dispatched after receiving a sync response.
* It can be used for modifying or replacing the response and logging
*
* @param context Contains the request, response and the window from where the request was made.
*
* @returns Promise that can resolve to a response to be used instead of sending out the response.
*/
afterSyncResponse?: (context: {
request: Request;
response: Response;
window: BrowserWindow;
}) => Promise<Response | void>;
}
4 changes: 2 additions & 2 deletions packages/happy-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import AbortSignal from './fetch/AbortSignal.js';
import Headers from './fetch/Headers.js';
import Request from './fetch/Request.js';
import Response from './fetch/Response.js';
import IAsyncRequestInterceptor from './fetch/types/IAsyncRequestInterceptor.js';
import IFetchInterceptor from './fetch/types/IFetchInterceptor.js';
import Blob from './file/Blob.js';
import File from './file/File.js';
import FileReader from './file/FileReader.js';
Expand Down Expand Up @@ -207,7 +207,7 @@ import type ITouchEventInit from './event/events/ITouchEventInit.js';
import type IWheelEventInit from './event/events/IWheelEventInit.js';

export type {
IAsyncRequestInterceptor,
IFetchInterceptor,
IAnimationEventInit,
IBrowser,
IBrowserContext,
Expand Down
47 changes: 9 additions & 38 deletions packages/happy-dom/test/fetch/Fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1364,18 +1364,15 @@ describe('Fetch', () => {
);
});

it('Uses intercepted response when beforeSend returns a Response', async () => {
it('Should use intercepted response and not send the request when beforeAsyncRequest returns a Response', async () => {
const originURL = 'https://localhost:8080/';
const responseText = 'some text';
const window = new Window({
url: originURL,
settings: {
fetch: {
intercept: {
asyncFetch: {
async beforeSend(_request, window) {
return new window.Response('intercepted text');
}
interceptor: {
async beforeAsyncRequest({ window }) {
return new window.Response('intercepted text');
}
}
}
Expand All @@ -1385,31 +1382,7 @@ describe('Fetch', () => {

mockModule('https', {
request: () => {
return {
end: () => {},
on: (event: string, callback: (response: HTTP.IncomingMessage) => void) => {
if (event === 'response') {
async function* generate(): AsyncGenerator<string> {
yield responseText;
}

const response = <HTTP.IncomingMessage>Stream.Readable.from(generate());

response.statusCode = 200;
response.statusMessage = 'OK';
response.headers = {};
response.rawHeaders = [
'content-type',
'text/html',
'content-length',
String(responseText.length)
];

callback(response);
}
},
setTimeout: () => {}
};
fail('No request should be made when beforeAsyncRequest returns a Response');
}
});

Expand All @@ -1418,18 +1391,16 @@ describe('Fetch', () => {
expect(await response.text()).toBe('intercepted text');
});

it('Makes a normal request when before does not return a Response', async () => {
it('Should make a normal request when before does not return a Response', async () => {
const originURL = 'https://localhost:8080/';
const responseText = 'some text';
const window = new Window({
url: originURL,
settings: {
fetch: {
intercept: {
asyncFetch: {
async beforeSend() {
return undefined;
}
interceptor: {
async beforeAsyncRequest() {
return undefined;
}
}
}
Expand Down
Loading

0 comments on commit ea4f90f

Please sign in to comment.