@loadmill/executer 0.1.35 → 0.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sequence.ts CHANGED
@@ -10,7 +10,6 @@ import clamp from 'lodash/clamp';
10
10
  import find from 'lodash/find';
11
11
  import map from 'lodash/map';
12
12
  import filter from 'lodash/filter';
13
- import forEach from 'lodash/forEach';
14
13
 
15
14
  import { ResolvedRequest } from './request-sequence-result';
16
15
  import { Asserter } from './asserter';
@@ -52,8 +51,6 @@ const {
52
51
  DEFAULT_REQUEST_DELAY,
53
52
  } = confDefaults;
54
53
  const {
55
- MAX_API_REQUEST_BODY_LENGTH,
56
- MAX_LOAD_REQUEST_BODY_LENGTH,
57
54
  MAX_RESPONSE_BYTES,
58
55
  MAX_RESPONSE_COLLECT,
59
56
  MIN_REQUEST_DELAY,
@@ -71,10 +68,15 @@ import {
71
68
  ALLOWED_RESPONSE_STATUSES,
72
69
  LoopConf,
73
70
  DEFAULT_REQUEST_TIMEOUT,
74
- CachePenetrationModes
71
+ CachePenetrationModes,
72
+ RequestPostData
75
73
  } from '@loadmill/core/dist/request';
76
74
 
77
75
  import { testRunEventsEmitter } from './test-run-event-emitter';
76
+ import { WSRequest, WSRequestArguments, WSSequenceHandler } from './ws';
77
+ import { RequestFailuresError } from './errors';
78
+ import { getMaxRequestBodySize } from './utils';
79
+ import { WSExtractionData } from '@loadmill/core/dist/parameters/extractors/ws-extractor';
78
80
 
79
81
  export const reqIdParamName = 'loadmill-request-id';
80
82
 
@@ -127,6 +129,7 @@ class SequenceExecutor {
127
129
  resolvedRequests: ResolvedRequest[] = [];
128
130
  keepaliveHTTPSAgent;
129
131
  keepaliveHTTPAgent;
132
+ wsHandler: WSSequenceHandler;
130
133
 
131
134
  constructor(
132
135
  private httpAgent,
@@ -143,6 +146,7 @@ class SequenceExecutor {
143
146
  freeSocketTimeout: SOCKET_TIMEOUT / 2
144
147
  });
145
148
  this.postScriptRunner = new PostScriptRunner();
149
+ this.wsHandler = new WSSequenceHandler();
146
150
  }
147
151
 
148
152
  startAndPass(requests: LoadmillRequest[]) {
@@ -213,9 +217,11 @@ class SequenceExecutor {
213
217
  log.info('Unexpected error info:', unexpectedError);
214
218
  }
215
219
 
220
+ this.wsHandler.closeAllConnections();
216
221
  throw error;
217
222
  }
218
223
  }
224
+ this.wsHandler.closeAllConnections();
219
225
  }
220
226
 
