pre-request.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import {
  2. Environment,
  3. HoppRESTRequest,
  4. parseBodyEnvVariablesE,
  5. parseRawKeyValueEntriesE,
  6. parseTemplateString,
  7. parseTemplateStringE,
  8. } from "@hoppscotch/data";
  9. import { runPreRequestScript } from "@hoppscotch/js-sandbox";
  10. import { flow, pipe } from "fp-ts/function";
  11. import * as TE from "fp-ts/TaskEither";
  12. import * as E from "fp-ts/Either";
  13. import * as RA from "fp-ts/ReadonlyArray";
  14. import * as A from "fp-ts/Array";
  15. import * as O from "fp-ts/Option";
  16. import * as S from "fp-ts/string";
  17. import qs from "qs";
  18. import { EffectiveHoppRESTRequest } from "../interfaces/request";
  19. import { error, HoppCLIError } from "../types/errors";
  20. import { HoppEnvs } from "../types/request";
  21. import { isHoppCLIError } from "./checks";
  22. import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
  23. import { toFormData } from "./mutators";
  24. import { getEffectiveFinalMetaData } from "./getters";
  25. import { PreRequestMetrics } from "../types/response";
  26. /**
  27. * Runs pre-request-script runner over given request which extracts set ENVs and
  28. * applies them on current request to generate updated request.
  29. * @param request HoppRESTRequest to be converted to EffectiveHoppRESTRequest.
  30. * @param envs Environment variables related to request.
  31. * @returns EffectiveHoppRESTRequest that includes parsed ENV variables with in
  32. * request OR HoppCLIError with error code and related information.
  33. */
  34. export const preRequestScriptRunner = (
  35. request: HoppRESTRequest,
  36. envs: HoppEnvs
  37. ): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
  38. pipe(
  39. TE.of(request),
  40. TE.chain(({ preRequestScript }) =>
  41. runPreRequestScript(preRequestScript, envs)
  42. ),
  43. TE.map(
  44. ({ selected, global }) =>
  45. <Environment>{ name: "Env", variables: [...selected, ...global] }
  46. ),
  47. TE.chainEitherKW((env) => getEffectiveRESTRequest(request, env)),
  48. TE.mapLeft((reason) =>
  49. isHoppCLIError(reason)
  50. ? reason
  51. : error({
  52. code: "PRE_REQUEST_SCRIPT_ERROR",
  53. data: reason,
  54. })
  55. )
  56. );
  57. /**
  58. * Outputs an executable request format with environment variables applied
  59. *
  60. * @param request The request to source from
  61. * @param environment The environment to apply
  62. *
  63. * @returns An object with extra fields defining a complete request
  64. */
  65. export function getEffectiveRESTRequest(
  66. request: HoppRESTRequest,
  67. environment: Environment
  68. ): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
  69. const envVariables = environment.variables;
  70. // Parsing final headers with applied ENVs.
  71. const _effectiveFinalHeaders = getEffectiveFinalMetaData(
  72. request.headers,
  73. environment
  74. );
  75. if (E.isLeft(_effectiveFinalHeaders)) {
  76. return _effectiveFinalHeaders;
  77. }
  78. const effectiveFinalHeaders = _effectiveFinalHeaders.right;
  79. // Parsing final parameters with applied ENVs.
  80. const _effectiveFinalParams = getEffectiveFinalMetaData(
  81. request.params,
  82. environment
  83. );
  84. if (E.isLeft(_effectiveFinalParams)) {
  85. return _effectiveFinalParams;
  86. }
  87. const effectiveFinalParams = _effectiveFinalParams.right;
  88. // Authentication
  89. if (request.auth.authActive) {
  90. // TODO: Support a better b64 implementation than btoa ?
  91. if (request.auth.authType === "basic") {
  92. const username = parseTemplateString(request.auth.username, envVariables);
  93. const password = parseTemplateString(request.auth.password, envVariables);
  94. effectiveFinalHeaders.push({
  95. active: true,
  96. key: "Authorization",
  97. value: `Basic ${btoa(`${username}:${password}`)}`,
  98. });
  99. } else if (
  100. request.auth.authType === "bearer" ||
  101. request.auth.authType === "oauth-2"
  102. ) {
  103. effectiveFinalHeaders.push({
  104. active: true,
  105. key: "Authorization",
  106. value: `Bearer ${parseTemplateString(
  107. request.auth.token,
  108. envVariables
  109. )}`,
  110. });
  111. } else if (request.auth.authType === "api-key") {
  112. const { key, value, addTo } = request.auth;
  113. if (addTo === "Headers") {
  114. effectiveFinalHeaders.push({
  115. active: true,
  116. key: parseTemplateString(key, envVariables),
  117. value: parseTemplateString(value, envVariables),
  118. });
  119. } else if (addTo === "Query params") {
  120. effectiveFinalParams.push({
  121. active: true,
  122. key: parseTemplateString(key, envVariables),
  123. value: parseTemplateString(value, envVariables),
  124. });
  125. }
  126. }
  127. }
  128. // Parsing final-body with applied ENVs.
  129. const _effectiveFinalBody = getFinalBodyFromRequest(request, envVariables);
  130. if (E.isLeft(_effectiveFinalBody)) {
  131. return _effectiveFinalBody;
  132. }
  133. const effectiveFinalBody = _effectiveFinalBody.right;
  134. if (request.body.contentType)
  135. effectiveFinalHeaders.push({
  136. active: true,
  137. key: "content-type",
  138. value: request.body.contentType,
  139. });
  140. // Parsing final-endpoint with applied ENVs.
  141. const _effectiveFinalURL = parseTemplateStringE(
  142. request.endpoint,
  143. envVariables
  144. );
  145. if (E.isLeft(_effectiveFinalURL)) {
  146. return E.left(
  147. error({
  148. code: "PARSING_ERROR",
  149. data: `${request.endpoint} (${_effectiveFinalURL.left})`,
  150. })
  151. );
  152. }
  153. const effectiveFinalURL = _effectiveFinalURL.right;
  154. return E.right({
  155. ...request,
  156. effectiveFinalURL,
  157. effectiveFinalHeaders,
  158. effectiveFinalParams,
  159. effectiveFinalBody,
  160. });
  161. }
  162. /**
  163. * Replaces template variables in request's body from the given set of ENVs,
  164. * to generate final request body without any template variables.
  165. * @param request Provides request's body, on which ENVs has to be applied.
  166. * @param envVariables Provides set of key-value pairs (environment variables),
  167. * used to parse-out template variables.
  168. * @returns Final request body without any template variables as value.
  169. * Or, HoppCLIError in case of error while parsing.
  170. */
  171. function getFinalBodyFromRequest(
  172. request: HoppRESTRequest,
  173. envVariables: Environment["variables"]
  174. ): E.Either<HoppCLIError, string | null | FormData> {
  175. if (request.body.contentType === null) {
  176. return E.right(null);
  177. }
  178. if (request.body.contentType === "application/x-www-form-urlencoded") {
  179. return pipe(
  180. request.body.body,
  181. parseRawKeyValueEntriesE,
  182. E.map(
  183. flow(
  184. RA.toArray,
  185. /**
  186. * Filtering out empty keys and non-active pairs.
  187. */
  188. A.filter(({ active, key }) => active && !S.isEmpty(key)),
  189. /**
  190. * Mapping each key-value to template-string-parser with either on array,
  191. * which will be resolved in further steps.
  192. */
  193. A.map(({ key, value }) => [
  194. parseTemplateStringE(key, envVariables),
  195. parseTemplateStringE(value, envVariables),
  196. ]),
  197. /**
  198. * Filtering and mapping only right-eithers for each key-value as [string, string].
  199. */
  200. A.filterMap(([key, value]) =>
  201. E.isRight(key) && E.isRight(value)
  202. ? O.some([key.right, value.right] as [string, string])
  203. : O.none
  204. ),
  205. tupleToRecord,
  206. qs.stringify
  207. )
  208. ),
  209. E.mapLeft((e) => error({ code: "PARSING_ERROR", data: e.message }))
  210. );
  211. }
  212. if (request.body.contentType === "multipart/form-data") {
  213. return pipe(
  214. request.body.body,
  215. A.filter((x) => x.key !== "" && x.active), // Remove empty keys
  216. // Sort files down
  217. arraySort((a, b) => {
  218. if (a.isFile) return 1;
  219. if (b.isFile) return -1;
  220. return 0;
  221. }),
  222. // FormData allows only a single blob in an entry,
  223. // we split array blobs into separate entries (FormData will then join them together during exec)
  224. arrayFlatMap((x) =>
  225. x.isFile
  226. ? x.value.map((v) => ({
  227. key: parseTemplateString(x.key, envVariables),
  228. value: v as string | Blob,
  229. }))
  230. : [
  231. {
  232. key: parseTemplateString(x.key, envVariables),
  233. value: parseTemplateString(x.value, envVariables),
  234. },
  235. ]
  236. ),
  237. toFormData,
  238. E.right
  239. );
  240. }
  241. return pipe(
  242. parseBodyEnvVariablesE(request.body.body, envVariables),
  243. E.mapLeft((e) =>
  244. error({
  245. code: "PARSING_ERROR",
  246. data: `${request.body.body} (${e})`,
  247. })
  248. )
  249. );
  250. }
  251. /**
  252. * Get pre-request-metrics (stats + duration) object based on existence of
  253. * PRE_REQUEST_ERROR code in given hopp-error list.
  254. * @param errors List of errors to check for PRE_REQUEST_ERROR code.
  255. * @param duration Time taken (in seconds) to execute the pre-request-script.
  256. * @returns Object containing details of pre-request-script's execution stats
  257. * i.e., failed/passed data and duration.
  258. */
  259. export const getPreRequestMetrics = (
  260. errors: HoppCLIError[],
  261. duration: number
  262. ): PreRequestMetrics =>
  263. pipe(
  264. errors,
  265. A.some(({ code }) => code === "PRE_REQUEST_SCRIPT_ERROR"),
  266. (hasPreReqErrors) =>
  267. hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
  268. (scripts) => <PreRequestMetrics>{ scripts, duration }
  269. );