diff --git a/packages/hoppscotch-cli/src/types/request.ts b/packages/hoppscotch-cli/src/types/request.ts
index 7aa90d0a9..0f6f780f5 100644
--- a/packages/hoppscotch-cli/src/types/request.ts
+++ b/packages/hoppscotch-cli/src/types/request.ts
@@ -7,6 +7,7 @@ import { HoppCLIError } from "./errors";
export type FormDataEntry = {
key: string;
value: string | Blob;
+ contentType?: string;
};
export type HoppEnvPair = Environment["variables"][number];
diff --git a/packages/hoppscotch-cli/src/utils/mutators.ts b/packages/hoppscotch-cli/src/utils/mutators.ts
index 6c4ac6547..153e66b24 100644
--- a/packages/hoppscotch-cli/src/utils/mutators.ts
+++ b/packages/hoppscotch-cli/src/utils/mutators.ts
@@ -52,7 +52,21 @@ const getValidRequests = (
export const toFormData = (values: FormDataEntry[]) => {
const formData = new FormData();
- values.forEach(({ key, value }) => formData.append(key, value));
+ values.forEach(({ key, value, contentType }) => {
+ if (contentType) {
+ formData.append(
+ key,
+ new Blob([value], {
+ type: contentType,
+ }),
+ key
+ );
+
+ return;
+ }
+
+ formData.append(key, value);
+ });
return formData;
};
diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts
index 6ec0c5eb0..2f8181069 100644
--- a/packages/hoppscotch-cli/src/utils/pre-request.ts
+++ b/packages/hoppscotch-cli/src/utils/pre-request.ts
@@ -422,11 +422,13 @@ function getFinalBodyFromRequest(
? x.value.map((v) => ({
key: parseTemplateString(x.key, resolvedVariables),
value: v as string | Blob,
+ contentType: x.contentType,
}))
: [
{
key: parseTemplateString(x.key, resolvedVariables),
value: parseTemplateString(x.value, resolvedVariables),
+ contentType: x.contentType,
},
]
),
diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json
index f72388ac0..31c0d7a3f 100644
--- a/packages/hoppscotch-common/locales/en.json
+++ b/packages/hoppscotch-common/locales/en.json
@@ -652,6 +652,7 @@
"structured": "Structured",
"text": "Text"
},
+ "show_content_type": "Show Content Type",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duration",
diff --git a/packages/hoppscotch-common/src/components/http/BodyParameters.vue b/packages/hoppscotch-common/src/components/http/BodyParameters.vue
index d12a8b66f..76ed35277 100644
--- a/packages/hoppscotch-common/src/components/http/BodyParameters.vue
+++ b/packages/hoppscotch-common/src/components/http/BodyParameters.vue
@@ -7,6 +7,17 @@
{{ t("request.body") }}
+
+ {
+ body.showIndividualContentType = !body.showIndividualContentType
+ }
+ "
+ >{{ t(`request.show_content_type`) }}
+
@@ -97,6 +109,26 @@
value: $event,
active: entry.active,
isFile: entry.isFile,
+ contentType: entry.contentType,
+ })
+ "
+ />
+
+
+
@@ -139,6 +171,7 @@
? !entry.active
: false,
isFile: entry.isFile,
+ contentType: entry.contentType,
})
"
/>
@@ -231,6 +264,7 @@ const workingParams = ref([
value: "",
active: true,
isFile: false,
+ contentType: undefined,
},
},
])
diff --git a/packages/hoppscotch-common/src/helpers/functional/formData.ts b/packages/hoppscotch-common/src/helpers/functional/formData.ts
index 2ea416735..4aa43f27f 100644
--- a/packages/hoppscotch-common/src/helpers/functional/formData.ts
+++ b/packages/hoppscotch-common/src/helpers/functional/formData.ts
@@ -1,12 +1,27 @@
type FormDataEntry = {
key: string
+ contentType?: string
value: string | Blob
}
export const toFormData = (values: FormDataEntry[]) => {
const formData = new FormData()
- values.forEach(({ key, value }) => formData.append(key, value))
+ values.forEach(({ key, value, contentType }) => {
+ if (contentType) {
+ formData.append(
+ key,
+ new Blob([value], {
+ type: contentType,
+ }),
+ key
+ )
+
+ return
+ }
+
+ formData.append(key, value)
+ })
return formData
}
diff --git a/packages/hoppscotch-common/src/helpers/new-codegen/har.ts b/packages/hoppscotch-common/src/helpers/new-codegen/har.ts
index c98a49e7e..7f84f9fd6 100644
--- a/packages/hoppscotch-common/src/helpers/new-codegen/har.ts
+++ b/packages/hoppscotch-common/src/helpers/new-codegen/har.ts
@@ -72,13 +72,24 @@ const buildHarPostParams = (
{
name: entry.key,
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
- contentType: file.type,
+ contentType: entry.contentType ? entry.contentType : file?.type,
}
)
}
+
+ if (entry.contentType) {
+ return {
+ name: entry.key,
+ value: entry.value,
+ fileName: entry.key,
+ contentType: entry.contentType,
+ }
+ }
+
return {
name: entry.key,
value: entry.value,
+ contentType: entry.contentType,
}
})
}
diff --git a/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts
index 6cb21c427..12c9924f8 100644
--- a/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts
+++ b/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts
@@ -427,6 +427,7 @@ export const resolvesEnvsInBody = (
value: entry.isFile
? entry.value
: parseTemplateString(entry.value, env.variables, false, true),
+ contentType: entry.contentType,
}
),
}
@@ -512,11 +513,13 @@ function getFinalBodyFromRequest(
? x.value.map((v) => ({
key: parseTemplateString(x.key, envVariables),
value: v as string | Blob,
+ contentType: x.contentType,
}))
: [
{
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
+ contentType: x.contentType,
},
]
),
diff --git a/packages/hoppscotch-data/src/rest/index.ts b/packages/hoppscotch-data/src/rest/index.ts
index b61906bef..80c7b02e1 100644
--- a/packages/hoppscotch-data/src/rest/index.ts
+++ b/packages/hoppscotch-data/src/rest/index.ts
@@ -12,19 +12,18 @@ import V2_VERSION, { HoppRESTRequestVariables } from "./v/2"
import V3_VERSION from "./v/3"
import V4_VERSION from "./v/4"
import V5_VERSION from "./v/5"
-import V6_VERSION, { HoppRESTReqBody } from "./v/6"
+import V6_VERSION from "./v/6"
import V7_VERSION, { HoppRESTHeaders, HoppRESTParams } from "./v/7"
import V8_VERSION, { HoppRESTAuth, HoppRESTRequestResponses } from "./v/8"
+import V9_VERSION, { HoppRESTReqBody } from "./v/9"
export * from "./content-types"
export {
- FormDataKeyValue,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthInherit,
HoppRESTAuthNone,
- HoppRESTReqBodyFormData,
} from "./v/1"
export { HoppRESTRequestVariables } from "./v/2"
@@ -35,8 +34,6 @@ export { HoppRESTAuthAPIKey } from "./v/4"
export { AuthCodeGrantTypeParams } from "./v/5"
-export { HoppRESTReqBody } from "./v/6"
-
export {
HoppRESTAuthAWSSignature,
HoppRESTHeaders,
@@ -54,13 +51,15 @@ export {
HoppRESTRequestResponses,
} from "./v/8"
+export { FormDataKeyValue, HoppRESTReqBody } from "./v/9"
+
const versionedObject = z.object({
// v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
})
export const HoppRESTRequest = createVersionedEntity({
- latestVersion: 8,
+ latestVersion: 9,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
@@ -71,6 +70,7 @@ export const HoppRESTRequest = createVersionedEntity({
6: V6_VERSION,
7: V7_VERSION,
8: V8_VERSION,
+ 9: V9_VERSION,
},
getVersion(data) {
// For V1 onwards we have the v string storing the number
@@ -113,7 +113,7 @@ const HoppRESTRequestEq = Eq.struct({
responses: lodashIsEqualEq,
})
-export const RESTReqSchemaVersion = "8"
+export const RESTReqSchemaVersion = "9"
export type HoppRESTParam = HoppRESTRequest["params"][number]
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
diff --git a/packages/hoppscotch-data/src/rest/v/9.ts b/packages/hoppscotch-data/src/rest/v/9.ts
new file mode 100644
index 000000000..717d4e72f
--- /dev/null
+++ b/packages/hoppscotch-data/src/rest/v/9.ts
@@ -0,0 +1,70 @@
+import { defineVersion } from "verzod"
+import { z } from "zod"
+
+import { V8_SCHEMA } from "./8"
+
+export const FormDataKeyValue = z
+ .object({
+ key: z.string(),
+ active: z.boolean(),
+ contentType: z.string().optional().catch(undefined),
+ })
+ .and(
+ z.union([
+ z.object({
+ isFile: z.literal(true),
+ value: z.array(z.instanceof(Blob).nullable()),
+ }),
+ z.object({
+ isFile: z.literal(false),
+ value: z.string(),
+ }),
+ ])
+ )
+
+export type FormDataKeyValue = z.infer
+
+export const HoppRESTReqBody = z.union([
+ z.object({
+ contentType: z.literal(null),
+ body: z.literal(null).catch(null),
+ }),
+ z.object({
+ contentType: z.literal("multipart/form-data"),
+ body: z.array(FormDataKeyValue).catch([]),
+ showIndividualContentType: z.boolean().optional().catch(false),
+ }),
+ z.object({
+ contentType: z.union([
+ z.literal("application/json"),
+ z.literal("application/ld+json"),
+ z.literal("application/hal+json"),
+ z.literal("application/vnd.api+json"),
+ z.literal("application/xml"),
+ z.literal("text/xml"),
+ z.literal("application/x-www-form-urlencoded"),
+ z.literal("text/html"),
+ z.literal("text/plain"),
+ ]),
+ body: z.string().catch(""),
+ }),
+])
+
+export type HoppRESTReqBody = z.infer
+
+export const V9_SCHEMA = V8_SCHEMA.extend({
+ v: z.literal("9"),
+ body: HoppRESTReqBody,
+})
+
+export default defineVersion({
+ schema: V9_SCHEMA,
+ initial: false,
+ up(old: z.infer) {
+ // No migration, the new contentType added to each formdata field is optional
+ return {
+ ...old,
+ v: "9" as const,
+ }
+ },
+})