feat(common): retry import openapi from url with proxy interceptor on network error (#5225)
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
parent
3452e721fa
commit
0b605fe9cb
3 changed files with 153 additions and 35 deletions
|
|
@ -669,7 +669,14 @@
|
|||
"import_summary_responses_title": "Responses",
|
||||
"import_summary_pre_request_scripts_title": "Pre-request scripts",
|
||||
"import_summary_post_request_scripts_title": "Post request scripts",
|
||||
"import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now."
|
||||
"import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now.",
|
||||
"cors_error_modal": {
|
||||
"title": "CORS Error Detected",
|
||||
"description": "The import failed due to CORS (Cross-Origin Resource Sharing) restrictions imposed by the server.",
|
||||
"explanation": "This is a security feature that prevents web pages from making requests to different domains. You can retry using our proxy service to bypass this restriction.",
|
||||
"url_label": "Attempted URL",
|
||||
"retry_with_proxy": "Retry with Proxy"
|
||||
}
|
||||
},
|
||||
"instances": {
|
||||
"switch": "Switch Hoppscotch Instance",
|
||||
|
|
|
|||
53
packages/hoppscotch-common/src/components.d.ts
vendored
53
packages/hoppscotch-common/src/components.d.ts
vendored
|
|
@ -1,11 +1,11 @@
|
|||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
|
||||
export {}
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AccessTokens: typeof import('./components/accessTokens/index.vue')['default']
|
||||
AccessTokensGenerateModal: typeof import('./components/accessTokens/GenerateModal.vue')['default']
|
||||
|
|
@ -124,6 +124,31 @@ declare module '@vue/runtime-core' {
|
|||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||
HistoryPersonal: typeof import('./components/history/Personal.vue')['default']
|
||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
|
||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
|
||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||
HttpAuthorizationAkamaiEG: typeof import('./components/http/authorization/AkamaiEG.vue')['default']
|
||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||
|
|
@ -179,7 +204,24 @@ declare module '@vue/runtime-core' {
|
|||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||
HttpTestTestResult: typeof import('./components/http/test/TestResult.vue')['default']
|
||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
||||
ImportExportCorsErrorModal: typeof import('./components/importExport/CorsErrorModal.vue')['default']
|
||||
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
||||
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
|
||||
ImportExportImportExportStepsAllCollectionImport: typeof import('./components/importExport/ImportExportSteps/AllCollectionImport.vue')['default']
|
||||
|
|
@ -248,5 +290,4 @@ declare module '@vue/runtime-core' {
|
|||
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
|
||||
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
<template>
|
||||
<div class="space-y-4">
|
||||
<div v-if="showCorsError">
|
||||
<div class="flex items-start space-x-4">
|
||||
<icon-lucide-alert-triangle
|
||||
class="text-yellow-500 flex-shrink-0 mt-1"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-secondaryDark">
|
||||
{{ t("import.cors_error_modal.description") }}
|
||||
</p>
|
||||
<p class="text-secondaryLight mt-2">
|
||||
{{ t("import.cors_error_modal.explanation") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||
|
|
@ -33,29 +48,34 @@
|
|||
<div>
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:label="
|
||||
showCorsError
|
||||
? t('import.cors_error_modal.retry_with_proxy')
|
||||
: t('import.title')
|
||||
"
|
||||
:disabled="disableImportCTA"
|
||||
:loading="isFetchingUrl || loading"
|
||||
@click="fetchUrlData"
|
||||
@click="showCorsError ? retryWithProxy() : fetchUrlData()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { parseBodyAsJSONOrYAML } from "~/helpers/functional/json"
|
||||
import { ProxyKernelInterceptorService } from "~/platform/std/kernel-interceptors/proxy"
|
||||
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
|
||||
|
||||
const interceptorService = useService(KernelInterceptorService)
|
||||
const proxyInterceptorService = useService(ProxyKernelInterceptorService)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = withDefaults(
|
||||
|
|
@ -74,8 +94,8 @@ const emit = defineEmits<{
|
|||
|
||||
const inputChooseGistToImportFrom = ref<string>("")
|
||||
const hasURL = ref(false)
|
||||
|
||||
const isFetchingUrl = ref(false)
|
||||
const showCorsError = ref(false)
|
||||
|
||||
watch(inputChooseGistToImportFrom, (url) => {
|
||||
hasURL.value = !!url
|
||||
|
|
@ -83,9 +103,22 @@ watch(inputChooseGistToImportFrom, (url) => {
|
|||
|
||||
const disableImportCTA = computed(() => !hasURL.value || props.loading)
|
||||
|
||||
const isCorsError = (error: any): boolean => {
|
||||
// Check for common CORS error patterns
|
||||
return (
|
||||
error?.kind === "network" ||
|
||||
error?.message?.includes("CORS") ||
|
||||
error?.message?.includes("Access to fetch") ||
|
||||
error?.message?.includes("Cross-Origin") ||
|
||||
error?.code === "ERR_NETWORK" ||
|
||||
error?.name === "TypeError"
|
||||
)
|
||||
}
|
||||
|
||||
const urlFetchLogic =
|
||||
props.fetchLogic ??
|
||||
async function (url: string) {
|
||||
try {
|
||||
const { response } = interceptorService.execute({
|
||||
id: Date.now(),
|
||||
url: url,
|
||||
|
|
@ -95,32 +128,69 @@ const urlFetchLogic =
|
|||
|
||||
const res = await response
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
return E.left("REQUEST_FAILED")
|
||||
}
|
||||
|
||||
if (E.isRight(res)) {
|
||||
const responsePayload = parseBodyAsJSONOrYAML<unknown>(res.right.body)
|
||||
|
||||
if (O.isSome(responsePayload)) {
|
||||
// stringify the response payload
|
||||
return E.right(JSON.stringify(responsePayload.value))
|
||||
}
|
||||
|
||||
return E.left("REQUEST_FAILED")
|
||||
}
|
||||
|
||||
// Return the actual error from the failed request
|
||||
return E.left(E.isLeft(res) ? res.left : "REQUEST_FAILED")
|
||||
} catch (error) {
|
||||
// Return the caught error for proper CORS detection
|
||||
return E.left(error)
|
||||
}
|
||||
}
|
||||
|
||||
const retryWithProxy = async () => {
|
||||
isFetchingUrl.value = true
|
||||
|
||||
try {
|
||||
// Store the current interceptor to restore later
|
||||
const previousInterceptorId = interceptorService.getCurrentId()
|
||||
|
||||
// Switch to proxy interceptor
|
||||
interceptorService.setActive(proxyInterceptorService.id)
|
||||
|
||||
// Retry the request with proxy
|
||||
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
|
||||
|
||||
// Restore previous interceptor
|
||||
if (previousInterceptorId) {
|
||||
interceptorService.setActive(previousInterceptorId)
|
||||
}
|
||||
|
||||
if (E.isRight(res)) {
|
||||
showCorsError.value = false
|
||||
emit("importFromURL", res.right)
|
||||
} else {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("import.failed"))
|
||||
} finally {
|
||||
isFetchingUrl.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUrlData() {
|
||||
isFetchingUrl.value = true
|
||||
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
// @ts-ignore
|
||||
if (isCorsError(res.left?.error)) {
|
||||
showCorsError.value = true
|
||||
} else {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
isFetchingUrl.value = false
|
||||
return
|
||||
}
|
||||
|
||||
emit("importFromURL", res.right)
|
||||
|
||||
isFetchingUrl.value = false
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue