From 8b0b36bbaf7ee77e4d158cc13e273dc874ab849d Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 16 Mar 2026 10:29:59 +0800 Subject: [PATCH] feat: Support converting json data into form for submission --- src/models/configurationSettings.ts | 15 ++++++++++++++ src/models/requestMetadata.ts | 5 +++++ src/utils/httpRequestParser.ts | 32 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src/models/configurationSettings.ts b/src/models/configurationSettings.ts index 20da7373..e24dfe3a 100644 --- a/src/models/configurationSettings.ts +++ b/src/models/configurationSettings.ts @@ -50,6 +50,7 @@ export interface IRestClientSettings { readonly enableSendRequestCodeLens: boolean; readonly enableCustomVariableReferencesCodeLens: boolean; readonly useContentDispositionFilename: boolean; + readonly jsonToForm?: boolean; } export class SystemSettings implements IRestClientSettings { @@ -319,6 +320,8 @@ export class RequestSettings implements Partial { private _followRedirect?: boolean = undefined; private _rememberCookiesForSubsequentRequests?: boolean = undefined; + + private _jsonToForm?: boolean = undefined; public get followRedirect() { return this._followRedirect; @@ -328,12 +331,20 @@ export class RequestSettings implements Partial { return this._rememberCookiesForSubsequentRequests; } + public get jsonToForm() { + return this._jsonToForm; + } + public constructor(metadatas: Map) { if (metadatas.has(RequestMetadata.NoRedirect)) { this._followRedirect = false; } else if (metadatas.has(RequestMetadata.NoCookieJar)) { this._rememberCookiesForSubsequentRequests = false; } + + if (metadatas.has(RequestMetadata.JsonToForm)) { + this._jsonToForm = true; + } } } @@ -471,6 +482,10 @@ export class RestClientSettings implements IRestClientSettings { return this.systemSettings.useContentDispositionFilename; } + public get jsonToForm() { + return this.requestSettings.jsonToForm; + } + private readonly systemSettings = SystemSettings.Instance; public constructor(private readonly requestSettings: RequestSettings) { diff --git a/src/models/requestMetadata.ts b/src/models/requestMetadata.ts index 0264d7db..2b24e355 100644 --- a/src/models/requestMetadata.ts +++ b/src/models/requestMetadata.ts @@ -21,6 +21,11 @@ export enum RequestMetadata { * Used to allow user to interactively input variables for this request */ Prompt = 'prompt', + + /** + * Represents a directive to automatically URL encode a JSON request body + */ + JsonToForm = 'jsontoform', } export function fromString(value: string): RequestMetadata | undefined { diff --git a/src/utils/httpRequestParser.ts b/src/utils/httpRequestParser.ts index 1a247690..74c305da 100644 --- a/src/utils/httpRequestParser.ts +++ b/src/utils/httpRequestParser.ts @@ -99,6 +99,38 @@ export class HttpRequestParser implements RequestParser { let body = await this.parseBody(bodyLines, contentTypeHeader); if (isGraphQlRequest) { body = await this.createGraphQlBody(variableLines, contentTypeHeader, body); + } else if (this.settings.jsonToForm && typeof body === 'string') { + try { + // Strip redundant quotes around JSON objects/arrays to fix unescaped quote errors + const fixedBody = body.replace(/"\s*(\{[\s\S]*?\})\s*"/g, "$1").replace(/"\s*(\[[\s\S]*?\])\s*"/g, "$1"); + const { parse } = require('jsonc-parser'); + const errors: any[] = []; + const parsedJson = parse(fixedBody, errors, { allowTrailingComma: true }); + if (errors.length > 0) { + throw new Error(`JSON parsing failed at offset ${errors[0].offset} with length ${errors[0].length}.`); + } + const encodedStringPairs: string[] = []; + for (const key in parsedJson) { + if (parsedJson.hasOwnProperty(key)) { + const value = parsedJson[key]; + // If value is object/array, stringify it first + const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value); + encodedStringPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(stringValue)}`); + } + } + body = encodedStringPairs.join('&'); + + // Force the content-type to be application/x-www-form-urlencoded if it was converted + const contentTypeKey = Object.keys(headers).find(k => k.toLowerCase() === 'content-type'); + if (contentTypeKey) { + headers[contentTypeKey] = 'application/x-www-form-urlencoded'; + } else { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + } catch (err) { + // Return descriptive error from JSON parse failure so the request errors out usefully + throw new Error(`Invalid JSON format for @jsonToForm directive: ${err.message}`); + } } else if (this.settings.formParamEncodingStrategy !== FormParamEncodingStrategy.Never && typeof body === 'string' && MimeUtility.isFormUrlEncoded(contentTypeHeader)) { if (this.settings.formParamEncodingStrategy === FormParamEncodingStrategy.Always) { const stringPairs = body.split('&');