Skip to content

Commit c0dbb3f

Browse files
committed
feat: enable insomnia.sendRequest
1 parent 2fab7e1 commit c0dbb3f

11 files changed

Lines changed: 787 additions & 12 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
raw file content

packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@playwright/test';
22

3-
import { loadFixture } from '../../playwright/paths';
3+
import { getFixturePath, loadFixture } from '../../playwright/paths';
44
import { test } from '../../playwright/test';;
55

66
test.describe('pre-request features tests', async () => {
@@ -30,7 +30,7 @@ test.describe('pre-request features tests', async () => {
3030
// but it is rewritten here
3131
insomnia.baseEnvironment.set('preDefinedValue', 'fromScript');
3232
// "customValue" is already defined in the folder environment.
33-
// folder version will override the following wone
33+
// folder version will override the following one
3434
insomnia.baseEnvironment.set('customValue', 'fromScript');
3535
`,
3636
body: `{
@@ -217,6 +217,111 @@ test.describe('pre-request features tests', async () => {
217217
expect(bodyJson.data).toEqual('rawContent');
218218
},
219219
},
220+
{
221+
name: 'sendRequest custom mode',
222+
preReqScript: `
223+
const rawReq = {
224+
url: 'http://127.0.0.1:4010/echo',
225+
method: 'POST',
226+
header: {
227+
'Content-Type': 'text/plain',
228+
},
229+
body: {
230+
mode: 'raw',
231+
raw: 'rawContent',
232+
},
233+
};
234+
const urlencodedReq = {
235+
url: 'http://127.0.0.1:4010/echo',
236+
method: 'POST',
237+
header: {
238+
'Content-Type': 'application/x-www-form-urlencoded',
239+
},
240+
body: {
241+
mode: 'urlencoded',
242+
urlencoded: [
243+
{ key: 'k1', value: 'v1' },
244+
{ key: 'k2', value: 'v2' },
245+
],
246+
},
247+
};
248+
const gqlReq = {
249+
url: 'http://127.0.0.1:4010/echo',
250+
method: 'POST',
251+
header: {
252+
'Content-Type': 'application/graphql',
253+
},
254+
body: {
255+
mode: 'graphql',
256+
graphql: {
257+
query: 'query',
258+
operationName: 'operation',
259+
variables: 'var',
260+
},
261+
},
262+
};
263+
const fileReq = {
264+
url: 'http://127.0.0.1:4010/echo',
265+
method: 'POST',
266+
header: {
267+
'Content-Type': 'application/octet-stream',
268+
},
269+
body: {
270+
mode: 'file',
271+
file: "${getFixturePath('files/rawfile.txt')}",
272+
},
273+
};
274+
const formdataReq = {
275+
url: 'http://127.0.0.1:4010/echo',
276+
method: 'POST',
277+
header: {
278+
// 'Content-Type': 'multipart/form-data',
279+
},
280+
body: {
281+
mode: 'formdata',
282+
formdata: [
283+
{ key: 'k1', type: 'text', value: 'v1' },
284+
{ key: 'k2', type: 'file', value: "${getFixturePath('files/rawfile.txt')}" },
285+
],
286+
},
287+
};
288+
const promises = [rawReq, urlencodedReq, gqlReq, fileReq, formdataReq].map(req => {
289+
return new Promise((resolve, reject) => {
290+
insomnia.sendRequest(
291+
req,
292+
(err, resp) => {
293+
if (err != null) {
294+
reject(err);
295+
} else {
296+
resolve(resp);
297+
}
298+
}
299+
);
300+
});
301+
});
302+
// send request
303+
const resps = await Promise.all(promises);
304+
// set envs
305+
insomnia.environment.set('rawBody', resps[0].body);
306+
insomnia.environment.set('urlencodedBody', resps[1].body);
307+
insomnia.environment.set('gqlBody', resps[2].body);
308+
insomnia.environment.set('fileBody', resps[3].body);
309+
insomnia.environment.set('formdataBody', resps[4].body);
310+
`,
311+
body: '{ "rawBody": {{ _.rawBody }}, "urlencodedBody": {{ _.urlencodedBody }}, "gqlBody": {{ _.gqlBody }}, "fileBody": {{ _.fileBody }}, "formdataBody": {{ _.formdataBody }} }',
312+
customVerify: (bodyJson: any) => {
313+
const reqBodyJsons = JSON.parse(bodyJson.data);
314+
expect(reqBodyJsons.rawBody.data).toEqual('rawContent');
315+
expect(reqBodyJsons.urlencodedBody.data).toEqual('k1=v1&k2=v2');
316+
expect(JSON.parse(reqBodyJsons.gqlBody.data)).toEqual({
317+
query: 'query',
318+
operationName: 'operation',
319+
variables: 'var',
320+
});
321+
expect(reqBodyJsons.fileBody.data).toEqual('raw file content');
322+
expect(reqBodyJsons.formdataBody.data).toEqual('--X-INSOMNIA-BOUNDARY\r\nContent-Disposition: form-data; name=\"k1\"\r\n\r\nv1\r\n--X-INSOMNIA-BOUNDARY\r\nContent-Disposition: form-data; name=\"k2\"; filename=\"rawfile.txt\"\r\nContent-Type: text/plain\r\n\r\nraw file content\r\n--X-INSOMNIA-BOUNDARY--\r\n');
323+
},
324+
},
220325
];
221326

222327
for (let i = 0; i < testCases.length; i++) {

packages/insomnia/src/global.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ declare global {
1111
bridge: {
1212
requireInterceptor: (module: string) => any;
1313
onmessage: (listener: (data: any, callback: (result: any) => void) => void) => void;
14+
cancelCurlRequest: (id: string) => void;
15+
curlRequest: (options: any) => Promise<any>;
16+
readCurlResponse: (options: { bodyPath: string; bodyCompression: Compression }) => Promise<{ body: string; error: string }>;
1417
};
1518
}
1619
}

packages/insomnia/src/hidden-window-preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const bridge: Window['bridge'] = {
2424

2525
throw Error(`no module is found for "${moduleName}"`);
2626
},
27+
28+
curlRequest: options => ipcRenderer.invoke('curlRequest', options),
29+
cancelCurlRequest: options => ipcRenderer.send('cancelCurlRequest', options),
30+
readCurlResponse: options => ipcRenderer.invoke('readCurlResponse', options),
2731
};
2832

2933
if (process.contextIsolated) {

packages/insomnia/src/main/network/curl.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CookieJar } from '../../models/cookie-jar';
1212
import { Environment } from '../../models/environment';
1313
import { RequestAuthentication, RequestHeader } from '../../models/request';
1414
import { Response } from '../../models/response';
15+
import { Compression, getBodyBuffer } from '../../models/response';
1516
import { addSetCookiesToToughCookieJar } from '../../network/set-cookie-util';
1617
import { urlMatchesCertHost } from '../../network/url-matches-cert-host';
1718
import { invariant } from '../../utils/invariant';
@@ -353,6 +354,7 @@ export interface CurlBridgeAPI {
353354
findMany: typeof findMany;
354355
};
355356
}
357+
356358
export const registerCurlHandlers = () => {
357359
ipcMain.handle('curl.open', openCurlConnection);
358360
ipcMain.on('curl.close', closeCurlConnection);
@@ -361,4 +363,20 @@ export const registerCurlHandlers = () => {
361363
ipcMain.handle('curl.event.findMany', (_, options: Parameters<typeof findMany>[0]) => findMany(options));
362364
};
363365

366+
ipcMain.handle('readCurlResponse', async (_, options: { bodyPath?: string; bodyCompression?: Compression }) => {
367+
const readFailureMsg = '[main/curlBridgeAPI] failed to read response body message';
368+
const bodyBufferOrErrMsg = getBodyBuffer(options, readFailureMsg);
369+
370+
if (!bodyBufferOrErrMsg) {
371+
return { body: '', error: readFailureMsg };
372+
} else if (typeof bodyBufferOrErrMsg === 'string') {
373+
if (bodyBufferOrErrMsg === readFailureMsg) {
374+
return { body: '', error: readFailureMsg };
375+
}
376+
return { body: '', error: `unknown error in loading response body: ${bodyBufferOrErrMsg}` };
377+
}
378+
379+
return { body: bodyBufferOrErrMsg.toString('utf8'), error: '' };
380+
});
381+
364382
electron.app.on('window-all-closed', closeAllCurlConnections);

packages/insomnia/src/models/response.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface ResponseHeader {
2323
value: string;
2424
}
2525

26-
type Compression = 'zip' | null | '__NEEDS_MIGRATION__' | undefined;
26+
export type Compression = 'zip' | null | '__NEEDS_MIGRATION__' | undefined;
2727

2828
export interface BaseResponse {
2929
environmentId: string | null;

packages/insomnia/src/network/cancellation.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { Request } from '../models/request';
33
import { RequestContext } from '../sdk/objects/interfaces';
44

55
const cancelRequestFunctionMap = new Map<string, () => void>();
6+
7+
export function setCancelRequestFunctionMap(id: string, cb: () => void) {
8+
return cancelRequestFunctionMap.set(id, cb);
9+
}
10+
11+
export function deleteCancelRequestFunctionMap(id: string) {
12+
return cancelRequestFunctionMap.delete(id);
13+
}
14+
615
export async function cancelRequestById(requestId: string) {
716
const cancel = cancelRequestFunctionMap.get(requestId);
817
if (cancel) {
@@ -65,7 +74,7 @@ export const cancellableCurlRequest = async (requestOptions: CurlRequestOptions)
6574
}
6675
};
6776

68-
const cancellablePromise = ({ signal, fn }: { signal: AbortSignal; fn: Promise<any> }) => {
77+
export const cancellablePromise = ({ signal, fn }: { signal: AbortSignal; fn: Promise<any> }) => {
6978
if (signal?.aborted) {
7079
return Promise.reject(new DOMException('Aborted', 'AbortError'));
7180
}

packages/insomnia/src/sdk/objects/insomnia.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import { Environment, Variables } from './environments';
44
import { RequestContext } from './interfaces';
55
import { unsupportedError } from './properties';
66
import { Request as ScriptRequest, RequestBodyOptions, RequestOptions } from './request';
7+
import { Response as ScriptResponse } from './response';
8+
import { HttpSendRequest } from './send-request';
79

810
export class InsomniaObject {
911
public environment: Environment;
1012
public collectionVariables: Environment;
1113
public baseEnvironment: Environment;
1214
public variables: Variables;
1315
public request: ScriptRequest;
16+
private httpRequestSender: HttpSendRequest;
1417

1518
// TODO: follows will be enabled after Insomnia supports them
1619
private _globals: Environment;
@@ -33,6 +36,26 @@ export class InsomniaObject {
3336
this._iterationData = rawObj.iterationData;
3437
this.variables = rawObj.variables;
3538
this.request = rawObj.request;
39+
40+
this.httpRequestSender = new HttpSendRequest({
41+
preferredHttpVersion: '',
42+
maxRedirects: 0,
43+
proxyEnabled: false,
44+
timeout: 0,
45+
validateSSL: false,
46+
followRedirects: false,
47+
maxTimelineDataSizeKB: 0,
48+
httpProxy: '',
49+
httpsProxy: '',
50+
noProxy: '',
51+
});
52+
}
53+
54+
sendRequest(
55+
request: string | ScriptRequest,
56+
cb: (error?: string, response?: ScriptResponse) => void
57+
) {
58+
return this.httpRequestSender.sendRequest(request, cb);
3659
}
3760

3861
// TODO: remove this after enabled globals

packages/insomnia/src/sdk/objects/request.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type RequestBodyMode = undefined | 'formdata' | 'urlencoded' | 'raw' | 'f
1414
export interface RequestBodyOptions {
1515
mode: RequestBodyMode;
1616
file?: string;
17-
formdata?: { key: string; value: string }[];
17+
formdata?: { key: string; value: string; type?: string }[];
1818
graphql?: { query: string; operationName: string; variables: object };
1919
raw?: string;
2020
urlencoded?: { key: string; value: string }[];
@@ -24,11 +24,13 @@ export interface RequestBodyOptions {
2424
export class FormParam extends Property {
2525
key: string;
2626
value: string;
27+
type?: string;
2728

28-
constructor(options: { key: string; value: string }) {
29+
constructor(options: { key: string; value: string; type?: string }) {
2930
super();
3031
this.key = options.key;
3132
this.value = options.value;
33+
this.type = options.type;
3234
}
3335

3436
static _postman_propertyAllowsMultipleValues() {
@@ -43,7 +45,7 @@ export class FormParam extends Property {
4345
// }
4446

4547
toJSON() {
46-
return { key: this.key, value: this.value };
48+
return { key: this.key, value: this.value, type: this.type };
4749
}
4850

4951
toString() {
@@ -64,8 +66,7 @@ function getClassFields(opts: RequestBodyOptions) {
6466
undefined,
6567
opts.formdata.
6668
map(formParamObj => new FormParam({
67-
key: formParamObj.key,
68-
value: formParamObj.value,
69+
...formParamObj,
6970
}))
7071
) :
7172
undefined;
@@ -149,10 +150,13 @@ export class RequestBody extends PropertyBase {
149150
try {
150151
switch (this.mode) {
151152
case 'formdata':
152-
// TODO: it should return data in "multipart/form-data" format
153-
throw Error('formdata is unsupported yet');
153+
return this.formdata ?
154+
this.formdata.map(param => param.toString(), {}).join('&') :
155+
'';
154156
case 'urlencoded':
155-
return this.urlencoded ? this.urlencoded.map(formData => formData.toString(), {}).join('&') : '';
157+
return this.urlencoded ?
158+
this.urlencoded.map(param => param.toString(), {}).join('&') :
159+
'';
156160
case 'raw':
157161
return this.raw || '';
158162
case 'file':
@@ -197,6 +201,10 @@ export interface RequestSize {
197201
}
198202

199203
function requestOptionsToClassFields(options: RequestOptions) {
204+
if (!options.url || options.url === '') {
205+
throw Error('Request URL is not specified');
206+
}
207+
200208
const url = typeof options.url === 'string' ? new Url(options.url) : options.url;
201209
const method = options.method || 'GET';
202210

0 commit comments

Comments
 (0)