diff --git a/packages/hoppscotch-cli/src/interfaces/request.ts b/packages/hoppscotch-cli/src/interfaces/request.ts index c8cfbd272..4aec5cbc6 100644 --- a/packages/hoppscotch-cli/src/interfaces/request.ts +++ b/packages/hoppscotch-cli/src/interfaces/request.ts @@ -43,5 +43,5 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest { active: boolean; description: string; }[]; - effectiveFinalBody: FormData | string | null; + effectiveFinalBody: FormData | string | File | null; } diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts index 2f8181069..de4563132 100644 --- a/packages/hoppscotch-cli/src/utils/pre-request.ts +++ b/packages/hoppscotch-cli/src/utils/pre-request.ts @@ -360,7 +360,7 @@ export async function getEffectiveRESTRequest( function getFinalBodyFromRequest( request: HoppRESTRequest, resolvedVariables: EnvironmentVariable[] -): E.Either { +): E.Either { if (request.body.contentType === null) { return E.right(null); } @@ -437,6 +437,20 @@ function getFinalBodyFromRequest( ); } + if (request.body.contentType === "application/octet-stream") { + const body = request.body.body; + + if (!body) { + return E.right(null); + } + + if (!(body instanceof File)) { + return E.right(null); + } + + return E.right(body); + } + return pipe( parseBodyEnvVariablesE(request.body.body, resolvedVariables), E.mapLeft((e) => diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index cce7e9746..61a1978cf 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -574,6 +574,9 @@ "extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.", "extention_enable_action": "Enable Browser Extension", "extention_not_enabled": "Extension not enabled." + }, + "requestBody": { + "agent_doesnt_support_binary_body": "Sending binary data via agent is not supported yet" } }, "layout": { @@ -666,7 +669,8 @@ "content_type_titles": { "others": "Others", "structured": "Structured", - "text": "Text" + "text": "Text", + "binary": "Binary" }, "show_content_type": "Show Content Type", "different_collection": "Cannot reorder requests from different collections", diff --git a/packages/hoppscotch-common/src/components/app/Inspection.vue b/packages/hoppscotch-common/src/components/app/Inspection.vue index e44bd75c8..9338f66c5 100644 --- a/packages/hoppscotch-common/src/components/app/Inspection.vue +++ b/packages/hoppscotch-common/src/components/app/Inspection.vue @@ -42,6 +42,7 @@ > {{ inspector.text.text }} + + { // Template refs const tippyActions = ref(null) + +const tabs = useService(RESTTabService) +const inspectionService = useService(InspectionService) + +const tabResults = inspectionService.getResultViewFor( + tabs.currentTabID.value, + (result) => result.locations.type === "body-content-type-header" +) diff --git a/packages/hoppscotch-common/src/components/http/BodyBinary.vue b/packages/hoppscotch-common/src/components/http/BodyBinary.vue new file mode 100644 index 000000000..e8f013f3c --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/BodyBinary.vue @@ -0,0 +1,70 @@ + + diff --git a/packages/hoppscotch-common/src/components/http/Headers.vue b/packages/hoppscotch-common/src/components/http/Headers.vue index d7e2b110e..9bcba6988 100644 --- a/packages/hoppscotch-common/src/components/http/Headers.vue +++ b/packages/hoppscotch-common/src/components/http/Headers.vue @@ -259,7 +259,7 @@ import { flow, pipe } from "fp-ts/function" import * as O from "fp-ts/Option" import * as RA from "fp-ts/ReadonlyArray" import { cloneDeep, isEqual } from "lodash-es" -import { reactive, ref, toRef, watch } from "vue" +import { reactive, Ref, ref, toRef, watch } from "vue" import draggable from "vuedraggable-es" import { computedAsync, useVModel } from "@vueuse/core" @@ -546,16 +546,22 @@ const clearContent = () => { const aggregateEnvs = useReadonlyStream(aggregateEnvs$, getAggregateEnvs()) -const computedHeaders = computedAsync( - async () => - (await getComputedHeaders(request.value, aggregateEnvs.value, false)).map( - (header, index) => ({ - id: `header-${index}`, - ...header, - }) - ), - [] -) +const computedHeaders: Ref< + { + source: "auth" | "body" + header: HoppRESTHeader + id: string + }[] +> = ref([]) + +watch([props.modelValue, aggregateEnvs], async () => { + computedHeaders.value = ( + await getComputedHeaders(props.modelValue, aggregateEnvs.value, false) + ).map((header, index) => ({ + id: `header-${index}`, + ...header, + })) +}) const inheritedProperties = computedAsync(async () => { if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers) @@ -671,7 +677,11 @@ const headerValueResults = inspectionService.getResultViewFor( const getInspectorResult = (results: InspectorResult[], index: number) => { return results.filter((result) => { - if (result.locations.type === "url" || result.locations.type === "response") + if ( + result.locations.type === "url" || + result.locations.type === "response" || + result.locations.type === "body-content-type-header" + ) return return result.locations.index === index }) diff --git a/packages/hoppscotch-common/src/components/http/Parameters.vue b/packages/hoppscotch-common/src/components/http/Parameters.vue index 3a0a12210..913ae3976 100644 --- a/packages/hoppscotch-common/src/components/http/Parameters.vue +++ b/packages/hoppscotch-common/src/components/http/Parameters.vue @@ -379,7 +379,11 @@ const parameterValueResults = inspectionService.getResultViewFor( const getInspectorResult = (results: InspectorResult[], index: number) => { return results.filter((result) => { - if (result.locations.type === "url" || result.locations.type === "response") + if ( + result.locations.type === "url" || + result.locations.type === "response" || + result.locations.type === "body-content-type-header" + ) return return result.locations.index === index }) diff --git a/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts index 12c9924f8..ce89ff74d 100644 --- a/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts +++ b/packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts @@ -44,7 +44,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest { effectiveFinalURL: string effectiveFinalHeaders: HoppRESTHeaders effectiveFinalParams: HoppRESTParams - effectiveFinalBody: FormData | string | null + effectiveFinalBody: FormData | string | null | File effectiveFinalRequestVariables: { key: string; value: string }[] } @@ -249,6 +249,32 @@ export const getComputedBodyHeaders = ( // Body should have a non-null content-type if (!req.body || req.body.contentType === null) return [] + if ( + req.body && + req.body.contentType === "application/octet-stream" && + req.body.body + ) { + const filename = req.body.body.name + const fileType = req.body.body.type + + const contentType = fileType ? fileType : "application/octet-stream" + + return [ + { + active: true, + key: "content-type", + value: contentType, + description: "", + }, + { + active: true, + key: "Content-Disposition", + value: `attachment; filename="${filename}"`, + description: "", + }, + ] + } + return [ { active: true, @@ -408,6 +434,10 @@ export const resolvesEnvsInBody = ( ): HoppRESTReqBody => { if (!body.contentType) return body + if (body.contentType === "application/octet-stream") { + return body + } + if (body.contentType === "multipart/form-data") { if (!body.body) { return { @@ -448,7 +478,7 @@ function getFinalBodyFromRequest( request: HoppRESTRequest, envVariables: Environment["variables"], showKeyIfSecret = false -): FormData | string | null { +): FormData | Blob | string | null { if (request.body.contentType === null) return null if (request.body.contentType === "application/x-www-form-urlencoded") { @@ -527,6 +557,10 @@ function getFinalBodyFromRequest( ) } + if (request.body.contentType === "application/octet-stream") { + return request.body.body + } + let bodyContent = request.body.body ?? "" if (isJSONContentType(request.body.contentType)) diff --git a/packages/hoppscotch-common/src/helpers/utils/contenttypes.ts b/packages/hoppscotch-common/src/helpers/utils/contenttypes.ts index a4610230f..482174073 100644 --- a/packages/hoppscotch-common/src/helpers/utils/contenttypes.ts +++ b/packages/hoppscotch-common/src/helpers/utils/contenttypes.ts @@ -1,6 +1,6 @@ import { ValidContentTypes } from "@hoppscotch/data" -export type Content = "json" | "xml" | "multipart" | "html" | "plain" +export type Content = "json" | "xml" | "multipart" | "html" | "plain" | "binary" export const knownContentTypes: Record = { "application/json": "json", @@ -10,6 +10,7 @@ export const knownContentTypes: Record = { "application/xml": "xml", "application/x-www-form-urlencoded": "multipart", "multipart/form-data": "multipart", + "application/octet-stream": "binary", "text/html": "html", "text/plain": "plain", "text/xml": "xml", @@ -19,6 +20,7 @@ type ContentTypeTitle = | "request.content_type_titles.text" | "request.content_type_titles.structured" | "request.content_type_titles.others" + | "request.content_type_titles.binary" type SegmentedContentType = { title: ContentTypeTitle @@ -41,6 +43,10 @@ export const segmentedContentTypes: SegmentedContentType[] = [ title: "request.content_type_titles.structured", contentTypes: ["application/x-www-form-urlencoded", "multipart/form-data"], }, + { + title: "request.content_type_titles.binary", + contentTypes: ["application/octet-stream"], + }, { title: "request.content_type_titles.others", contentTypes: ["text/html", "text/plain"], diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue index e423f5321..53c949c58 100644 --- a/packages/hoppscotch-common/src/pages/index.vue +++ b/packages/hoppscotch-common/src/pages/index.vue @@ -146,6 +146,7 @@ import { useService } from "dioc/vue" import { InspectionService } from "~/services/inspection" import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector" import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector" +import { InterceptorsInspectorService } from "~/services/inspection/inspectors/interceptors.inspector" import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector" import { cloneDeep } from "lodash-es" import { RESTTabService } from "~/services/tab/rest" @@ -417,6 +418,8 @@ useService(HeaderInspectorService) useService(EnvironmentInspectorService) useService(ResponseInspectorService) useService(AuthorizationInspectorService) +useService(InterceptorsInspectorService) + for (const inspectorDef of platform.additionalInspectors ?? []) { useService(inspectorDef.service) } diff --git a/packages/hoppscotch-common/src/services/inspection/index.ts b/packages/hoppscotch-common/src/services/inspection/index.ts index 4e564635a..01ab4ded1 100644 --- a/packages/hoppscotch-common/src/services/inspection/index.ts +++ b/packages/hoppscotch-common/src/services/inspection/index.ts @@ -46,6 +46,9 @@ export type InspectorLocation = | { type: "response" } + | { + type: "body-content-type-header" + } /** * Defines info about an inspector result so the UI can render it @@ -60,7 +63,7 @@ export interface InspectorResult { text: string apply: () => void } - doc: { + doc?: { text: string link: string } diff --git a/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts b/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts index 58ab7e736..5ba1ba197 100644 --- a/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts +++ b/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts @@ -100,7 +100,8 @@ export class EnvironmentInspectorService extends Service implements Inspector { position: locations.type === "url" || locations.type === "body" || - locations.type === "response" + locations.type === "response" || + locations.type === "body-content-type-header" ? "key" : locations.position, index: index, @@ -222,7 +223,8 @@ export class EnvironmentInspectorService extends Service implements Inspector { position: locations.type === "url" || locations.type === "body" || - locations.type === "response" + locations.type === "response" || + locations.type === "body-content-type-header" ? "key" : locations.position, index: index, diff --git a/packages/hoppscotch-common/src/services/inspection/inspectors/interceptors.inspector.ts b/packages/hoppscotch-common/src/services/inspection/inspectors/interceptors.inspector.ts new file mode 100644 index 000000000..c5d5faaf6 --- /dev/null +++ b/packages/hoppscotch-common/src/services/inspection/inspectors/interceptors.inspector.ts @@ -0,0 +1,65 @@ +import { Service } from "dioc" +import { InspectionService, Inspector, InspectorResult } from ".." +import { computed, Ref } from "vue" +import { + HoppRESTRequest, + HoppRESTResponseOriginalRequest, +} from "@hoppscotch/data" + +import IconAlertCircle from "~icons/lucide/alert-circle" +import { InterceptorService } from "~/services/interceptor.service" +import { getI18n } from "~/modules/i18n" + +/** + * This inspector is responsible for inspecting the interceptor usage. + * + * NOTE: Initializing this service registers it as a inspector with the Inspection Service. + */ +export class InterceptorsInspectorService extends Service implements Inspector { + public static readonly ID = "INTERCEPTORS_INSPECTOR_SERVICE" + + inspectorID = "interceptors" + + private t = getI18n() + + private readonly inspection = this.bind(InspectionService) + private readonly interceptors = this.bind(InterceptorService) + + onServiceInit() { + this.inspection.registerInspector(this) + } + + getInspections( + req: Readonly> + ) { + return computed((): InspectorResult[] => { + const isBinaryBody = + req.value.body.contentType === "application/octet-stream" + + // TODO: define the supported capabilities in the interceptor + const isAgent = this.interceptors.currentInterceptorID.value === "agent" + + if (isBinaryBody && isAgent) { + return [ + { + isApplicable: true, + icon: IconAlertCircle, + severity: 2, + text: { + type: "text", + text: this.t( + "inspections.requestBody.agent_doesnt_support_binary_body" + ), + }, + locations: { + type: "body-content-type-header", + }, + id: "interceptors-inspector-binary-agent-body-content-type-header", + }, + ] + } + + return [] + }) + } +} diff --git a/packages/hoppscotch-data/src/rest/content-types.ts b/packages/hoppscotch-data/src/rest/content-types.ts index d3d27cecc..e1e51b70f 100644 --- a/packages/hoppscotch-data/src/rest/content-types.ts +++ b/packages/hoppscotch-data/src/rest/content-types.ts @@ -7,6 +7,7 @@ export const knownContentTypes = { "text/xml": "xml", "application/x-www-form-urlencoded": "multipart", "multipart/form-data": "multipart", + "application/octet-stream": "binary", "text/html": "html", "text/plain": "plain", } diff --git a/packages/hoppscotch-data/src/rest/v/9.ts b/packages/hoppscotch-data/src/rest/v/9.ts index be20eaf6f..ede23c41b 100644 --- a/packages/hoppscotch-data/src/rest/v/9.ts +++ b/packages/hoppscotch-data/src/rest/v/9.ts @@ -41,6 +41,10 @@ export const HoppRESTReqBody = z.union([ body: z.array(FormDataKeyValue).catch([]), showIndividualContentType: z.boolean().optional().catch(false), }), + z.object({ + contentType: z.literal("application/octet-stream"), + body: z.instanceof(File).nullable().catch(null), + }), z.object({ contentType: z.union([ z.literal("application/json"), @@ -50,6 +54,7 @@ export const HoppRESTReqBody = z.union([ z.literal("application/xml"), z.literal("text/xml"), z.literal("application/x-www-form-urlencoded"), + z.literal("binary"), z.literal("text/html"), z.literal("text/plain"), ]),