Skip to content

Commit 3a72370

Browse files
committed
feat: add doOriginalCall method
1 parent a48f657 commit 3a72370

File tree

6 files changed

+156
-34
lines changed

6 files changed

+156
-34
lines changed

src/common/utils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,14 @@ export function isObject(obj: unknown) {
7575
export function tryToParseObject(body: unknown) {
7676
const isObjLiked = typeof body === 'string' && body[0] === '{' && body[body.length-1] === '}';
7777
const isArrLiked = typeof body === 'string' && body[0] === '[' && body[body.length-1] === ']';
78-
if (isObjLiked || isArrLiked) {
79-
try {
80-
return JSON.parse(body);
81-
} catch(e) {
82-
return body;
83-
}
84-
} else {
78+
79+
if (!isObjLiked && !isArrLiked) {
80+
return body;
81+
}
82+
83+
try {
84+
return JSON.parse(body);
85+
} catch(e) {
8586
return body;
8687
}
8788
}

src/interceptor/base.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getQuery, tryToParseObject } from '../common/utils';
22
import MockItem from '../mocker/mock-item';
33
import Mocker from '../mocker/mocker';
4-
import { HttpVerb, MixedRequestInfo, RequestInfo } from '../types';
4+
import { HttpVerb, RequestInfo } from '../types';
55
import InterceptorFetch from './fetch';
66
import InterceptorNode from './node/http-and-https';
77
import InterceptorWxRequest from './wx-request';
@@ -67,17 +67,17 @@ export default class BaseInterceptor {
6767
return mockItem;
6868
}
6969

70-
public getRequestInfo(mixedRequestInfo: MixedRequestInfo) : RequestInfo {
70+
public getRequestInfo(requestInfo: RequestInfo) : RequestInfo {
7171
const info: RequestInfo = {
72-
url: mixedRequestInfo.url,
73-
method: mixedRequestInfo.method || 'GET',
74-
query: getQuery(mixedRequestInfo.url),
72+
url: requestInfo.url,
73+
method: requestInfo.method || 'GET',
74+
query: getQuery(requestInfo.url),
7575
};
76-
if (mixedRequestInfo.headers || mixedRequestInfo.header) {
77-
info.headers = mixedRequestInfo.headers || mixedRequestInfo.header;
76+
if (requestInfo.headers || requestInfo.header) {
77+
info.headers = requestInfo.headers || requestInfo.header;
7878
}
79-
if (mixedRequestInfo.body !== undefined) {
80-
info.body = tryToParseObject(mixedRequestInfo.body);
79+
if (requestInfo.body !== undefined) {
80+
info.body = tryToParseObject(requestInfo.body);
8181
}
8282
return info;
8383
}

src/interceptor/fetch.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { sleep, tryToParseJson } from '../common/utils';
44
import { HTTPStatusCodes } from '../config';
55
import MockItem from '../mocker/mock-item';
66
import Mocker from '../mocker/mocker';
7-
import { FetchRequest, FetchResponse, HttpVerb, RemoteResponse, RequestInfo } from '../types';
7+
import { FetchRequest, FetchResponse, HttpVerb, OriginalResponse, RemoteResponse, RequestInfo } from '../types';
88
import { AnyObject } from './../types';
99
import Base from './base';
1010

@@ -49,13 +49,20 @@ export default class FetchInterceptor extends Base{
4949
const requestUrl = me.getFullRequestUrl(url, method);
5050

5151
return new Promise((resolve, reject) => {
52-
const mockItem:MockItem | null = me.matchMockRequest(requestUrl, method);
52+
const mockItem:MockItem | null = me.matchMockRequest(requestUrl, method);
53+
5354
if (!mockItem) {
5455
me.fetch(requestUrl, params).then(resolve).catch(reject);
5556
return;
5657
}
5758

5859
const requestInfo = me.getRequestInfo({ ...params, url: requestUrl, method: method as HttpVerb });
60+
requestInfo.doOriginalCall = async (): Promise<OriginalResponse> => {
61+
const res = await me.getOriginalResponse(requestUrl, params);
62+
requestInfo.doOriginalCall = undefined;
63+
return res;
64+
};
65+
5966
const remoteInfo = mockItem?.getRemoteInfo(requestUrl);
6067
if (remoteInfo) {
6168
params.method = remoteInfo.method || method;
@@ -127,6 +134,29 @@ export default class FetchInterceptor extends Base{
127134
});
128135
}
129136

137+
private async getOriginalResponse(requestUrl: string, params: FetchRequest | AnyObject): Promise<OriginalResponse> {
138+
let status = null;
139+
const headers: Record<string, string | string[]> = {};
140+
let responseText = null;
141+
let responseJson = null;
142+
let responseBuffer = null;
143+
let responseBlob = null;
144+
try {
145+
const res = await this.fetch(requestUrl, params);
146+
status = res.status;
147+
if (typeof Headers === 'function' && res.headers instanceof Headers) {
148+
res.headers.forEach((val: string, key: string) => (headers[key.toLocaleLowerCase()] = val));
149+
}
150+
responseBlob = await res.blob();
151+
responseText = await responseBlob.text();
152+
responseBuffer = await responseBlob.arrayBuffer();
153+
responseJson = tryToParseJson(responseText);
154+
return { status, headers, responseText, responseJson, responseBuffer, responseBlob, error: null };
155+
} catch(err) {
156+
return { status, headers, responseText, responseJson, responseBuffer, responseBlob, error: err as Error };
157+
}
158+
}
159+
130160
/**
131161
* Make mock request.
132162
* @param {MockItem} mockItem

src/interceptor/xml-http-request.ts

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { HTTPStatusCodes } from '../config';
44
import MockItem from '../mocker/mock-item';
55
import Mocker from '../mocker/mocker';
66
import { HttpVerb, RemoteResponse, XMLHttpRequestInstance } from '../types';
7+
import { OriginalResponse } from './../types';
78
import Base from './base';
89

910
export default class XMLHttpRequestInterceptor extends Base {
@@ -74,6 +75,12 @@ export default class XMLHttpRequestInterceptor extends Base {
7475
this.mockResponse = new NotResolved();
7576
this.requestInfo = me.getRequestInfo({ url: requestUrl, method, });
7677
this.requestArgs = [method, requestUrl, async, user, password];
78+
79+
this.requestInfo.doOriginalCall = async (): Promise<OriginalResponse> => {
80+
const res = await me.getOriginalResponse(this);
81+
this.requestInfo.doOriginalCall = undefined;
82+
return res;
83+
};
7784
return;
7885
}
7986
}
@@ -128,17 +135,20 @@ export default class XMLHttpRequestInterceptor extends Base {
128135
* @param {Record<string, string>} remoteInfo
129136
*/
130137
private sendRemoteResult(xhr: XMLHttpRequestInstance, mockItem: MockItem, remoteInfo: Record<string, string>) {
131-
const [ method, , async, user, password ] = xhr.requestArgs;
138+
const [ method, async, user, password ] = xhr.requestArgs;
132139

133140
const newXhr = new XMLHttpRequest();
141+
newXhr.responseType = xhr.responseType;
142+
newXhr.timeout = xhr.timeout;
143+
134144
Object.assign(newXhr, { isMockRequest: false, bypassMock: true });
135145
newXhr.onreadystatechange = () => {
136146
if (newXhr.readyState === 4) {
137147
const remoteResponse: RemoteResponse = {
138148
status: newXhr.status,
139149
headers: newXhr.getAllResponseHeaders().split('\r\n').reduce((res: Record<string, string>, item: string) => {
140150
const [key, val] = item.split(':');
141-
if (key) {
151+
if (key && val) {
142152
res[key.toLowerCase()] = val.trim();
143153
}
144154
return res;
@@ -164,11 +174,83 @@ export default class XMLHttpRequestInterceptor extends Base {
164174
return xhr;
165175
}
166176

177+
private getOriginalResponse(xhr: XMLHttpRequestInstance): Promise<OriginalResponse> {
178+
const [ method, requestUrl, async, user, password ] = xhr.requestArgs;
179+
const { requestInfo } = xhr;
180+
181+
return new Promise(resolve => {
182+
const newXhr = new XMLHttpRequest();
183+
newXhr.responseType = xhr.responseType;
184+
newXhr.timeout = xhr.timeout;
185+
186+
Object.assign(newXhr, { isMockRequest: false, bypassMock: true });
187+
let status: OriginalResponse['status'] = null;
188+
let headers: OriginalResponse['headers'] = {};
189+
let responseText: OriginalResponse['responseText'] = null;
190+
let responseJson: OriginalResponse['responseJson'] = null;
191+
let responseBuffer: OriginalResponse['responseBuffer'] = null;
192+
let responseBlob: OriginalResponse['responseBlob'] = null;
193+
194+
newXhr.onreadystatechange = function handleLoad() {
195+
if (newXhr.readyState === 4) {
196+
const responseType = newXhr.responseType;
197+
status = newXhr.status;
198+
headers = newXhr.getAllResponseHeaders()
199+
.split('\r\n')
200+
.reduce((res: Record<string, string>, item: string) => {
201+
const [key, val] = item.split(':');
202+
if (key && val) {
203+
res[key.toLowerCase()] = val.trim();
204+
}
205+
return res;
206+
}, {} as Record<string, string>);
207+
208+
responseText = !responseType || responseType === 'text' || responseType === 'json'
209+
? newXhr.responseText
210+
: (typeof newXhr.response === 'string' ? typeof newXhr.response : null);
211+
212+
responseJson = tryToParseJson(responseText as string);
213+
responseBuffer = (typeof ArrayBuffer === 'function') && (newXhr.response instanceof ArrayBuffer)
214+
? newXhr.response
215+
: null;
216+
responseBlob = (typeof Blob === 'function') && (newXhr.response instanceof Blob)
217+
? newXhr.response
218+
: null;
219+
220+
resolve({ status, headers, responseText, responseJson, responseBuffer, responseBlob, error: null});
221+
}
222+
};
223+
newXhr.open(method as string, requestUrl as string, async as boolean, user as string, password as string);
224+
newXhr.ontimeout = function handleTimeout() {
225+
const error = new Error('timeout exceeded');
226+
resolve({ status, headers, responseText, responseJson, responseBuffer, responseBlob, error });
227+
};
228+
229+
// Real errors are hidden from us by the browser
230+
// onerror should only fire if it's a network error
231+
newXhr.onerror = function handleError() {
232+
const error = new Error('network error');
233+
resolve({ status, headers, responseText, responseJson, responseBuffer, responseBlob, error });
234+
};
235+
236+
// Handle browser request cancellation (as opposed to a manual cancellation)
237+
newXhr.onabort = function handleAbort() {
238+
const error = new Error('request aborted');
239+
resolve({ status, headers, responseText, responseJson, responseBuffer, responseBlob, error });
240+
};
241+
242+
243+
Object.entries(requestInfo.headers || {}).forEach(([key, val]: [string, string]) => {
244+
newXhr.setRequestHeader(key, val);
245+
});
246+
newXhr.send(requestInfo.rawBody as Document); // raw body
247+
});
248+
}
249+
167250
/**
168251
* Make mock request.
169252
* @param {XMLHttpRequestInstance} xhr
170-
* @param {MockItemInfo} mockItem
171-
* @param {RequestInfo} requestInfo
253+
* @param {RemoteResponse | null} remoteResponse
172254
*/
173255
private async doMockRequest(xhr: XMLHttpRequestInstance, remoteResponse: RemoteResponse | null = null) {
174256
let isBypassed = false;
@@ -185,8 +267,7 @@ export default class XMLHttpRequestInterceptor extends Base {
185267
/**
186268
* Make mock response.
187269
* @param {XMLHttpRequestInstance} xhr
188-
* @param {MockItemInfo} mockItem
189-
* @param {RequestInfo} requestInfo
270+
* @param {RemoteResponse | null} remoteResponse
190271
*/
191272
private async doMockResponse(xhr: XMLHttpRequestInstance, remoteResponse: RemoteResponse | null = null) {
192273
const { mockItem, requestInfo } = xhr;
@@ -326,7 +407,9 @@ export default class XMLHttpRequestInterceptor extends Base {
326407
return (header: string, value: string) => {
327408
if (this.isMockRequest) {
328409
this.requestInfo.headers = this.requestInfo.headers || {};
410+
this.requestInfo.header = this.requestInfo.header || {};
329411
this.requestInfo.headers[header] = value;
412+
this.requestInfo.header[header] = value;
330413
return;
331414
}
332415
return original.call(this, header, value);

src/mocker/mock-item.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export default class MockItem {
2020
public times: number;
2121
public key: string;
2222
public deProxy = false; // Use this option to make the mock use case run in the browser instead of nodejs.
23+
// eslint-disable-next-line @typescript-eslint/ban-types
24+
public doOriginalRequest: Function;
2325

2426
/**
2527
* Format specified mock item.

src/types.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,25 @@ export interface MockConfigData {
6565
export interface RequestInfo {
6666
url: string;
6767
method: HttpVerb;
68-
query: object; // url search query
68+
query?: object; // url search query
6969
headers?: object; // request header
70+
header?: object; // request header
7071
body?: unknown; // post body
7172
rawBody?: unknown; // post body
73+
doOriginalCall?: () => Promise<OriginalResponse>; // can only be called once
74+
}
75+
76+
export interface OriginalResponse {
77+
// if the original call is returned successfully, the following fields will be populated
78+
status: number | null; // http response status
79+
headers: Record<string, string | string[]>; // response headers
80+
responseText: string | null;
81+
responseJson: unknown | null;
82+
responseBuffer: ArrayBuffer | null;
83+
responseBlob: Blob | null;
84+
85+
// if the original call throws an exception, the error field will be populated
86+
error: Error | null;
7287
}
7388

7489
export interface RemoteResponse {
@@ -79,15 +94,6 @@ export interface RemoteResponse {
7994
responseJson: AnyObject;
8095
}
8196

82-
export interface MixedRequestInfo {
83-
url: string;
84-
method: HttpVerb;
85-
query?: object; // url search query
86-
headers?: object; // request header
87-
header?: object; // request header
88-
body?: unknown; // post body
89-
}
90-
9197
export interface XMLHttpRequestInstance extends XMLHttpRequest {
9298
bypassMock: boolean;
9399
isMockRequest: string;

0 commit comments

Comments
 (0)