221
227
  shouldStop(request: LoadmillRequest) {
@@ -249,7 +255,11 @@ class SequenceExecutor {
249
255
  reqIndex: number,
250
256
  requests: LoadmillRequest[]
251
257
  ) {
252
- const { skipBefore } = request;
258
+ const { skipBefore, disabled } = request;
259
+
260
+ if (disabled) {
261
+ return reqIndex + 1;
262
+ }
253
263
 
254
264
  if (skipBefore) {
255
265
  const { goTo, negate, condition } = skipBefore;
@@ -305,12 +315,14 @@ class SequenceExecutor {
305
315
  let failedAssertionsHistogram, resTime;
306
316
  let loopIteration = 0;
307
317
  const maxIterations = getLoopIterations(requestConf.loop);
318
+ this.wsHandler.clearMessages();
308
319
 
309
320
  while (loopIteration < maxIterations) {
310
321
  loopIteration++;
311
322
  const request = this.prepareRequest(requestConf, index);
312
323
 
313
324
  const res = await this.sendRequest(request, index);
325
+
314
326
  // Setting now to avoid possible user overwrite:
315
327
  resTime = Number(this.parameters.__responseTime);
316
328
 
@@ -321,7 +333,9 @@ class SequenceExecutor {
321
333
  );
322
334
  }
323
335
 
324
- failedAssertionsHistogram = this.processSuccessfulResponse(
336
+ res.wsExtractionData = { messages: this.wsHandler.messages, timeLimit: timeout - resTime } as WSExtractionData;
337
+
338
+ failedAssertionsHistogram = await this.processSuccessfulResponse(
325
339
  index,
326
340
  requestConf,
327
341
  res
@@ -497,8 +511,18 @@ class SequenceExecutor {
497
511
  }
498
512
 
499
513
  prepareRequest(requestConf: LoadmillRequest, reqIndex: number) {
514
+ if (this.isWSRequest(requestConf) && !envUtils.isBrowser()) {
515
+ return this.prepareWSRequest(requestConf, reqIndex);
516
+ }
517
+ return this.prepareHttpRequest(requestConf, reqIndex);
518
+ }
519
+ private isWSRequest({ url }: LoadmillRequest) {
520
+ const resolvedUrl = this.resolve(url, (e) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
521
+ return resolvedUrl.startsWith('ws://') || resolvedUrl.startsWith('wss://');
522
+ }
523
+ private prepareHttpRequest(requestConf: LoadmillRequest, reqIndex: number) {
500
524
  const urlObj = new URI(
501
- resolveUrl(requestConf.url, this.parameters, (err) =>
525
+ resolveUrl(requestConf.url, this.parameters, (err: Error) =>
502
526
  setParameterErrorHistogram(err, 'Failed to compute URL - ')
503
527
  )
504
528
  );
@@ -516,7 +540,7 @@ class SequenceExecutor {
516
540
 
517
541
  const url = urlObj.toString();
518
542
  this.resolvedRequests[reqIndex].url = url;
519
-
543
+
520
544
  this.validateDomain(uriUtils.getDomain(url));
521
545
 
522
546
  const method = requestConf.method.toLowerCase();
@@ -633,7 +657,7 @@ class SequenceExecutor {
633
657
  // This makes superagent populate res.text regardless of the response's content type:
634
658
  request.buffer();
635
659
 
636
- // Otherwise we expose superagent and its version -
660
+ // Otherwise we expose superagent and its version -
637
661
  // better have something more general (in case the user didnt set one):
638
662
  const uaHeader = request.get('User-Agent');
639
663
  if (!uaHeader || uaHeader.startsWith('node-superagent')) {
@@ -650,6 +674,66 @@ class SequenceExecutor {
650
674
  return request;
651
675
  }
652
676
 
677
+ prepareWSRequest(requestConf: LoadmillRequest, reqIndex: number) {
678
+ const wsRequestArgs = this.resolveAndSetWSReqData(requestConf, reqIndex);
679
+
680
+ return new WSRequest(
681
+ wsRequestArgs,
682
+ this.wsHandler,
683
+ (e: Error) => this.setSingleFailure(reqIndex, 'Websocket error ' + e.message),
684
+ );
685
+ }
686
+
687
+ resolveAndSetWSReqData = (
688
+ { url, headers, postData, timeout = DEFAULT_REQUEST_TIMEOUT, expectedStatus = 'SUCCESS' }: LoadmillRequest,
689
+ reqIndex: number
690
+ ): WSRequestArguments => {
691
+ const preparedUrl = this.prepareWsUrl(url, reqIndex);
692
+ const preparedHeaders = this.prepareWsHeaders(headers, reqIndex);
693
+ const preparedMessage = this.prepareWsMessage(postData, reqIndex);
694
+ return {
695
+ expectedStatus,
696
+ headers: preparedHeaders,
697
+ message: preparedMessage,
698
+ timeout,
699
+ url: preparedUrl,
700
+ };
701
+ };
702
+
703
+ prepareWsUrl = (url: string, reqIndex: number) => {
704
+ const resolvedUrl = this.resolve(url, (e: Error) => setParameterErrorHistogram(e, 'Failed to compute URL - '));
705
+ this.resolvedRequests[reqIndex].url = resolvedUrl;
706
+ return resolvedUrl;
707
+ };
708
+
709
+ prepareWsHeaders = (headers: LoadmillHeaders[] | undefined, reqIndex: number) => {
710
+ const resolvedHeadersObj: LoadmillHeaders = {};
711
+ const resolvedHeaders: LoadmillHeaders[] = [];
712
+ if (headers && !isEmpty(headers)) {
713
+ resolvedHeaders.push(...this.resolveHeaders(headers));
714
+ resolvedHeaders.forEach(({ name, value }) => resolvedHeadersObj[name] = value);
715
+ }
716
+ this.resolvedRequests[reqIndex].headers = resolvedHeaders;
717
+ return resolvedHeadersObj;
718
+ };
719
+
720
+ prepareWsMessage = (postData: RequestPostData | undefined, reqIndex: number) => {
721
+ let resolvedMessage: string = '';
722
+ if (postData) {
723
+ resolvedMessage = this.resolve(postData.text, (err: Error) =>
724
+ setParameterErrorHistogram(err, 'Failed to compute Websocket message - ')
725
+ );
726
+ if (resolvedMessage && resolvedMessage.length > getMaxRequestBodySize()) {
727
+ throw new RequestFailuresError('Websocket message size is too large');
728
+ }
729
+ this.resolvedRequests[reqIndex].postData = {
730
+ mimeType: postData.mimeType,
731
+ text: resolvedMessage,
732
+ };
733
+ }
734
+ return resolvedMessage;
735
+ };
736
+
653
737
  checkProgressEvent = (
654
738
  requestIndex: number,
655
739
  request,
@@ -664,7 +748,7 @@ class SequenceExecutor {
664
748
  };
665
749
 
666
750
  private validateDomain(domain: string) {
667
- if (isEmpty(domain)){
751
+ if (isEmpty(domain)) {
668
752
  const message = 'HTTP request domain name is empty';
669
753
  throw new RequestFailuresError(message, { [message]: 1 });
670
754
  }
@@ -711,9 +795,9 @@ class SequenceExecutor {
711
795
  )
712
796
  );
713
797
 
714
- processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
798
+ async processSuccessfulResponse(reqIndex, requestConf: LoadmillRequest, res) {
715
799
  // modifies parameters:
716
- this.handleExtractions(requestConf, res);
800
+ await this.handleExtractions(requestConf, res);
717
801
 
718
802
  if (!envUtils.isBrowser()) {
719
803
  this.handlePostScript(requestConf, res);
@@ -741,33 +825,33 @@ class SequenceExecutor {
741
825
  );
742
826
  }
743
827
 
744
- handleExtractions(requestConf: LoadmillRequest, res) {
745
- requestConf.extract?.forEach((extractions: Extractions) =>
746
- this.extractInScope(res, extractions)
747
- );
828
+ async handleExtractions(requestConf: LoadmillRequest, res) {
829
+ for (const extractions of (requestConf.extract || [])) {
830
+ await this.extractInScope(res, extractions);
831
+ }
748
832
  }
749
833
 
750
- extractInScope(res, extractions: Extractions) {
834
+ async extractInScope(res, extractions: Extractions) {
751
835
  const contextParameters = Object.assign({}, this.parameters);
752
- const extractionCombiner = new ExtractionCombiner(contextParameters, res);
836
+ const extractionCombiner = new ExtractionCombiner(contextParameters, res, res.wsExtractionData);
753
837
 
754
- forEach(extractions, (extraction: Extraction, name: string) =>
755
- this.extract(name, extraction, extractionCombiner)
756
- );
838
+ for (const [name, extraction] of Object.entries(extractions)) {
839
+ await this.extract(name, extraction, extractionCombiner);
840
+ }
757
841
  }
758
842
 
759
- extract(
843
+ async extract(
760
844
  parameterName: string,
761
845
  extraction: Extraction,
762
846
  extractionCombiner: ExtractionCombiner
763
847
  ) {
764
848
  log.trace('Parameter extraction start: ', { parameterName, extraction });
765
849
 
766
- const combinedExtractor = extractionCombiner.combine(extraction);
850
+ const combinedExtractor = await extractionCombiner.combine(extraction);
767
851
  let result;
768
852
 
769
853
  try {
770
- result = combinedExtractor();
854
+ result = await combinedExtractor();
771
855
  } catch (error) {
772
856
  const genericMessage = `Failed to extract value for parameter "${parameterName}"`;
773
857
  log.debug(genericMessage, error);
@@ -889,9 +973,6 @@ function isSimpleRequest(headers) {
889
973
  );
890
974
  }
891
975
 
892
- const getMaxRequestBodySize = () =>
893
- envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
894
-
895
976
  const getLoopIterations = (LoopConf?: LoopConf) => {
896
977
  const declared = (LoopConf && LoopConf.iterations) || 1;
897
978
  return Math.min(MAX_REQUEST_LOOPS_ITERATIONS, declared);
@@ -909,7 +990,7 @@ const extendResponseHeaders = (headers, redirectHeaders) => {
909
990
 
910
991
  const isExpectedStatus = ({ expectedStatus, url }, status: number) => {
911
992
  if (expectedStatus === ALLOWED_RESPONSE_STATUSES.SUCCESS) {
912
- return 200 <= status && status < 400;
993
+ return (200 <= status && status < 400) || status === 101;
913
994
  }
914
995
  else if (expectedStatus === ALLOWED_RESPONSE_STATUSES.ERROR) {
915
996
  log.debug('user asked to fail this request', url);
@@ -945,12 +1026,3 @@ const setTCPReuse = (request, agent, sslAgent) => {
945
1026
  };
946
1027
 
947
1028
  };
948
-
949
- class RequestFailuresError extends Error {
950
- constructor(message: string, public histogram: Histogram = { [message]: 1 }) {
951
- super(message);
952
-
953
- // Workaround suggested in: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
954
- Object.setPrototypeOf(this, RequestFailuresError.prototype);
955
- }
956
- }
package/src/utils.ts ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ MAX_API_REQUEST_BODY_LENGTH,
3
+ MAX_LOAD_REQUEST_BODY_LENGTH
4
+ } from '@loadmill/core/dist/conf/extrema';
5
+ import * as envUtils from '@loadmill/universal/dist/env-utils';
6
+
7
+ export const getMaxRequestBodySize = () =>
8
+ envUtils.isBrowser() ? MAX_LOAD_REQUEST_BODY_LENGTH : MAX_API_REQUEST_BODY_LENGTH;
package/src/ws.ts ADDED
@@ -0,0 +1,268 @@
1
+ import WebSocket from 'ws'; // todo should be picked with other option (socketio or non in browser)
2
+ import { delay } from '@loadmill/universal/dist/promise-utils';
3
+ import log from '@loadmill/universal/dist/log';
4
+ import { HttpResponseStatus, LoadmillHeaders } from '@loadmill/core/dist/request';
5
+ import { RequestFailuresError } from './errors';
6
+ import { ClientRequest, IncomingMessage, IncomingHttpHeaders } from 'http';
7
+ import { Socket } from 'net';
8
+
9
+ declare class WS extends WebSocket {
10
+ _req: WSClientRequest;
11
+ }
12
+ declare class WSClientRequest extends ClientRequest {
13
+ socket: Socket & { parser: ParserIncomingMessage };
14
+ }
15
+ declare class ParserIncomingMessage {
16
+ incoming: IncomingMessage;
17
+ }
18
+
19
+
20
+ enum WSState {
21
+ CONNECTING,
22
+ OPEN,
23
+ CLOSING,
24
+ CLOSED,
25
+ }
26
+
27
+ const SWITCHING_PROTOCOLS = 'Switching Protocols';
28
+ const WS_CONNECTION_TIMEOUT_MS = 10000;
29
+
30
+ export class WSRequest {
31
+ private hasErrorOccured?: boolean; // need this otherwise we have race condition between verifyConnectedAndOpen and onError
32
+ private ws: WebSocket;
33
+ public url: string; // need this for isExpectedStatus function
34
+ public expectedStatus: HttpResponseStatus; // need this for isExpectedStatus function
35
+
36
+ constructor(
37
+ private readonly wsRequestArgs: WSRequestArguments,
38
+ private readonly wsHandler: WSSequenceHandler,
39
+ private readonly onError: (e: Error) => void,
40
+ ) {
41
+ this.url = wsRequestArgs.url;
42
+ this.expectedStatus = wsRequestArgs.expectedStatus;
43
+ }
44
+
45
+ /**
46
+ * This function is executed when we are ready to send the ws request
47
+ * @param cb This callback is being executed after we successfully connected to ws and sent a ws message if there was any
48
+ */
49
+ async ok(cb: (response: WSResponse) => boolean) {
50
+ const response = {
51
+ status: 101,
52
+ res: {
53
+ statusMessage: SWITCHING_PROTOCOLS,
54
+ },
55
+ header: {},
56
+ req: {},
57
+ };
58
+
59
+ const existingWS = this.wsHandler.getConnection(this.wsRequestArgs.url);
60
+
61
+ log.debug(`Connection state ${getConnectionState(existingWS)}`);
62
+ if (!existingWS || (existingWS && existingWS.readyState !== WSState.OPEN)) {
63
+ await this.addConnection(response);
64
+ } else {
65
+ this.ws = existingWS;
66
+ log.debug('Reusing existing ws connection', this.ws.url);
67
+ }
68
+
69
+ try {
70
+ this.sendMessage();
71
+ } catch (e) {
72
+ log.error('Failed to send a ws message', e);
73
+ }
74
+ cb(response);
75
+ return { };
76
+ }
77
+
78
+ private async addConnection(response: WSResponse) {
79
+ this.ws = new WebSocket(this.wsRequestArgs.url, undefined, { headers: this.wsRequestArgs.headers });
80
+ this.addEventListeners(response);
81
+ !this.hasErrorOccured && await this.verifyOpen();
82
+ this.wsHandler.storeConnection(this.wsRequestArgs.url, this.ws);
83
+ // todo add some debug details for the user to know we created a new connection in this request
84
+ }
85
+
86
+ private async addEventListeners(response: WSResponse) {
87
+ this.addOnErrorListener(response);
88
+ this.addOnUpgradeListener(response);
89
+ this.addOnOpenListener();
90
+ this.addOnMessageListener();
91
+ }
92
+
93
+ private addOnErrorListener(response: WSResponse) {
94
+ this.ws.addEventListener('error', event => {
95
+ log.debug('inside addEventListener error');
96
+ try {
97
+ const target = event.target as WS;
98
+ this.updateResponseOnError(target, response);
99
+ } catch (e) {
100
+ log.error('Got an error inside addEventListener error', e);
101
+ }
102
+ });
103
+
104
+ this.ws.on('error', (e) => {
105
+ try {
106
+ log.debug('inside on error', e);
107
+ this.onError(e);
108
+ this.hasErrorOccured = true;
109
+ } catch (e) {
110
+ log.warn('Failed to handle a ws error', e);
111
+ }
112
+ try {
113
+ log.debug('closing ws');
114
+ this.ws.close();
115
+ } catch (e) {
116
+ log.warn('Failed to close a ws connection', e);
117
+ }
118
+ });
119
+ }
120
+
121
+ private updateResponseOnError(ws: WS, response: WSResponse) {
122
+ const statusCode = ws._req.socket.parser.incoming.statusCode;
123
+ const statusMessage = ws._req.socket.parser.incoming.statusMessage;
124
+ const headers = ws._req.socket.parser.incoming.headers;
125
+ log.debug('addEventListener error statusCode', statusCode);
126
+ log.debug('addEventListener error statusMessage', statusMessage);
127
+ log.debug('addEventListener error headers', headers);
128
+ response.status = statusCode;
129
+ response.res.statusMessage = statusMessage;
130
+ response.header = headers;
131
+ }
132
+
133
+ private addOnUpgradeListener(response: WSResponse) {
134
+ this.ws.on('upgrade', (event) => {
135
+ try {
136
+ const { statusCode, statusMessage, headers } = event;
137
+ log.debug('inside upgrade event', { statusCode, statusMessage, headers });
138
+ response.status = statusCode || 101;
139
+ response.res.statusMessage = statusMessage || SWITCHING_PROTOCOLS;
140
+ response.header = headers as LoadmillHeaders;
141
+ } catch (e) {
142
+ log.debug('upgrade event err', e);
143
+ }
144
+ });
145
+ }
146
+
147
+ private addOnOpenListener() {
148
+ this.ws.on('open', () => {
149
+ log.debug('inside on open, currently doing nothing here.');
150
+ });
151
+ }
152
+
153
+ private addOnMessageListener() {
154
+ this.ws.on('message', (message: string) => {
155
+ try {
156
+ log.debug('got incoming message', message);
157
+ this.wsHandler.addMessage(message);
158
+ } catch (e) {
159
+ log.debug('error getting message', e);
160
+ }
161
+ });
162
+ }
163
+
164
+ async verifyOpen() {
165
+ const readyState = await this.waitForConnectedState(this.wsRequestArgs.timeout);
166
+ log.debug(`Connection state ${WSState[readyState]}`);
167
+ if (readyState !== WebSocket.OPEN && !this.hasErrorOccured) {
168
+ log.error('Could not connect to WebSocket address', { connectionState: WSState[readyState], url: this.ws.url });
169
+ throw new RequestFailuresError('Could not connect to WebSocket address');
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Waiting for WS connection to go out of CONNECTING state
175
+ * @param socket The ws connection object
176
+ * @param timeout The timeout in milliseconds for the waiting procedure
177
+ * @returns WSState
178
+ */
179
+ waitForConnectedState = async (timeout: number = WS_CONNECTION_TIMEOUT_MS): Promise<WSState> => {
180
+ const WS_CONNECTION_INTERVAL_MS = 100;
181
+ if (this.ws.readyState !== WebSocket.CONNECTING) {
182
+ return this.ws.readyState;
183
+ }
184
+ else {
185
+ const maxIterations = timeout / WS_CONNECTION_INTERVAL_MS;
186
+ let i = 0;
187
+ while (this.ws.readyState === WebSocket.CONNECTING && i < maxIterations) {
188
+ await delay(WS_CONNECTION_INTERVAL_MS);
189
+ i++;
190
+ }
191
+ return this.ws.readyState;
192
+ }
193
+ };
194
+
195
+ private sendMessage() {
196
+ log.debug('about to send message', { readyState: WSState[this.ws.readyState] } );
197
+ if (this.ws.readyState === WSState.OPEN && this.wsRequestArgs.message) {
198
+ log.debug('sending message', this.wsRequestArgs.message);
199
+ this.ws.send(this.wsRequestArgs.message);
200
+ }
201
+ }
202
+
203
+ // need this because the SequenceExecutor expectes a request obj with on func prop
204
+ on(_eventName: string, _cb: Function) {
205
+ //do nothing
206
+ }
207
+ }
208
+
209
+ export class WSSequenceHandler {
210
+ private _connections: { [url: string]: WebSocket };
211
+ messages: string[];
212
+ constructor() {
213
+ this._connections = {};
214
+ this.clearMessages();
215
+ }
216
+
217
+ getConnection(url: string): WebSocket | undefined {
218
+ return this._connections[url];
219
+ }
220
+
221
+ storeConnection(url: string, wsConnection: WebSocket) {
222
+ this._connections[url] = wsConnection;
223
+ }
224
+
225
+ async closeAllConnections() {
226
+ try {
227
+ log.debug('Closing all ws connections');
228
+ for (const connection of Object.values(this._connections)) {
229
+ connection.close();
230
+ }
231
+ } catch (e) {
232
+ log.warn('Failed to close all connections', e, { connections: this._connections });
233
+ }
234
+ }
235
+
236
+ getMessages() {
237
+ return this.messages;
238
+ }
239
+
240
+ addMessage(message: string) {
241
+ this.messages.push(message);
242
+ }
243
+
244
+ clearMessages() {
245
+ this.messages = [];
246
+ }
247
+ }
248
+
249
+ function getConnectionState(existingWS?: WebSocket) {
250
+ return existingWS ? WSState[existingWS.readyState] : null;
251
+ }
252
+
253
+ export type WSRequestArguments = {
254
+ expectedStatus: HttpResponseStatus;
255
+ headers: LoadmillHeaders
256
+ message: string;
257
+ timeout: number;
258
+ url: string;
259
+ };
260
+
261
+ export type WSResponse = {
262
+ status?: number;
263
+ res: {
264
+ statusMessage?: string;
265
+ };
266
+ header: LoadmillHeaders | IncomingHttpHeaders;
267
+ req: {};
268
+ }