feat: add the ability to configure query params encoding for requests (#4412)
Co-authored-by: nivedin <nivedinp@gmail.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -230,6 +230,7 @@ declare module 'vue' {
|
||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
|
||||
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
|
||||
SmartEncodingPicker: typeof import('./components/smart/EncodingPicker.vue')['default']
|
||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
v-for="(mode, index) of modes"
|
||||
:key="`mode-${index}`"
|
||||
class="flex w-fit"
|
||||
>
|
||||
<HoppSmartRadio
|
||||
v-tippy="{ theme: 'tooltip', maxWidth: 500 }"
|
||||
:value="mode"
|
||||
:label="t(getEncodingModeName(mode))"
|
||||
:title="t(getEncodingModeTooltip(mode))"
|
||||
:selected="mode === activeMode"
|
||||
:class="'!px-0 hover:bg-transparent'"
|
||||
@change="changeMode(mode)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EncodeModes, EncodeMode, applySetting } from "~/newstore/settings"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const modes = EncodeModes
|
||||
const activeMode = useSetting("ENCODE_MODE")
|
||||
|
||||
const changeMode = (mode: EncodeMode) => {
|
||||
applySetting("ENCODE_MODE", mode)
|
||||
}
|
||||
|
||||
const getEncodingModeName = (mode: string) => {
|
||||
switch (mode) {
|
||||
case "enable":
|
||||
return "action.enable"
|
||||
case "disable":
|
||||
return "action.disable"
|
||||
case "auto":
|
||||
return "settings.auto_encode_mode"
|
||||
default:
|
||||
return "settings.encode_mode"
|
||||
}
|
||||
}
|
||||
|
||||
const getEncodingModeTooltip = (mode: string) => {
|
||||
switch (mode) {
|
||||
case "enable":
|
||||
return "settings.enable_encode_mode_tooltip"
|
||||
case "disable":
|
||||
return "settings.disable_encode_mode_tooltip"
|
||||
case "auto":
|
||||
return "settings.auto_encode_mode_tooltip"
|
||||
default:
|
||||
return "settings.enable_encode_mode_tooltip"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -20,6 +20,10 @@ export const HoppAccentColors = [
|
||||
"pink",
|
||||
] as const
|
||||
|
||||
export const EncodeModes = ["enable", "disable", "auto"] as const
|
||||
|
||||
export type EncodeMode = (typeof EncodeModes)[number]
|
||||
|
||||
export type HoppAccentColor = (typeof HoppAccentColors)[number]
|
||||
|
||||
export type SettingsDef = {
|
||||
@@ -59,6 +63,7 @@ export type SettingsDef = {
|
||||
}
|
||||
THEME_COLOR: HoppAccentColor
|
||||
BG_COLOR: HoppBgColor
|
||||
ENCODE_MODE: EncodeMode
|
||||
TELEMETRY_ENABLED: boolean
|
||||
EXPAND_NAVIGATION: boolean
|
||||
SIDEBAR: boolean
|
||||
@@ -107,6 +112,7 @@ export const getDefaultSettings = (): SettingsDef => ({
|
||||
},
|
||||
THEME_COLOR: "indigo",
|
||||
BG_COLOR: "system",
|
||||
ENCODE_MODE: "enable",
|
||||
TELEMETRY_ENABLED: true,
|
||||
EXPAND_NAVIGATION: false,
|
||||
SIDEBAR: true,
|
||||
|
||||
@@ -4,38 +4,13 @@
|
||||
<div class="md:grid md:grid-cols-3 md:gap-4">
|
||||
<div class="p-8 md:col-span-1">
|
||||
<h3 class="heading">
|
||||
{{ t("settings.theme") }}
|
||||
{{ t("settings.general") }}
|
||||
</h3>
|
||||
<p class="my-1 text-secondaryLight">
|
||||
{{ t("settings.theme_description") }}
|
||||
{{ t("settings.general_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-8 p-8 md:col-span-2">
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.background") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t(getColorModeName(colorMode.preference)) }}
|
||||
<span v-if="colorMode.preference === 'system'">
|
||||
({{ t(getColorModeName(colorMode.value)) }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SmartColorModePicker />
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.accent_color") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ ACCENT_COLOR.charAt(0).toUpperCase() + ACCENT_COLOR.slice(1) }}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SmartAccentModePicker />
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.language") }}
|
||||
@@ -44,6 +19,19 @@
|
||||
<SmartChangeLanguage />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.query_parameters_encoding") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.query_parameters_encoding_description") }}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SmartEncodingPicker />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.experiments") }}
|
||||
@@ -96,6 +84,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:grid md:grid-cols-3 md:gap-4">
|
||||
<div class="p-8 md:col-span-1">
|
||||
<h3 class="heading">
|
||||
{{ t("settings.theme") }}
|
||||
</h3>
|
||||
<p class="my-1 text-secondaryLight">
|
||||
{{ t("settings.theme_description") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-8 p-8 md:col-span-2">
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.background") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t(getColorModeName(colorMode.preference)) }}
|
||||
<span v-if="colorMode.preference === 'system'">
|
||||
({{ t(getColorModeName(colorMode.value)) }})
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SmartColorModePicker />
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.accent_color") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ ACCENT_COLOR.charAt(0).toUpperCase() + ACCENT_COLOR.slice(1) }}
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<SmartAccentModePicker />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md:grid md:grid-cols-3 md:gap-4">
|
||||
<div class="p-8 md:col-span-1">
|
||||
<h3 class="heading">
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
RequestRunResult,
|
||||
} from "~/services/interceptor.service"
|
||||
import { Service } from "dioc"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { ref, watch } from "vue"
|
||||
import { z } from "zod"
|
||||
@@ -24,6 +23,7 @@ import { UIExtensionService } from "~/services/ui-extension.service"
|
||||
import { x25519 } from "@noble/curves/ed25519"
|
||||
import { base16 } from "@scure/base"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { preProcessRequest } from "../helpers"
|
||||
|
||||
type KeyValuePair = {
|
||||
key: string
|
||||
@@ -98,33 +98,6 @@ type RunRequestResponse = {
|
||||
// and the axios present in this package
|
||||
type AxiosRequestConfig = Parameters<Interceptor["runRequest"]>[0]
|
||||
|
||||
export const preProcessRequest = (
|
||||
req: AxiosRequestConfig
|
||||
): AxiosRequestConfig => {
|
||||
const reqClone = cloneDeep(req)
|
||||
|
||||
// If the parameters are URLSearchParams, inject them to URL instead
|
||||
// This prevents issues of marshalling the URLSearchParams to the proxy
|
||||
if (reqClone.params instanceof URLSearchParams) {
|
||||
try {
|
||||
const url = new URL(reqClone.url ?? "")
|
||||
|
||||
for (const [key, value] of reqClone.params.entries()) {
|
||||
url.searchParams.append(key, value)
|
||||
}
|
||||
|
||||
reqClone.url = url.toString()
|
||||
} catch (e) {
|
||||
// making this a non-empty block, so we can make the linter happy.
|
||||
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
|
||||
}
|
||||
|
||||
reqClone.params = {}
|
||||
}
|
||||
|
||||
return reqClone
|
||||
}
|
||||
|
||||
async function processBody(
|
||||
axiosReq: AxiosRequestConfig
|
||||
): Promise<BodyDef | null> {
|
||||
|
||||
@@ -6,34 +6,7 @@ import {
|
||||
RequestRunResult,
|
||||
} from "../../../services/interceptor.service"
|
||||
import axios, { AxiosRequestConfig, CancelToken } from "axios"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
export const preProcessRequest = (
|
||||
req: AxiosRequestConfig
|
||||
): AxiosRequestConfig => {
|
||||
const reqClone = cloneDeep(req)
|
||||
|
||||
// If the parameters are URLSearchParams, inject them to URL instead
|
||||
// This prevents issues of marshalling the URLSearchParams to the proxy
|
||||
if (reqClone.params instanceof URLSearchParams) {
|
||||
try {
|
||||
const url = new URL(reqClone.url ?? "")
|
||||
|
||||
for (const [key, value] of reqClone.params.entries()) {
|
||||
url.searchParams.append(key, value)
|
||||
}
|
||||
|
||||
reqClone.url = url.toString()
|
||||
} catch (e) {
|
||||
// making this a non-empty block, so we can make the linter happy.
|
||||
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
|
||||
}
|
||||
|
||||
reqClone.params = {}
|
||||
}
|
||||
|
||||
return reqClone
|
||||
}
|
||||
import { preProcessRequest } from "./helpers"
|
||||
|
||||
async function runRequest(
|
||||
req: AxiosRequestConfig,
|
||||
@@ -41,11 +14,9 @@ async function runRequest(
|
||||
): RequestRunResult["response"] {
|
||||
const timeStart = Date.now()
|
||||
|
||||
const processedReq = preProcessRequest(req)
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
...processedReq,
|
||||
...req,
|
||||
cancelToken,
|
||||
responseType: "arraybuffer",
|
||||
})
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
InterceptorError,
|
||||
RequestRunResult,
|
||||
} from "~/services/interceptor.service"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { computed, readonly, ref } from "vue"
|
||||
import { browserIsChrome, browserIsFirefox } from "~/helpers/utils/userAgent"
|
||||
import SettingsExtension from "~/components/settings/Extension.vue"
|
||||
import InterceptorsExtensionSubtitle from "~/components/interceptors/ExtensionSubtitle.vue"
|
||||
import InterceptorsErrorPlaceholder from "~/components/interceptors/ErrorPlaceholder.vue"
|
||||
import { until } from "@vueuse/core"
|
||||
import { preProcessRequest } from "./helpers"
|
||||
|
||||
export const defineSubscribableObject = <T extends object>(obj: T) => {
|
||||
const proxyObject = {
|
||||
@@ -55,31 +55,6 @@ export const cancelRunningExtensionRequest = () => {
|
||||
window.__POSTWOMAN_EXTENSION_HOOK__?.cancelRequest()
|
||||
}
|
||||
|
||||
const preProcessRequest = (req: AxiosRequestConfig): AxiosRequestConfig => {
|
||||
const reqClone = cloneDeep(req)
|
||||
|
||||
// If the parameters are URLSearchParams, inject them to URL instead
|
||||
// This prevents marshalling issues with structured cloning of URLSearchParams
|
||||
if (reqClone.params instanceof URLSearchParams) {
|
||||
try {
|
||||
const url = new URL(reqClone.url ?? "")
|
||||
|
||||
for (const [key, value] of reqClone.params.entries()) {
|
||||
url.searchParams.append(key, value)
|
||||
}
|
||||
|
||||
reqClone.url = url.toString()
|
||||
} catch (e) {
|
||||
// making this a non-empty block, so we can make the linter happy.
|
||||
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
|
||||
}
|
||||
|
||||
reqClone.params = {}
|
||||
}
|
||||
|
||||
return reqClone
|
||||
}
|
||||
|
||||
export type ExtensionStatus = "available" | "unknown-origin" | "waiting"
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { AxiosRequestConfig } from "axios"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { useSetting } from "~/composables/settings"
|
||||
|
||||
// Helper function to check if a string is already encoded
|
||||
const isEncoded = (value: string) => {
|
||||
try {
|
||||
return value !== decodeURIComponent(value)
|
||||
} catch (e) {
|
||||
return false // in case of malformed URI sequence
|
||||
}
|
||||
}
|
||||
|
||||
export const preProcessRequest = (
|
||||
req: AxiosRequestConfig
|
||||
): AxiosRequestConfig => {
|
||||
const reqClone = cloneDeep(req)
|
||||
const encodeMode = useSetting("ENCODE_MODE")
|
||||
|
||||
// If the parameters are URLSearchParams, inject them to URL instead
|
||||
// This prevents issues of marshalling the URLSearchParams to the proxy
|
||||
if (reqClone.params instanceof URLSearchParams) {
|
||||
try {
|
||||
const url = new URL(reqClone.url ?? "")
|
||||
|
||||
for (const [key, value] of reqClone.params.entries()) {
|
||||
let finalValue = value
|
||||
if (
|
||||
encodeMode.value === "enable" ||
|
||||
(encodeMode.value === "auto" &&
|
||||
/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(value))
|
||||
) {
|
||||
// Check if the value is already encoded (e.g., contains % symbols)
|
||||
if (!isEncoded(value)) {
|
||||
finalValue = encodeURIComponent(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the parameter with the final value
|
||||
url.searchParams.append(key, finalValue)
|
||||
}
|
||||
|
||||
// decode the URL to prevent double encoding
|
||||
reqClone.url = decodeURIComponent(url.toString())
|
||||
} catch (e) {
|
||||
// making this a non-empty block, so we can make the linter happy.
|
||||
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
|
||||
}
|
||||
|
||||
reqClone.params = {}
|
||||
}
|
||||
|
||||
return reqClone
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Interceptor, RequestRunResult } from "~/services/interceptor.service"
|
||||
import { AxiosRequestConfig, CancelToken } from "axios"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { preProcessRequest } from "./browser"
|
||||
import { preProcessRequest } from "./helpers"
|
||||
import { v4 } from "uuid"
|
||||
import axios from "axios"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
|
||||
@@ -26,6 +26,8 @@ const ThemeColorSchema = z.enum([
|
||||
|
||||
const BgColorSchema = z.enum(["system", "light", "dark", "black"])
|
||||
|
||||
const EncodeMode = z.enum(["enable", "disable", "auto"])
|
||||
|
||||
const SettingsDefSchema = z.object({
|
||||
syncCollections: z.boolean(),
|
||||
syncHistory: z.boolean(),
|
||||
@@ -41,6 +43,7 @@ const SettingsDefSchema = z.object({
|
||||
}),
|
||||
THEME_COLOR: ThemeColorSchema,
|
||||
BG_COLOR: BgColorSchema,
|
||||
ENCODE_MODE: EncodeMode.catch("enable"),
|
||||
TELEMETRY_ENABLED: z.boolean(),
|
||||
EXPAND_NAVIGATION: z.boolean(),
|
||||
SIDEBAR: z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user