Skip to content

Commit 0b605fe

Browse files
feat(common): retry import openapi from url with proxy interceptor on network error (hoppscotch#5225)
Co-authored-by: nivedin <[email protected]>
1 parent 3452e72 commit 0b605fe

File tree

3 files changed

+150
-32
lines changed

3 files changed

+150
-32
lines changed

packages/hoppscotch-common/locales/en.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,14 @@
669669
"import_summary_responses_title": "Responses",
670670
"import_summary_pre_request_scripts_title": "Pre-request scripts",
671671
"import_summary_post_request_scripts_title": "Post request scripts",
672-
"import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now."
672+
"import_summary_not_supported_by_hoppscotch_import": "We do not support importing {featureLabel} from this source right now.",
673+
"cors_error_modal": {
674+
"title": "CORS Error Detected",
675+
"description": "The import failed due to CORS (Cross-Origin Resource Sharing) restrictions imposed by the server.",
676+
"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.",
677+
"url_label": "Attempted URL",
678+
"retry_with_proxy": "Retry with Proxy"
679+
}
673680
},
674681
"instances": {
675682
"switch": "Switch Hoppscotch Instance",

packages/hoppscotch-common/src/components.d.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
// generated by unplugin-vue-components
2-
// We suggest you to commit this file into source control
1+
/* eslint-disable */
2+
// @ts-nocheck
3+
// Generated by unplugin-vue-components
34
// Read more: https://github.com/vuejs/core/pull/3399
4-
import '@vue/runtime-core'
5-
65
export {}
76

8-
declare module '@vue/runtime-core' {
7+
/* prettier-ignore */
8+
declare module 'vue' {
99
export interface GlobalComponents {
1010
AccessTokens: typeof import('./components/accessTokens/index.vue')['default']
1111
AccessTokensGenerateModal: typeof import('./components/accessTokens/GenerateModal.vue')['default']
@@ -124,6 +124,31 @@ declare module '@vue/runtime-core' {
124124
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
125125
HistoryPersonal: typeof import('./components/history/Personal.vue')['default']
126126
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
127+
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
128+
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
129+
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
130+
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
131+
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
132+
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
133+
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
134+
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
135+
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
136+
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
137+
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
138+
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
139+
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
140+
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
141+
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
142+
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
143+
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
144+
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
145+
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
146+
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
147+
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
148+
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
149+
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
150+
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
151+
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
127152
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
128153
HttpAuthorizationAkamaiEG: typeof import('./components/http/authorization/AkamaiEG.vue')['default']
129154
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
@@ -179,7 +204,24 @@ declare module '@vue/runtime-core' {
179204
HttpTests: typeof import('./components/http/Tests.vue')['default']
180205
HttpTestTestResult: typeof import('./components/http/test/TestResult.vue')['default']
181206
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
207+
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
208+
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
209+
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
210+
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
211+
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
212+
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
213+
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
214+
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
215+
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
216+
IconLucideInfo: typeof import('~icons/lucide/info')['default']
217+
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
218+
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
219+
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
220+
IconLucideSearch: typeof import('~icons/lucide/search')['default']
221+
IconLucideUsers: typeof import('~icons/lucide/users')['default']
222+
IconLucideX: typeof import('~icons/lucide/x')['default']
182223
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
224+
ImportExportCorsErrorModal: typeof import('./components/importExport/CorsErrorModal.vue')['default']
183225
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
184226
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
185227
ImportExportImportExportStepsAllCollectionImport: typeof import('./components/importExport/ImportExportSteps/AllCollectionImport.vue')['default']
@@ -248,5 +290,4 @@ declare module '@vue/runtime-core' {
248290
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
249291
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
250292
}
251-
252293
}

packages/hoppscotch-common/src/components/importExport/ImportExportSteps/UrlImport.vue

Lines changed: 95 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
<template>
22
<div class="space-y-4">
3-
<div>
3+
<div v-if="showCorsError">
4+
<div class="flex items-start space-x-4">
5+
<icon-lucide-alert-triangle
6+
class="text-yellow-500 flex-shrink-0 mt-1"
7+
/>
8+
<div>
9+
<p class="text-secondaryDark">
10+
{{ t("import.cors_error_modal.description") }}
11+
</p>
12+
<p class="text-secondaryLight mt-2">
13+
{{ t("import.cors_error_modal.explanation") }}
14+
</p>
15+
</div>
16+
</div>
17+
</div>
18+
<div v-else>
419
<p class="flex items-center">
520
<span
621
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
@@ -33,29 +48,34 @@
3348
<div>
3449
<HoppButtonPrimary
3550
class="w-full"
36-
:label="t('import.title')"
51+
:label="
52+
showCorsError
53+
? t('import.cors_error_modal.retry_with_proxy')
54+
: t('import.title')
55+
"
3756
:disabled="disableImportCTA"
3857
:loading="isFetchingUrl || loading"
39-
@click="fetchUrlData"
58+
@click="showCorsError ? retryWithProxy() : fetchUrlData()"
4059
/>
4160
</div>
4261
</div>
4362
</template>
4463

4564
<script setup lang="ts">
46-
import { computed, ref, watch } from "vue"
4765
import { useI18n } from "@composables/i18n"
48-
import { useToast } from "~/composables/toast"
49-
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
5066
import { useService } from "dioc/vue"
5167
import * as E from "fp-ts/Either"
5268
import * as O from "fp-ts/Option"
69+
import { computed, ref, watch } from "vue"
70+
import { useToast } from "~/composables/toast"
5371
import { parseBodyAsJSONOrYAML } from "~/helpers/functional/json"
72+
import { ProxyKernelInterceptorService } from "~/platform/std/kernel-interceptors/proxy"
73+
import { KernelInterceptorService } from "~/services/kernel-interceptor.service"
5474
5575
const interceptorService = useService(KernelInterceptorService)
76+
const proxyInterceptorService = useService(ProxyKernelInterceptorService)
5677
5778
const t = useI18n()
58-
5979
const toast = useToast()
6080
6181
const props = withDefaults(
@@ -74,53 +94,103 @@ const emit = defineEmits<{
7494
7595
const inputChooseGistToImportFrom = ref<string>("")
7696
const hasURL = ref(false)
77-
7897
const isFetchingUrl = ref(false)
98+
const showCorsError = ref(false)
7999
80100
watch(inputChooseGistToImportFrom, (url) => {
81101
hasURL.value = !!url
82102
})
83103
84104
const disableImportCTA = computed(() => !hasURL.value || props.loading)
85105
106+
const isCorsError = (error: any): boolean => {
107+
// Check for common CORS error patterns
108+
return (
109+
error?.kind === "network" ||
110+
error?.message?.includes("CORS") ||
111+
error?.message?.includes("Access to fetch") ||
112+
error?.message?.includes("Cross-Origin") ||
113+
error?.code === "ERR_NETWORK" ||
114+
error?.name === "TypeError"
115+
)
116+
}
117+
86118
const urlFetchLogic =
87119
props.fetchLogic ??
88120
async function (url: string) {
89-
const { response } = interceptorService.execute({
90-
id: Date.now(),
91-
url: url,
92-
method: "GET",
93-
version: "HTTP/1.1",
94-
})
121+
try {
122+
const { response } = interceptorService.execute({
123+
id: Date.now(),
124+
url: url,
125+
method: "GET",
126+
version: "HTTP/1.1",
127+
})
128+
129+
const res = await response
130+
131+
if (E.isRight(res)) {
132+
const responsePayload = parseBodyAsJSONOrYAML<unknown>(res.right.body)
133+
134+
if (O.isSome(responsePayload)) {
135+
return E.right(JSON.stringify(responsePayload.value))
136+
}
137+
}
138+
139+
// Return the actual error from the failed request
140+
return E.left(E.isLeft(res) ? res.left : "REQUEST_FAILED")
141+
} catch (error) {
142+
// Return the caught error for proper CORS detection
143+
return E.left(error)
144+
}
145+
}
146+
147+
const retryWithProxy = async () => {
148+
isFetchingUrl.value = true
95149
96-
const res = await response
150+
try {
151+
// Store the current interceptor to restore later
152+
const previousInterceptorId = interceptorService.getCurrentId()
97153
98-
if (E.isLeft(res)) {
99-
return E.left("REQUEST_FAILED")
100-
}
154+
// Switch to proxy interceptor
155+
interceptorService.setActive(proxyInterceptorService.id)
101156
102-
const responsePayload = parseBodyAsJSONOrYAML<unknown>(res.right.body)
157+
// Retry the request with proxy
158+
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
103159
104-
if (O.isSome(responsePayload)) {
105-
// stringify the response payload
106-
return E.right(JSON.stringify(responsePayload.value))
160+
// Restore previous interceptor
161+
if (previousInterceptorId) {
162+
interceptorService.setActive(previousInterceptorId)
107163
}
108164
109-
return E.left("REQUEST_FAILED")
165+
if (E.isRight(res)) {
166+
showCorsError.value = false
167+
emit("importFromURL", res.right)
168+
} else {
169+
toast.error(t("import.failed"))
170+
}
171+
} catch (error) {
172+
toast.error(t("import.failed"))
173+
} finally {
174+
isFetchingUrl.value = false
110175
}
176+
}
111177
112178
async function fetchUrlData() {
113179
isFetchingUrl.value = true
114180
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
115181
116182
if (E.isLeft(res)) {
117-
toast.error(t("import.failed"))
183+
// @ts-ignore
184+
if (isCorsError(res.left?.error)) {
185+
showCorsError.value = true
186+
} else {
187+
toast.error(t("import.failed"))
188+
}
118189
isFetchingUrl.value = false
119190
return
120191
}
121192
122193
emit("importFromURL", res.right)
123-
124194
isFetchingUrl.value = false
125195
}
126196
</script>

0 commit comments

Comments
 (0)