insomnia/plugins/insomnia-plugin-response/index.js
Brian Goad 7dd07ba2fe
Fixes 944 | Remove the check for returning more than one JSON result in Response Plugin (#5192)
* Remove the check for returning more than one result in JSON. If it's an array of objects, the following lines will just stringify. This allows the response to be parsed by JSONPath

* Update test to permit more than one result

* add edge-case

Co-authored-by: Filipe Freire <livrofubia@gmail.com>
2022-10-25 09:25:39 +00:00

283 lines
7.9 KiB
JavaScript

const { JSONPath } = require('jsonpath-plus');
const iconv = require('iconv-lite');
const { query: queryXPath } = require('insomnia-xpath');
function isFilterableField(field) {
return field !== 'raw' && field !== 'url';
}
const defaultTriggerBehaviour = 'never';
module.exports.templateTags = [
{
name: 'response',
displayName: 'Response',
description: "reference values from other request's responses",
args: [
{
displayName: 'Attribute',
type: 'enum',
options: [
{
displayName: 'Body Attribute',
description: 'value of response body',
value: 'body',
},
{
displayName: 'Raw Body',
description: 'entire response body',
value: 'raw',
},
{
displayName: 'Header',
description: 'value of response header',
value: 'header',
},
{
displayName: 'Request URL',
description: 'Url of initiating request',
value: 'url',
},
],
},
{
displayName: 'Request',
type: 'model',
model: 'Request',
},
{
type: 'string',
encoding: 'base64',
hide: args => !isFilterableField(args[0].value),
displayName: args => {
switch (args[0].value) {
case 'body':
return 'Filter (JSONPath or XPath)';
case 'header':
return 'Header Name';
default:
return 'Filter';
}
},
},
{
displayName: 'Trigger Behavior',
help: 'Configure when to resend the dependent request',
type: 'enum',
defaultValue: defaultTriggerBehaviour,
options: [
{
displayName: 'Never',
description: 'never resend request',
value: 'never',
},
{
displayName: 'No History',
description: 'resend when no responses present',
value: 'no-history',
},
{
displayName: 'When Expired',
description: 'resend when existing response has expired',
value: 'when-expired',
},
{
displayName: 'Always',
description: 'resend request when needed',
value: 'always',
},
],
},
{
displayName: 'Max age (seconds)',
help: 'The maximum age of a response to use before it expires',
type: 'number',
hide: args => {
const triggerBehavior = (args[3] && args[3].value) || defaultTriggerBehaviour;
return triggerBehavior !== 'when-expired';
},
defaultValue: 60,
},
],
async run(context, field, id, filter, resendBehavior, maxAgeSeconds) {
filter = filter || '';
resendBehavior = (resendBehavior || defaultTriggerBehaviour).toLowerCase();
if (!['body', 'header', 'raw', 'url'].includes(field)) {
throw new Error(`Invalid response field ${field}`);
}
if (!id) {
throw new Error('No request specified');
}
const request = await context.util.models.request.getById(id);
if (!request) {
throw new Error(`Could not find request ${id}`);
}
const environmentId = context.context.getEnvironmentId();
let response = await context.util.models.response.getLatestForRequestId(id, environmentId);
let shouldResend = false;
switch (resendBehavior) {
case 'no-history':
shouldResend = !response;
break;
case 'when-expired':
if (!response) {
shouldResend = true;
} else {
const ageSeconds = (Date.now() - response.created) / 1000;
shouldResend = ageSeconds > maxAgeSeconds;
}
break;
case 'always':
shouldResend = true;
break;
case 'never':
default:
shouldResend = false;
break;
}
// Make sure we only send the request once per render so we don't have infinite recursion
const requestChain = context.context.getExtraInfo('requestChain') || [];
if (requestChain.some(id => id === request._id)) {
console.log('[response tag] Preventing recursive render');
shouldResend = false;
}
if (shouldResend && context.renderPurpose === 'send') {
console.log('[response tag] Resending dependency');
requestChain.push(request._id)
response = await context.network.sendRequest(request, [
{ name: 'requestChain', value: requestChain }
]);
}
if (!response) {
console.log('[response tag] No response found');
throw new Error('No responses for request');
}
if (response.error) {
console.log('[response tag] Response error ' + response.error);
throw new Error('Failed to send dependent request ' + response.error);
}
if (!response.statusCode) {
console.log('[response tag] Invalid status code ' + response.statusCode);
throw new Error('No successful responses for request');
}
if (isFilterableField(field) && !filter) {
throw new Error(`No ${field} filter specified`);
}
const sanitizedFilter = filter.trim();
const bodyBuffer = context.util.models.response.getBodyBuffer(response, '');
const match = response.contentType && response.contentType.match(/charset=([\w-]+)/);
const charset = match && match.length >= 2 ? match[1] : 'utf-8';
switch (field) {
case 'header':
return matchHeader(response.headers, sanitizedFilter);
case 'url':
return response.url;
case 'raw':
// Sometimes iconv conversion fails so fallback to regular buffer
try {
return iconv.decode(bodyBuffer, charset);
} catch (err) {
console.warn('[response] Failed to decode body', err);
return bodyBuffer.toString();
}
case 'body':
// Sometimes iconv conversion fails so fallback to regular buffer
let body;
try {
body = iconv.decode(bodyBuffer, charset);
} catch (err) {
console.warn('[response] Failed to decode body', err);
body = bodyBuffer.toString();
}
if (sanitizedFilter.indexOf('$') === 0) {
return matchJSONPath(body, sanitizedFilter);
} else {
return matchXPath(body, sanitizedFilter);
}
default:
throw new Error(`Unknown field ${field}`);
}
},
},
];
function matchJSONPath(bodyStr, query) {
let body;
let results;
try {
body = JSON.parse(bodyStr);
} catch (err) {
throw new Error(`Invalid JSON: ${err.message}`);
}
try {
results = JSONPath({json: body, path: query});
} catch (err) {
throw new Error(`Invalid JSONPath query: ${query}`);
}
if (results.length === 0) {
throw new Error(`Returned no results: ${query}`);
}
if (results.length > 1 ) {
return JSON.stringify(results);
}
if (typeof results[0] !== 'string') {
return JSON.stringify(results[0]);
} else {
return results[0];
}
}
function matchXPath(bodyStr, query) {
const results = queryXPath(bodyStr, query);
if (results.length === 0) {
throw new Error(`Returned no results: ${query}`);
} else if (results.length > 1) {
throw new Error(`Returned more than one result: ${query}`);
}
return results[0].inner;
}
function matchHeader(headers, name) {
if (!headers.length) {
throw new Error('No headers available');
}
const header = headers.find(h => h.name.toLowerCase() === name.toLowerCase());
if (!header) {
const names = headers.map(c => `"${c.name}"`).join(',\n\t');
throw new Error(`No header with name "${name}".\nChoices are [\n\t${names}\n]`);
}
return header.value;
}