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") }}
+
+ {{ 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, + } + }, +})