diff --git a/packages/hoppscotch-app/components/http/Body.vue b/packages/hoppscotch-app/components/http/Body.vue
index f2a635210..368766c44 100644
--- a/packages/hoppscotch-app/components/http/Body.vue
+++ b/packages/hoppscotch-app/components/http/Body.vue
@@ -50,6 +50,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+ {{ t("empty.headers") }}
+
+
+
+
+
+
+
+
diff --git a/packages/hoppscotch-app/helpers/functional/array.ts b/packages/hoppscotch-app/helpers/functional/array.ts
new file mode 100644
index 000000000..f2e387d17
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/functional/array.ts
@@ -0,0 +1,2 @@
+export const stringArrayJoin = (separator: string) => (arr: string[]) =>
+ arr.join(separator)
diff --git a/packages/hoppscotch-app/helpers/rawKeyValue.ts b/packages/hoppscotch-app/helpers/rawKeyValue.ts
new file mode 100644
index 000000000..b89f2d3ac
--- /dev/null
+++ b/packages/hoppscotch-app/helpers/rawKeyValue.ts
@@ -0,0 +1,39 @@
+import * as A from "fp-ts/Array"
+import * as RA from "fp-ts/ReadonlyArray"
+import * as S from "fp-ts/string"
+import { pipe, flow } from "fp-ts/function"
+import { stringArrayJoin } from "./functional/array"
+
+export type RawKeyValueEntry = {
+ key: string
+ value: string
+ active: boolean
+}
+
+const parseRawKeyValueEntry = (str: string): RawKeyValueEntry => {
+ const trimmed = str.trim()
+ const inactive = trimmed.startsWith("#")
+
+ const [key, value] = trimmed.split(":").map(S.trim)
+
+ return {
+ key: inactive ? key.replaceAll(/^#+\s*/g, "") : key, // Remove comment hash and early space
+ value,
+ active: !inactive,
+ }
+}
+
+export const parseRawKeyValueEntries = flow(
+ S.split("\n"),
+ RA.map(parseRawKeyValueEntry),
+ RA.toArray
+)
+
+export const rawKeyValueEntriesToString = (entries: RawKeyValueEntry[]) =>
+ pipe(
+ entries,
+ A.map(({ key, value, active }) =>
+ active ? `${key}: ${value}` : `# ${key}: ${value}`
+ ),
+ stringArrayJoin("\n")
+ )
diff --git a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts
index 73dbd0928..d204dd750 100644
--- a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts
+++ b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts
@@ -1,7 +1,6 @@
-import * as RA from "fp-ts/ReadonlyArray"
-import * as S from "fp-ts/string"
+import * as A from "fp-ts/Array"
import qs from "qs"
-import { pipe, flow } from "fp-ts/function"
+import { pipe } from "fp-ts/function"
import { combineLatest, Observable } from "rxjs"
import { map } from "rxjs/operators"
import {
@@ -11,6 +10,7 @@ import {
} from "@hoppscotch/data"
import { parseTemplateString, parseBodyEnvVariables } from "../templating"
import { tupleToRecord } from "../functional/record"
+import { parseRawKeyValueEntries } from "../rawKeyValue"
import { Environment, getGlobalVariables } from "~/newstore/environments"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -66,18 +66,15 @@ function getFinalBodyFromRequest(
if (request.body.contentType === "application/x-www-form-urlencoded") {
return pipe(
request.body.body,
- S.split("\n"),
- RA.map(
- flow(
- // Define how each lines are parsed
+ parseRawKeyValueEntries,
- S.split(":"), // Split by ":"
- RA.map(S.trim), // Remove trailing spaces in key/value begins and ends
- ([key, value]) => [key, value ?? ""] as [string, string] // Add a default empty by default
- )
- ),
- RA.toArray,
- tupleToRecord, // Convert the tuple to a record
+ // Filter out active
+ A.filter((x) => x.active),
+ // Convert to tuple
+ A.map(({ key, value }) => [key, value] as [string, string]),
+ // Tuple to Record object
+ tupleToRecord,
+ // Stringify
qs.stringify
)
}
diff --git a/packages/hoppscotch-app/newstore/RESTSession.ts b/packages/hoppscotch-app/newstore/RESTSession.ts
index 324f1aaf9..d00d7314f 100644
--- a/packages/hoppscotch-app/newstore/RESTSession.ts
+++ b/packages/hoppscotch-app/newstore/RESTSession.ts
@@ -1,3 +1,5 @@
+import * as A from "fp-ts/Array"
+import { pipe } from "fp-ts/function"
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
import { Ref } from "@nuxtjs/composition-api"
import {
@@ -15,6 +17,11 @@ import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { useStream } from "~/helpers/utils/composables"
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
+import {
+ parseRawKeyValueEntries,
+ rawKeyValueEntriesToString,
+ RawKeyValueEntry,
+} from "~/helpers/rawKeyValue"
type RESTSession = {
request: HoppRESTRequest
@@ -203,9 +210,30 @@ const dispatchers = defineDispatchers({
curr: RESTSession,
{ newContentType }: { newContentType: ValidContentTypes | null }
) {
+ // TODO: Cleaner implementation
// TODO: persist body evenafter switching content typees
if (curr.request.body.contentType !== "multipart/form-data") {
if (newContentType === "multipart/form-data") {
+ // Preserve entries when comping from urlencoded to multipart
+ if (
+ curr.request.body.contentType === "application/x-www-form-urlencoded"
+ ) {
+ return {
+ ...curr.request,
+ body:
{
+ contentType: "multipart/form-data",
+ body: pipe(
+ curr.request.body.body,
+ parseRawKeyValueEntries,
+ A.map(
+ ({ key, value, active }) =>
+ { key, value, active, isFile: false }
+ )
+ ),
+ },
+ }
+ }
+
// Going from non-formdata to form-data, discard contents and set empty array as body
return {
request: {
@@ -232,6 +260,29 @@ const dispatchers = defineDispatchers({
}
}
} else if (newContentType !== "multipart/form-data") {
+ if (newContentType === "application/x-www-form-urlencoded") {
+ return {
+ request: {
+ ...curr.request,
+ body: {
+ contentType: newContentType,
+ body: pipe(
+ curr.request.body.body,
+ A.map(
+ ({ key, value, isFile, active }) =>
+ {
+ key,
+ value: isFile ? "" : value,
+ active,
+ }
+ ),
+ rawKeyValueEntriesToString
+ ),
+ },
+ },
+ }
+ }
+
// Going from formdata to non-formdata, discard contents and set empty string
return {
request: {