fix: wire response + init error handling

This commit is contained in:
liyasthomas
2021-07-15 09:40:45 +05:30
parent 86c9e09782
commit 3ef8e677c7
16 changed files with 283 additions and 152 deletions

View File

@@ -1,44 +1,59 @@
<template> <template>
<div class="flex bg-primary border-b border-dividerLight"> <div class="flex bg-primary border-b justify-between border-dividerLight">
<SmartLink <span class="flex">
to="https://forms.gle/8yFiEynXB7h477Ns6" <SmartLink
blank to="https://forms.gle/8yFiEynXB7h477Ns6"
class=" blank
relative
flex
items-center
justify-center
px-4
py-3
transition
group
"
>
<i class="material-icons mr-4">science</i>
<span class="text-secondaryDark text-xs">
<span class="md:hidden"> Beta Layout </span>
<span class="hidden md:inline">
You're currently viewing experimental beta layout
</span>
</span>
<span
class=" class="
relative
flex flex
items-center items-center
justify-center justify-center
pl-4 px-4
ml-4 py-3
font-semibold
transition transition
border-l group
group-hover:text-accentDark
border-divider
text-accent text-xs
" "
> >
<span class="md:hidden"> Give Feedback </span> <i class="material-icons mr-4">science</i>
<span class="hidden md:inline"> Report a problem </span> <span class="text-secondaryDark text-xs">
</span> <span class="md:hidden"> Beta Layout </span>
</SmartLink> <span class="hidden md:inline">
You're currently viewing an experimental beta layout
</span>
</span>
<span
class="
flex
items-center
justify-center
pl-4
ml-4
font-semibold
transition
border-l
group-hover:text-accentDark
border-divider
text-accent text-xs
"
>
<span class="md:hidden"> Give Feedback </span>
<span class="hidden md:inline"> Report a problem </span>
</span>
</SmartLink>
<SmartLink
to="https://hoppscotch.io"
class="flex items-center justify-center transition group"
>
<span class="text-secondaryDark text-xs">
Switch back to the Hoppscotch website
</span>
</SmartLink>
</span>
<ButtonSecondary
v-tippy="{ theme: 'tooltip' }"
icon="close"
:title="$t('close')"
/>
</div> </div>
</template> </template>

View File

