From 726de879240514e16b8dda842ce8a2eab3626851 Mon Sep 17 00:00:00 2001 From: mahamdan Date: Thu, 30 Nov 2023 19:37:35 +0200 Subject: [PATCH] [CSCwh10631] Form data content types should not provide JSON option and should properly encode payload as query parameters instead of JSON-like string converted into Form Data --- src/components/Operation/Operation.tsx | 58 ++++++++++++++++++++++---- src/components/TryOut/Body.tsx | 6 ++- src/components/TryOut/TryOut.tsx | 11 ++--- src/utils/tryout.ts | 2 +- 4 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index ff9b2f1646..7c866620c6 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -14,7 +14,12 @@ import { } from '../../common-elements'; import { OperationModel } from '../../services/models'; import styled from '../../styled-components'; -import { appendParamsToPath, mapStatusCodeToType, setCookieParams } from '../../utils/tryout'; +import { + appendParamsToPath, + entriesToQueryString, + mapStatusCodeToType, + setCookieParams, +} from '../../utils/tryout'; import { CallbacksList } from '../Callbacks'; import { Endpoint } from '../Endpoint/Endpoint'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; @@ -75,7 +80,13 @@ export class Operation extends React.Component { * defines param location as being one of 'path', 'query', 'cookie' or 'header', while * fetch API defines request as having RequestInit type, which has 'headers' as a member field */ - handleApiCall = ({ queryParams, pathParams, cookieParams, header: headers, body }: Request) => { + handleApiCall = ({ + queryParams, + pathParams, + cookieParams, + header: headers, + body = null, + }: Request) => { const { operation: { httpVerb, path, requestBody }, } = this.props; @@ -83,7 +94,7 @@ export class Operation extends React.Component { const requestBodyContent = requestBody?.content; const activeMimeIdx = requestBodyContent?.activeMimeIdx; const contentType = - activeMimeIdx !== undefined && requestBodyContent?.mediaTypes[activeMimeIdx]?.name; + activeMimeIdx === undefined ? undefined : requestBodyContent?.mediaTypes[activeMimeIdx]?.name; const isFormData = contentType === 'multipart/form-data'; @@ -91,9 +102,14 @@ export class Operation extends React.Component { headers = { 'Content-Type': contentType || 'application/json', ...headers }; } - const formData = new FormData(); - if (isFormData && body && typeof body === 'object') { - Object.entries(body as any).forEach(([key, value]) => { + const getFormDataWithObjectEntriesAppended = ( + object: Record, + formData: FormData, + ) => { + if (!object || typeof object !== 'object' || !formData) { + return formData; + } + Object.entries(object as any).forEach(([key, value]) => { const isFileValue = (value as any) instanceof File; const isJsonValue = !isFileValue && typeof value === 'object' && value !== null; formData.append( @@ -101,7 +117,33 @@ export class Operation extends React.Component { isFileValue ? (value as any) : isJsonValue ? JSON.stringify(value)! : value, ); }); - } + return formData; + }; + + const getFormDataFromObject = (object: Record) => { + const formData = new FormData(); + return getFormDataWithObjectEntriesAppended(object, formData); + }; + + const getQueryStringFromObject = (object: Record) => { + const entries = Object.entries(object as any); + return entriesToQueryString(entries); + }; + + const getBodyByContentType = (body: BodyInit | null, contentType: string | undefined): any => { + if (typeof body === 'string' || body === null) { + return body; + } + const isFormData = contentType === 'multipart/form-data'; + const isEncodedFormContent = contentType === 'application/x-www-form-urlencoded'; + if (isFormData) { + return getFormDataFromObject(body); + } + if (isEncodedFormContent) { + return getQueryStringFromObject(body); + } + return JSON.stringify(body); + }; const request: RequestInit = Object.values(NoRequestBodyHttpVerb) @@ -114,7 +156,7 @@ export class Operation extends React.Component { : { method: httpVerb, headers, - body: typeof body === 'string' ? body : isFormData ? formData : JSON.stringify(body), + body: getBodyByContentType(body, contentType), }; setCookieParams(cookieParams); diff --git a/src/components/TryOut/Body.tsx b/src/components/TryOut/Body.tsx index 1d407c5004..b46a9c0bc7 100644 --- a/src/components/TryOut/Body.tsx +++ b/src/components/TryOut/Body.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { toLower } from 'lodash'; import { RequestBodyModel } from '../../services'; import { JsonViewer } from '../JsonViewer/JsonViewer'; @@ -35,6 +36,9 @@ export const Body = ({ requestPayload, }: BodyProps) => { if (!specBody) return null; + const contentType = toLower(specBody?.content?.active?.name); + const FORM_DATA_CONTENT_TYPES = ['multipart/form-data', 'application/x-www-form-urlencoded']; + const shouldEnableJsonOption = !FORM_DATA_CONTENT_TYPES.includes(contentType); return ( <> @@ -49,7 +53,7 @@ export const Body = ({ cursor={'pointer'} onChange={({ target: { value } }) => setIsFormData(value === 'form-data')} > - + {shouldEnableJsonOption && } diff --git a/src/components/TryOut/TryOut.tsx b/src/components/TryOut/TryOut.tsx index 41eb0e52b1..9e33805b62 100644 --- a/src/components/TryOut/TryOut.tsx +++ b/src/components/TryOut/TryOut.tsx @@ -65,7 +65,12 @@ export const TryOut = observer( header: {}, body: getInitialBodyByOperation(operation), }); - const [isFormData, setIsFormData] = React.useState(false); + + const contentType = toLower(operation?.requestBody?.content?.active?.name); + const isFormDataContent = contentType === 'multipart/form-data'; + const isEncodedFormContent = contentType === 'application/x-www-form-urlencoded'; + + const [isFormData, setIsFormData] = React.useState(isFormDataContent || isEncodedFormContent); const [error, setError] = React.useState(undefined); const [showError, setShowError] = React.useState(false); const [requiredFields, setRequiredFields] = React.useState( @@ -304,11 +309,7 @@ export const TryOut = observer( const pathParams = operation.parameters?.filter(param => param.in === 'path'); const cookieParams = operation.parameters?.filter(param => param.in === 'cookie'); - const contentType = toLower(operation?.requestBody?.content?.active?.name); const isJsonContent = contentType === 'application/json'; - const isFormDataContent = contentType === 'multipart/form-data'; - const isEncodedFormContent = contentType === 'application/x-www-form-urlencoded'; - const schemaType = operation.requestBody?.content?.mediaTypes[0]?.schema?.type; const onHeaderChange = (fieldName, value, arrayIndex, ancestors, location) => diff --git a/src/utils/tryout.ts b/src/utils/tryout.ts index c3af330e54..50a4372601 100644 --- a/src/utils/tryout.ts +++ b/src/utils/tryout.ts @@ -19,7 +19,7 @@ const appendPathParamsToPath = (path: string, pathParams: Record * ] * becomes "a=b&c=d" */ -const entriesToQueryString = (entries): string => { +export const entriesToQueryString = (entries): string => { let queryString = ''; for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i];