Skip to content

Commit fb7e98a

Browse files
committed
fix(servers): prevent UI crash when chaning Server with variables
Closes #7525
1 parent 62031f3 commit fb7e98a

File tree

5 files changed

+105
-13
lines changed

5 files changed

+105
-13
lines changed

src/core/components/info.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react"
22
import PropTypes from "prop-types"
33
import ImPropTypes from "react-immutable-proptypes"
44
import { sanitizeUrl } from "core/utils"
5-
import { buildUrl } from "core/utils/url"
5+
import { safeBuildUrl } from "core/utils/url"
66

77

88
export class InfoBasePath extends React.Component {
@@ -35,7 +35,7 @@ class Contact extends React.Component {
3535
render(){
3636
let { data, getComponent, selectedServer, url: specUrl} = this.props
3737
let name = data.get("name") || "the developer"
38-
let url = buildUrl(data.get("url"), specUrl, {selectedServer})
38+
let url = safeBuildUrl(data.get("url"), specUrl, {selectedServer})
3939
let email = data.get("email")
4040

4141
const Link = getComponent("Link")
@@ -66,8 +66,8 @@ class License extends React.Component {
6666
let { license, getComponent, selectedServer, url: specUrl } = this.props
6767

6868
const Link = getComponent("Link")
69-
let name = license.get("name") || "License"
70-
let url = buildUrl(license.get("url"), specUrl, {selectedServer})
69+
let name = license.get("name") || "License"
70+
let url = safeBuildUrl(license.get("url"), specUrl, {selectedServer})
7171

7272
return (
7373
<div className="info__license">
@@ -113,11 +113,11 @@ export default class Info extends React.Component {
113113
let version = info.get("version")
114114
let description = info.get("description")
115115
let title = info.get("title")
116-
let termsOfServiceUrl = buildUrl(info.get("termsOfService"), specUrl, {selectedServer})
116+
let termsOfServiceUrl = safeBuildUrl(info.get("termsOfService"), specUrl, {selectedServer})
117117
let contact = info.get("contact")
118118
let license = info.get("license")
119119
let rawExternalDocsUrl = externalDocs && externalDocs.get("url")
120-
let externalDocsUrl = buildUrl(rawExternalDocsUrl, specUrl, {selectedServer})
120+
let externalDocsUrl = safeBuildUrl(rawExternalDocsUrl, specUrl, {selectedServer})
121121
let externalDocsDescription = externalDocs && externalDocs.get("description")
122122

123123
const Markdown = getComponent("Markdown", true)

src/core/components/operation-tag.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PropTypes from "prop-types"
33
import ImPropTypes from "react-immutable-proptypes"
44
import Im from "immutable"
55
import { createDeepLinkPath, escapeDeepLinkPath, sanitizeUrl } from "core/utils"
6-
import { buildUrl } from "core/utils/url"
6+
import { safeBuildUrl } from "core/utils/url"
77
import { isFunc } from "core/utils"
88

99
export default class OperationTag extends React.Component {
@@ -59,7 +59,7 @@ export default class OperationTag extends React.Component {
5959
let rawTagExternalDocsUrl = tagObj.getIn(["tagDetails", "externalDocs", "url"])
6060
let tagExternalDocsUrl
6161
if (isFunc(oas3Selectors) && isFunc(oas3Selectors.selectedServer)) {
62-
tagExternalDocsUrl = buildUrl( rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() } )
62+
tagExternalDocsUrl = safeBuildUrl( rawTagExternalDocsUrl, specUrl, { selectedServer: oas3Selectors.selectedServer() } )
6363
} else {
6464
tagExternalDocsUrl = rawTagExternalDocsUrl
6565
}

src/core/components/operation.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { PureComponent } from "react"
22
import PropTypes from "prop-types"
33
import { getList } from "core/utils"
44
import { getExtensions, sanitizeUrl, escapeDeepLinkPath } from "core/utils"
5-
import { buildUrl } from "core/utils/url"
5+
import { safeBuildUrl } from "core/utils/url"
66
import { Iterable, List } from "immutable"
77
import ImPropTypes from "react-immutable-proptypes"
88

@@ -82,7 +82,7 @@ export default class Operation extends PureComponent {
8282
schemes
8383
} = op
8484

85-
const externalDocsUrl = externalDocs ? buildUrl(externalDocs.url, specSelectors.url(), { selectedServer: oas3Selectors.selectedServer() }) : ""
85+
const externalDocsUrl = externalDocs ? safeBuildUrl(externalDocs.url, specSelectors.url(), { selectedServer: oas3Selectors.selectedServer() }) : ""
8686
let operation = operationProps.getIn(["op"])
8787
let responses = operation.get("responses")
8888
let parameters = getList(operation, ["parameters"])

src/core/utils/url.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ export function buildBaseUrl(selectedServer, specUrl) {
1212
if (!selectedServer) return specUrl
1313
if (isAbsoluteUrl(selectedServer)) return addProtocol(selectedServer)
1414

15-
return new URL(selectedServer, specUrl).href
15+
return new URL(selectedServer, specUrl).href
1616
}
1717

1818
export function buildUrl(url, specUrl, { selectedServer="" } = {}) {
19-
if (!url) return
19+
if (!url) return undefined
2020
if (isAbsoluteUrl(url)) return url
2121

2222
const baseUrl = buildBaseUrl(selectedServer, specUrl)
@@ -25,3 +25,15 @@ export function buildUrl(url, specUrl, { selectedServer="" } = {}) {
2525
}
2626
return new URL(url, baseUrl).href
2727
}
28+
29+
/**
30+
* Safe version of buildUrl function. `selectedServer` can contain server variables
31+
* which can fail the URL resolution.
32+
*/
33+
export function safeBuildUrl(url, specUrl, { selectedServer="" } = {}) {
34+
try {
35+
return buildUrl(url, specUrl, { selectedServer })
36+
} catch {
37+
return undefined
38+
}
39+
}

test/unit/core/utils.js

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import { Map, fromJS } from "immutable"
32
import {
43
mapToList,
@@ -36,6 +35,7 @@ import {
3635
isAbsoluteUrl,
3736
buildBaseUrl,
3837
buildUrl,
38+
safeBuildUrl,
3939
} from "core/utils/url"
4040

4141
import win from "core/window"
@@ -1445,6 +1445,7 @@ describe("utils", () => {
14451445
const absoluteServerUrl = "https://server-example.com/base-path/path"
14461446
const serverUrlRelativeToBase = "server-example/base-path/path"
14471447
const serverUrlRelativeToHost = "/server-example/base-path/path"
1448+
const serverUrlWithVariables = "https://api.example.com:{port}/{basePath}"
14481449

14491450
const specUrlAsInvalidUrl = "./examples/test.yaml"
14501451
const specUrlOas2NonUrlString = "an allowed OAS2 TermsOfService description string"
@@ -1488,6 +1489,85 @@ describe("utils", () => {
14881489
it("build relative url when no servers defined AND specUrl is OAS2 non-url string", () => {
14891490
expect(buildUrl(urlRelativeToHost, specUrlOas2NonUrlString, { selectedServer: noServerSelected })).toBe("http://localhost/relative-url/base-path/path")
14901491
})
1492+
1493+
it("throws error when server url contains non-transcluded server variables", () => {
1494+
const buildUrlThunk = () => buildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlWithVariables })
1495+
1496+
expect(buildUrlThunk).toThrow(/^Invalid base URL/)
1497+
})
1498+
})
1499+
1500+
describe("safeBuildUrl", () => {
1501+
const { location } = window
1502+
beforeAll(() => {
1503+
delete window.location
1504+
window.location = {
1505+
href: "http://localhost/",
1506+
}
1507+
})
1508+
afterAll(() => {
1509+
window.location = location
1510+
})
1511+
1512+
const specUrl = "https://petstore.swagger.io/v2/swagger.json"
1513+
1514+
const noUrl = ""
1515+
const absoluteUrl = "https://example.com/base-path/path"
1516+
const urlRelativeToBase = "relative-url/base-path/path"
1517+
const urlRelativeToHost = "/relative-url/base-path/path"
1518+
1519+
const noServerSelected = ""
1520+
const absoluteServerUrl = "https://server-example.com/base-path/path"
1521+
const serverUrlRelativeToBase = "server-example/base-path/path"
1522+
const serverUrlRelativeToHost = "/server-example/base-path/path"
1523+
const serverUrlWithVariables = "https://api.example.com:{port}/{basePath}"
1524+
1525+
const specUrlAsInvalidUrl = "./examples/test.yaml"
1526+
const specUrlOas2NonUrlString = "an allowed OAS2 TermsOfService description string"
1527+
1528+
it("build no url", () => {
1529+
expect(safeBuildUrl(noUrl, specUrl, { selectedServer: absoluteServerUrl })).toBe(undefined)
1530+
expect(safeBuildUrl(noUrl, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe(undefined)
1531+
expect(safeBuildUrl(noUrl, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe(undefined)
1532+
})
1533+
1534+
it("build absolute url", () => {
1535+
expect(safeBuildUrl(absoluteUrl, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://example.com/base-path/path")
1536+
expect(safeBuildUrl(absoluteUrl, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://example.com/base-path/path")
1537+
expect(safeBuildUrl(absoluteUrl, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://example.com/base-path/path")
1538+
})
1539+
1540+
it("build relative url with no server selected", () => {
1541+
expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: noServerSelected })).toBe("https://petstore.swagger.io/v2/relative-url/base-path/path")
1542+
expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: noServerSelected })).toBe("https://petstore.swagger.io/relative-url/base-path/path")
1543+
})
1544+
1545+
it("build relative url with absolute server url", () => {
1546+
expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://server-example.com/base-path/relative-url/base-path/path")
1547+
expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: absoluteServerUrl })).toBe("https://server-example.com/relative-url/base-path/path")
1548+
})
1549+
1550+
it("build relative url with server url relative to base", () => {
1551+
expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://petstore.swagger.io/v2/server-example/base-path/relative-url/base-path/path")
1552+
expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlRelativeToBase })).toBe("https://petstore.swagger.io/relative-url/base-path/path")
1553+
})
1554+
1555+
it("build relative url with server url relative to host", () => {
1556+
expect(safeBuildUrl(urlRelativeToBase, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://petstore.swagger.io/server-example/base-path/relative-url/base-path/path")
1557+
expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlRelativeToHost })).toBe("https://petstore.swagger.io/relative-url/base-path/path")
1558+
})
1559+
1560+
it("build relative url when no servers defined AND specUrl is invalid Url", () => {
1561+
expect(safeBuildUrl(urlRelativeToHost, specUrlAsInvalidUrl, { selectedServer: noServerSelected })).toBe("http://localhost/relative-url/base-path/path")
1562+
})
1563+
1564+
it("build relative url when no servers defined AND specUrl is OAS2 non-url string", () => {
1565+
expect(safeBuildUrl(urlRelativeToHost, specUrlOas2NonUrlString, { selectedServer: noServerSelected })).toBe("http://localhost/relative-url/base-path/path")
1566+
})
1567+
1568+
it("build no url when server url contains non-transcluded server variables", () => {
1569+
expect(safeBuildUrl(urlRelativeToHost, specUrl, { selectedServer: serverUrlWithVariables })).toBe(undefined)
1570+
})
14911571
})
14921572

14931573
describe("requiresValidationURL", () => {

0 commit comments

Comments
 (0)