@@ -418,45 +418,45 @@ export default {
}) })
}, },
parsePostmanCollection({ info, name, item }) { parsePostmanCollection({ info, name, item }) {
const postwomanCollection = { const hoppscotchCollection = {
name: "", name: "",
folders: [], folders: [],
requests: [], requests: [],
} }
postwomanCollection.name = info ? info.name : name hoppscotchCollection.name = info ? info.name : name
if (item && item.length > 0) { if (item && item.length > 0) {
for (const collectionItem of item) { for (const collectionItem of item) {
if (collectionItem.request) { if (collectionItem.request) {
if ( if (
Object.prototype.hasOwnProperty.call( Object.prototype.hasOwnProperty.call(
postwomanCollection, hoppscotchCollection,
"folders" "folders"
) )
) { ) {
postwomanCollection.name = info ? info.name : name hoppscotchCollection.name = info ? info.name : name
postwomanCollection.requests.push( hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem) this.parsePostmanRequest(collectionItem)
) )
} else { } else {
postwomanCollection.name = name || "" hoppscotchCollection.name = name || ""
postwomanCollection.requests.push( hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem) this.parsePostmanRequest(collectionItem)
) )
} }
} else if (this.hasFolder(collectionItem)) { } else if (this.hasFolder(collectionItem)) {
postwomanCollection.folders.push( hoppscotchCollection.folders.push(
this.parsePostmanCollection(collectionItem) this.parsePostmanCollection(collectionItem)
) )
} else { } else {
postwomanCollection.requests.push( hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem) this.parsePostmanRequest(collectionItem)
) )
} }
} }
} }
return postwomanCollection return hoppscotchCollection
}, },
parsePostmanRequest({ name, request }) { parsePostmanRequest({ name, request }) {
const pwRequest = { const pwRequest = {

View File

@@ -271,45 +271,45 @@ export default {
}) })
}, },
parsePostmanCollection({ info, name, item }) { parsePostmanCollection({ info, name, item }) {
const postwomanCollection = { const hoppscotchCollection = {
name: "", name: "",
folders: [], folders: [],
requests: [], requests: [],
} }
postwomanCollection.name = info ? info.name : name hoppscotchCollection.name = info ? info.name : name
if (item && item.length > 0) { if (item && item.length > 0) {
for (const collectionItem of item) { for (const collectionItem of item) {
if (collectionItem.request) { if (collectionItem.request) {
if ( if (
Object.prototype.hasOwnProperty.call( Object.prototype.hasOwnProperty.call(
postwomanCollection, hoppscotchCollection,
"folders" "folders"
) )
) { ) {
postwomanCollection.name = info ? info.name : name hoppscotchCollection.name = info ? info.name : name
postwomanCollection.requests.push( hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem) this.parsePostmanRequest(collectionItem)
) )
} else { } else {
postwomanCollection.name = name || "" hoppscotchCollection.name = name || ""
postwomanCollection.requests.push( hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem) this.parsePostmanRequest(collectionItem)
) )
} }
} else if (this.hasFolder(collectionItem)) { } else if (this.hasFolder(collectionItem)) {
postwomanCollection.folders.push( hoppscotchCollection.folders.push(
this.parsePostmanCollection(collectionItem) this.parsePostmanCollection(collectionItem)
) )
} else { } else {
postwomanCollection.requests.push( hoppscotchCollection.requests.push(
this.parsePostmanRequest(collectionItem) this.parsePostmanRequest(collectionItem)
) )
} }
} }
} }
return postwomanCollection return hoppscotchCollection
}, },
parsePostmanRequest({ name, request }) { parsePostmanRequest({ name, request }) {
const pwRequest = { const pwRequest = {

View File

@@ -204,13 +204,13 @@ export default {
) { ) {
this.importFromPostman(importFileObj) this.importFromPostman(importFileObj)
} else { } else {
this.importFromPostwoman(importFileObj) this.importFromHoppscotch(importFileObj)
} }
} }
reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0]) reader.readAsText(this.$refs.inputChooseFileToImportFrom.files[0])
this.$refs.inputChooseFileToImportFrom.value = "" this.$refs.inputChooseFileToImportFrom.value = ""
}, },
importFromPostwoman(environments) { importFromHoppscotch(environments) {
appendEnvironments(environments) appendEnvironments(environments)
this.fileImported() this.fileImported()
}, },
@@ -220,7 +220,7 @@ export default {
environment.variables.push({ key, value }) environment.variables.push({ key, value })
) )
const environments = [environment] const environments = [environment]
this.importFromPostwoman(environments) this.importFromHoppscotch(environments)
}, },
exportJSON() { exportJSON() {
let text = this.environmentJson let text = this.environmentJson

View File

@@ -92,6 +92,7 @@ import {
deleteGraphqlHistoryEntry, deleteGraphqlHistoryEntry,
deleteRESTHistoryEntry, deleteRESTHistoryEntry,
} from "~/newstore/history" } from "~/newstore/history"
import { setRESTRequest } from "~/newstore/RESTSession"
export default { export default {
props: { props: {
@@ -133,7 +134,7 @@ export default {
}) })
}, },
useHistory(entry) { useHistory(entry) {
this.$emit("useHistory", entry) setRESTRequest(entry)
}, },
deleteHistory(entry) { deleteHistory(entry) {
if (this.page === "rest") deleteRESTHistoryEntry(entry) if (this.page === "rest") deleteRESTHistoryEntry(entry)

View File

@@ -36,7 +36,7 @@
@click="$emit('use-entry')" @click="$emit('use-entry')"
> >
<span class="truncate"> <span class="truncate">
{{ `${entry.url}${entry.path}` }} {{ `${entry.endpoint}` }}
</span> </span>
</span> </span>
<ButtonSecondary <ButtonSecondary
@@ -70,13 +70,15 @@ export default {
}, },
computed: { computed: {
duration() { duration() {
const { duration } = this.entry if (this.entry.meta.responseDuration) {
return duration > 0 const responseDuration = this.entry.meta.responseDuration
? `${this.$t("duration")}: ${duration}ms` return responseDuration > 0
: this.$t("no_duration") ? `${this.$t("duration")}: ${responseDuration}ms`
: this.$t("no_duration")
} else return this.$t("no_duration")
}, },
entryStatus() { entryStatus() {
const foundStatusGroup = findStatusGroup(this.entry.status) const foundStatusGroup = findStatusGroup(this.entry.statusCode)
return ( return (
foundStatusGroup || { foundStatusGroup || {
className: "", className: "",

View File

@@ -49,9 +49,9 @@
:spellcheck="false" :spellcheck="false"
:value="header.key" :value="header.key"
autofocus autofocus
@change=" @input="
updateHeader(index, { updateHeader(index, {
key: $event.target.value, key: $event,
value: header.value, value: header.value,
active: header.active, active: header.active,
}) })

View File

@@ -1,6 +1,6 @@
<template> <template>
<AppSection label="response"> <AppSection label="response">
<HttpResponseMeta v-if="!loading" :response="response" /> <HttpResponseMeta :response="response" />
<LensesResponseBodyRenderer v-if="!loading" :response="response" /> <LensesResponseBodyRenderer v-if="!loading" :response="response" />
</AppSection> </AppSection>
</template> </template>

View File

@@ -1,33 +1,39 @@
<template> <template>
<div <div>
class=" <span v-if="response == null">
flex {{ $t("waiting_send_req") }}
sticky </span>
top-0 <div
z-10 v-else
bg-primary class="
items-center flex
p-4 sticky
font-mono font-semibold top-0
space-x-8 z-10
" bg-primary
> items-center
<i v-if="response.type === 'loading'" class="animate-spin material-icons"> p-4
refresh font-mono font-semibold
</i> space-x-8
<div v-else :class="statusCategory.className"> "
<span v-if="response.statusCode"> >
<span class="text-secondaryDark"> Status: </span> <i v-if="response.type === 'loading'" class="animate-spin material-icons">
{{ response.statusCode || $t("waiting_send_req") }} refresh
</span> </i>
<span v-if="response.meta.responseDuration" class="text-xs"> <div v-else :class="statusCategory.className">
<span class="text-secondaryDark"> Time: </span> <span v-if="response.statusCode">
{{ `${response.meta.responseDuration} ms` }} <span class="text-secondaryDark"> Status: </span>
</span> {{ response.statusCode || $t("waiting_send_req") }}
<span v-if="response.meta.responseSize" class="text-xs"> </span>
<span class="text-secondaryDark"> Size: </span> <span v-if="response.meta.responseDuration" class="text-xs">
{{ `${response.meta.responseSize} B` }} <span class="text-secondaryDark"> Time: </span>
</span> {{ `${response.meta.responseDuration} ms` }}
</span>
<span v-if="response.meta.responseSize" class="text-xs">
<span class="text-secondaryDark"> Size: </span>
{{ `${response.meta.responseSize} B` }}
</span>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -194,7 +194,7 @@ export default {
this.client = new Paho.Client( this.client = new Paho.Client(
parseUrl.hostname, parseUrl.hostname,
parseUrl.port !== "" ? Number(parseUrl.port) : 8081, parseUrl.port !== "" ? Number(parseUrl.port) : 8081,
"postwoman" "hoppscotch"
) )
this.client.connect({ this.client.connect({
onSuccess: this.onConnectionSuccess, onSuccess: this.onConnectionSuccess,

View File

@@ -51,7 +51,10 @@ export const sendNetworkRequest = (req: any) =>
export function createRESTNetworkRequestStream( export function createRESTNetworkRequestStream(
req: EffectiveHoppRESTRequest req: EffectiveHoppRESTRequest
): Observable<HoppRESTResponse> { ): Observable<HoppRESTResponse> {
const response = new BehaviorSubject<HoppRESTResponse>({ type: "loading" }) const response = new BehaviorSubject<HoppRESTResponse>({
type: "loading",
req,
})
const headers = req.effectiveFinalHeaders.reduce((acc, { key, value }) => { const headers = req.effectiveFinalHeaders.reduce((acc, { key, value }) => {
return Object.assign(acc, { [key]: value }) return Object.assign(acc, { [key]: value })
@@ -60,32 +63,73 @@ export function createRESTNetworkRequestStream(
const timeStart = Date.now() const timeStart = Date.now()
runAppropriateStrategy({ runAppropriateStrategy({
method: req.method as any,
url: req.effectiveFinalURL, url: req.effectiveFinalURL,
headers, headers,
}).then((res: any) => {
const timeEnd = Date.now()
const contentLength = res.headers["content-length"]
? parseInt(res.headers["content-length"])
: (res.data as ArrayBuffer).byteLength
const resObj: HoppRESTResponse = {
type: "success",
statusCode: res.status,
body: res.data,
headers: Object.keys(res.headers).map((x) => ({
key: x,
value: res.headers[x],
})),
meta: {
responseSize: contentLength,
responseDuration: timeEnd - timeStart,
},
}
response.next(resObj)
response.complete()
}) })
.then((res: any) => {
const timeEnd = Date.now()
const contentLength = res.headers["content-length"]
? parseInt(res.headers["content-length"])
: (res.data as ArrayBuffer).byteLength
const resObj: HoppRESTResponse = {
type: "success",
statusCode: res.status,
body: res.data,
headers: Object.keys(res.headers).map((x) => ({
key: x,
value: res.headers[x],
})),
meta: {
responseSize: contentLength,
responseDuration: timeEnd - timeStart,
},
req,
}
response.next(resObj)
response.complete()
})
.catch((err) => {
if (err.response) {
const timeEnd = Date.now()
const contentLength = err.response.headers["content-length"]
? parseInt(err.response.headers["content-length"])
: (err.response.data as ArrayBuffer).byteLength
const resObj: HoppRESTResponse = {
type: "fail",
body: err.response.data,
headers: Object.keys(err.response.headers).map((x) => ({
key: x,
value: err.response.headers[x],
})),
meta: {
responseDuration: timeEnd - timeStart,
responseSize: contentLength,
},
req,
statusCode: err.response.status,
}
response.next(resObj)
response.complete()
} else {
const resObj: HoppRESTResponse = {
type: "network_fail",
error: err,
req,
}
response.next(resObj)
response.complete()
}
})
return response return response
} }

View File

@@ -1,3 +1,5 @@
export const RESTReqSchemaVersion = "1"
export type HoppRESTParam = { export type HoppRESTParam = {
key: string key: string
value: string value: string
@@ -11,8 +13,44 @@ export type HoppRESTHeader = {
} }
export interface HoppRESTRequest { export interface HoppRESTRequest {
v: string
method: string method: string
endpoint: string endpoint: string
params: HoppRESTParam[] params: HoppRESTParam[]
headers: HoppRESTHeader[] headers: HoppRESTHeader[]
} }
export function isHoppRESTRequest(x: any): x is HoppRESTRequest {
return x && typeof x === "object" && "v" in x
}
export function translateToNewRequest(x: any): HoppRESTRequest {
if (isHoppRESTRequest(x)) {
return x
} else {
// Old format
const endpoint: string = `${x.url}${x.path}`
const headers: HoppRESTHeader[] = x.headers
// Remove old keys from params
const params: HoppRESTParam[] = (x.params as any[]).map(
({ key, value, active }) => ({
key,
value,
active,
})
)
const method = x.method
return {
endpoint,
headers,
params,
method,
v: RESTReqSchemaVersion,
}
}
}

View File

@@ -1,14 +1,25 @@
import { HoppRESTRequest } from "./HoppRESTRequest"
export type HoppRESTResponse = export type HoppRESTResponse =
| { type: "loading" } | { type: "loading"; req: HoppRESTRequest }
| { | {
type: "fail" type: "fail"
headers: { key: string; value: string }[] headers: { key: string; value: string }[]
body: ArrayBuffer body: ArrayBuffer
statusCode: number statusCode: number
meta: {
responseSize: number // in bytes
responseDuration: number // in millis
}
req: HoppRESTRequest
} }
| { | {
type: "network_fail" type: "network_fail"
error: Error error: Error
req: HoppRESTRequest
} }
| { | {
type: "success" type: "success"
@@ -19,4 +30,6 @@ export type HoppRESTResponse =
responseSize: number // in bytes responseSize: number // in bytes
responseDuration: number // in millis responseDuration: number // in millis
} }
req: HoppRESTRequest
} }

View File

@@ -1,9 +1,10 @@
import { pluck, distinctUntilChanged, map } from "rxjs/operators" import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore" import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { import {
HoppRESTHeader, HoppRESTHeader,
HoppRESTParam, HoppRESTParam,
HoppRESTRequest, HoppRESTRequest,
RESTReqSchemaVersion,
} from "~/helpers/types/HoppRESTRequest" } from "~/helpers/types/HoppRESTRequest"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
@@ -115,6 +116,7 @@ type RESTSession = {
const defaultRESTSession: RESTSession = { const defaultRESTSession: RESTSession = {
request: { request: {
v: RESTReqSchemaVersion,
endpoint: "https://httpbin.org/get", endpoint: "https://httpbin.org/get",
params: [], params: [],
headers: [], headers: [],
@@ -124,6 +126,11 @@ const defaultRESTSession: RESTSession = {
} }
const dispatchers = defineDispatchers({ const dispatchers = defineDispatchers({
setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
return {
request: req,
}
},
setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) { setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
const paramsInNewURL = getParamsInURL(newEndpoint) const paramsInNewURL = getParamsInURL(newEndpoint)
const updatedParams = recalculateParams( const updatedParams = recalculateParams(
@@ -297,6 +304,15 @@ const dispatchers = defineDispatchers({
const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers) const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
export function setRESTRequest(req: HoppRESTRequest) {
restSessionStore.dispatch({
dispatcher: "setRequest",
payload: {
req,
},
})
}
export function setRESTEndpoint(newEndpoint: string) { export function setRESTEndpoint(newEndpoint: string) {
restSessionStore.dispatch({ restSessionStore.dispatch({
dispatcher: "setEndpoint", dispatcher: "setEndpoint",
@@ -438,3 +454,10 @@ export const restResponse$ = restSessionStore.subject$.pipe(
pluck("response"), pluck("response"),
distinctUntilChanged() distinctUntilChanged()
) )
export const completedRESTResponse$ = restResponse$.pipe(
filter(
(res) =>
res !== null && res.type !== "loading" && res.type !== "network_fail"
)
)

View File

@@ -1,6 +1,7 @@
import eq from "lodash/eq" import eq from "lodash/eq"
import { pluck } from "rxjs/operators" import { pluck } from "rxjs/operators"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore" import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { completedRESTResponse$ } from "./RESTSession"
export const defaultRESTHistoryState = { export const defaultRESTHistoryState = {
state: [] as any[], state: [] as any[],
@@ -136,3 +137,18 @@ export function toggleGraphqlHistoryEntryStar(entry: any) {
payload: { entry }, payload: { entry },
}) })
} }
// Listen to completed responses to add to history
completedRESTResponse$.subscribe((res) => {
if (res !== null) {
if (res.type === "loading" || res.type === "network_fail") return
addRESTHistoryEntry({
...res.req,
type: res.type,
meta: res.meta,
statusCode: res.statusCode,
star: false,
})
}
})

View File

@@ -462,11 +462,7 @@
<aside class="h-full"> <aside class="h-full">
<SmartTabs styles="sticky z-10 top-0"> <SmartTabs styles="sticky z-10 top-0">
<SmartTab :id="'history'" :label="$t('history')" :selected="true"> <SmartTab :id="'history'" :label="$t('history')" :selected="true">
<History <History :page="'rest'" ref="historyComponent" />
:page="'rest'"
@useHistory="handleUseHistory"
ref="historyComponent"
/>
</SmartTab> </SmartTab>
<SmartTab :id="'collections'" :label="$t('collections')"> <SmartTab :id="'collections'" :label="$t('collections')">
@@ -767,7 +763,7 @@ export default {
computed: { computed: {
/** /**
* Check content types that can be automatically * Check content types that can be automatically
* serialized by postwoman. * serialized by Hoppscotch.
*/ */
canListParameters() { canListParameters() {
return ( return (
@@ -1157,29 +1153,6 @@ export default {
behavior: "smooth", behavior: "smooth",
}) })
}, },
handleUseHistory(entry) {
this.name = entry.name
this.method = entry.method
this.uri = entry.url + entry.path
this.url = entry.url
this.path = entry.path
this.showPreRequestScript = entry.usesPreScripts
this.preRequestScript = entry.preRequestScript
this.auth = entry.auth
this.httpUser = entry.httpUser
this.httpPassword = entry.httpPassword
this.bearerToken = entry.bearerToken
this.headers = entry.headers
this.params = entry.params
this.bodyParams = entry.bodyParams
this.rawParams = entry.rawParams
this.rawInput = entry.rawInput
this.contentType = entry.contentType
this.requestType = entry.requestType
this.testScript = entry.testScript
this.testsEnabled = entry.usesPostScripts
if (this.SCROLL_INTO_ENABLED) this.scrollInto("request")
},
async makeRequest(auth, headers, requestBody, preRequestScript) { async makeRequest(auth, headers, requestBody, preRequestScript) {
const requestOptions = { const requestOptions = {
method: this.method, method: this.method,