spyne 0.22.4 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/spyne.esm.js +3 -3
- package/lib/spyne.esm.js.LICENSE.txt +2 -2
- package/lib/spyne.esm.js.map +1 -1
- package/lib/spyne.umd.js +2 -2
- package/lib/spyne.umd.js.LICENSE.txt +2 -2
- package/package.json +1 -1
- package/src/spyne/channels/channel-fetch-class.js +16 -7
- package/src/spyne/spyne-app.js +6 -3
- package/src/spyne/utils/channel-fetch-util.js +238 -92
- package/src/spyne/utils/sanitize-data.js +260 -34
- package/src/spyne/utils/sanitize-html.js +109 -16
- package/src/spyne/utils/spyne-app-properties.js +35 -6
- package/src/spyne/utils/spyne-trait.js +5 -2
- package/src/spyne/views/dom-element-template.js +11 -4
- package/src/spyne/views/dom-element.js +21 -2
- package/src/spyne/views/view-stream-broadcaster.js +4 -0
- package/src/tests/channels/channel-fetch.test.js +88 -1
- package/src/tests/utils/channel-fetch-util.test.js +44 -2
- package/src/tests/utils/security.test.js +288 -8
package/package.json
CHANGED
|
@@ -17,11 +17,13 @@ export class ChannelFetch extends Channel {
|
|
|
17
17
|
* <li>Requires a url property at instantiation.</li>
|
|
18
18
|
* <li>The response data is published as a ChannelPayload.</li>
|
|
19
19
|
* <li>The action for the returned ChannelFetch data is always in the following format, "CHANNEL_NAME_RESPONSE_EVENT"</li>
|
|
20
|
+
* <li>Fetch failures (network errors, non-OK HTTP statuses, unparseable responses) are published as a conformed error ChannelPayload with the action format, "CHANNEL_NAME_ERROR_EVENT"</li>
|
|
21
|
+
* <li>The error payload contains filterable properties such as errorType, status, statusText, url and message.</li>
|
|
20
22
|
* <li>The default request type is a GET request that returns a JSON object.</li>
|
|
21
23
|
* <li>However, any type of request and return type can be configured by using the body property when creating the instance.</li>
|
|
22
|
-
* <li>The mapFn gives access to the returned response and can be use to parse the data before it is published to other Channels and ViewStream instances.</li>
|
|
24
|
+
* <li>The mapFn gives access to the returned response and can be use to parse the data before it is published to other Channels and ViewStream instances. The mapFn is not called for error payloads.</li>
|
|
23
25
|
* <li>Channel Fetch will send the last response to future subscribers, and will not make further http(s) requests unless directed to do so.</li>
|
|
24
|
-
* <li>Data can be Fetched again, by sending a "
|
|
26
|
+
* <li>Data can be Fetched again, by sending a "CHANNEL_NAME_REQUEST_EVENT" action from a ViewStream's sendInfoToChannel method.</li>
|
|
25
27
|
* </ul>
|
|
26
28
|
*
|
|
27
29
|
* @constructor
|
|
@@ -35,7 +37,7 @@ export class ChannelFetch extends Channel {
|
|
|
35
37
|
*
|
|
36
38
|
* @example
|
|
37
39
|
* TITLE["<h4>A ViewStream Instance Sending A Fetch Update Request</h4>"]
|
|
38
|
-
* const action = "
|
|
40
|
+
* const action = "CHANNEL_FETCHCHANNEL_NAME_REQUEST_EVENT";
|
|
39
41
|
* const url = "//site.com/json/";
|
|
40
42
|
* const body = {
|
|
41
43
|
* method: "POST"
|
|
@@ -46,7 +48,7 @@ export class ChannelFetch extends Channel {
|
|
|
46
48
|
*
|
|
47
49
|
*/
|
|
48
50
|
|
|
49
|
-
constructor(name, props = {}) {
|
|
51
|
+
constructor(name, props = { disableSanitize:false }) {
|
|
50
52
|
// ALLOW FOR GENERIC MAP PROPERTY
|
|
51
53
|
|
|
52
54
|
ChannelFetch.validateMapMethod(props, name)
|
|
@@ -56,6 +58,7 @@ export class ChannelFetch extends Channel {
|
|
|
56
58
|
}
|
|
57
59
|
props.extendedActionsArr = [
|
|
58
60
|
`${name}_RESPONSE_EVENT`,
|
|
61
|
+
`${name}_ERROR_EVENT`,
|
|
59
62
|
[`${name}_REQUEST_EVENT`, 'onFetchUpdate']
|
|
60
63
|
]
|
|
61
64
|
props.sendCachedPayload = true
|
|
@@ -89,6 +92,7 @@ export class ChannelFetch extends Channel {
|
|
|
89
92
|
addRegisteredActions(name) {
|
|
90
93
|
const arr = [
|
|
91
94
|
'CHANNEL_RESPONSE_EVENT',
|
|
95
|
+
'CHANNEL_ERROR_EVENT',
|
|
92
96
|
['CHANNEL_UPDATE_RESPONSE_EVENT', 'onFetchUpdate'],
|
|
93
97
|
['CHANNEL_FETCH_REQUEST_EVENT', 'onFetchUpdate']
|
|
94
98
|
]
|
|
@@ -108,7 +112,12 @@ export class ChannelFetch extends Channel {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
onFetchReturned(streamItem) {
|
|
111
|
-
|
|
115
|
+
const isFetchError = streamItem?.isChannelFetchError === true
|
|
116
|
+
const action = isFetchError
|
|
117
|
+
? `${this.props.name}_ERROR_EVENT`
|
|
118
|
+
: `${this.props.name}_RESPONSE_EVENT`
|
|
119
|
+
|
|
120
|
+
return this.createChannelPayloadItem(streamItem, action)
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
createChannelPayloadItem(payload, action = `${this.props.name}_RESPONSE_EVENT`) {
|
|
@@ -118,12 +127,12 @@ export class ChannelFetch extends Channel {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
getPropsForFetch(evt) {
|
|
121
|
-
const dataObj =
|
|
130
|
+
const dataObj = evt?.payload ?? evt?.viewStreamInfo?.payload ?? {}
|
|
122
131
|
return pick(['mapFn', 'url', 'header', 'body', 'mode', 'method', 'responseType', 'debug'], dataObj)
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
consolidateAllFetchProps(options, props = this.props) {
|
|
126
|
-
const propsOptions = pick(['mapFn', 'url', 'header', 'body', 'mode', 'method', 'responseType', 'debug'], props)
|
|
135
|
+
const propsOptions = pick(['mapFn', 'url', 'header', 'body', 'mode', 'disableSanitize', 'method', 'responseType', 'debug'], props)
|
|
127
136
|
const mergeOptions = (o1, o2) => mergeDeepRight(o1, o2)
|
|
128
137
|
const filterOutUndefined = reject(isNil)
|
|
129
138
|
return compose(filterOutUndefined, mergeOptions)(propsOptions, options)
|
package/src/spyne/spyne-app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { sanitizeHTMLConfigure } from './utils/sanitize-html.js'
|
|
|
6
6
|
import { sanitizeDataConfigure } from './utils/sanitize-data.js'
|
|
7
7
|
|
|
8
8
|
const _channels = new ChannelsMap()
|
|
9
|
-
const version = '0.
|
|
9
|
+
const version = '0.24.0'
|
|
10
10
|
|
|
11
11
|
class SpyneApplication {
|
|
12
12
|
/**
|
|
@@ -41,7 +41,7 @@ class SpyneApplication {
|
|
|
41
41
|
init(config = {}, testMode = false) {
|
|
42
42
|
// this.channels = new ChannelsMap();
|
|
43
43
|
/*!
|
|
44
|
-
* Spyne 0.
|
|
44
|
+
* Spyne 0.24.0
|
|
45
45
|
* https://spynejs.org
|
|
46
46
|
*
|
|
47
47
|
* @license
|
|
@@ -74,6 +74,9 @@ class SpyneApplication {
|
|
|
74
74
|
scrollLockY: 0,
|
|
75
75
|
debug: false,
|
|
76
76
|
strict: false,
|
|
77
|
+
mode: 'app',
|
|
78
|
+
disableSanitize: false,
|
|
79
|
+
iframes: {},
|
|
77
80
|
baseHref: undefined,
|
|
78
81
|
IMG_PATH: imgPath,
|
|
79
82
|
pluginMethods:{
|
|
@@ -137,7 +140,7 @@ class SpyneApplication {
|
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
sanitizeHTMLConfigure(SpyneAppProperties.config);
|
|
140
|
-
sanitizeDataConfigure(SpyneAppProperties.config)
|
|
143
|
+
sanitizeDataConfigure(SpyneAppProperties.config);
|
|
141
144
|
|
|
142
145
|
}
|
|
143
146
|
|
|
@@ -1,64 +1,43 @@
|
|
|
1
|
-
import { from } from 'rxjs'
|
|
2
|
-
import {
|
|
3
|
-
import { compose, prop, defaultTo,
|
|
4
|
-
import sanitizeData
|
|
1
|
+
import { from, of } from 'rxjs'
|
|
2
|
+
import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
|
|
3
|
+
import { compose, prop, defaultTo, pick, mergeDeepRight } from 'ramda'
|
|
4
|
+
import sanitizeData from './sanitize-data.js'
|
|
5
5
|
|
|
6
6
|
export class ChannelFetchUtil {
|
|
7
7
|
/**
|
|
8
8
|
* @module ChannelFetchUtil
|
|
9
9
|
* @type util
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
11
|
* @desc
|
|
13
|
-
* This is the core object used for ChannelFetch. This utility wraps the
|
|
12
|
+
* This is the core object used for ChannelFetch. This utility wraps the JavaScript
|
|
13
|
+
* fetch API into an observable.
|
|
14
14
|
*
|
|
15
15
|
* @constructor
|
|
16
16
|
* @param {Object} options Properties used to create the fetch request
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {Boolean} testMode Controls
|
|
19
|
-
*
|
|
20
|
-
* @property {Object} options.url - = undefined; The url used for the request
|
|
21
|
-
* @property {Object} options.serverOptions - = undefined; The properties, header, body, mode, method, for the request
|
|
22
|
-
* @property {Object} options.mapFn - = undefined; A method that can be used to parse the data before it's returned
|
|
23
|
-
* @property {Object} options.responseType - = 'json'; Default is json
|
|
24
|
-
* @property {Object} options.debug - = false; will trace the fetch response to the console befor the observable completes
|
|
25
|
-
* @property {Function} Subscriber - = undefined; the method that will be called when the fetch is complete.
|
|
26
|
-
* @property {Boolean} testMode - = false; Used for unit testing.
|
|
27
|
-
*
|
|
28
|
-
* @returns The fetched response parsed by the set parameters.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* TITLE['<h4>Using ChannelFetchUtil to Retrieve an Image</h4>']
|
|
33
|
-
*
|
|
34
|
-
* const url = "/static/images/myimage.jpg";
|
|
35
|
-
* const responseType = "blob";
|
|
36
|
-
* const onImgBlobReturned = (blob)=>{
|
|
37
|
-
* let blobUrl = URL.createObjectURL(blob);
|
|
38
|
-
* this.appendView(
|
|
39
|
-
* new ViewStream({
|
|
40
|
-
* tagName: 'img',
|
|
41
|
-
* src: blobUrl,
|
|
42
|
-
* width:300
|
|
43
|
-
* })
|
|
44
|
-
* )}
|
|
45
|
-
*
|
|
46
|
-
* new ChannelFetchUtil({url, responseType}, onImgBlobReturned);
|
|
17
|
+
* @param {Function} subscriber A method assigned to listen to the result
|
|
18
|
+
* @param {Boolean} testMode Controls initialization for unit tests
|
|
19
|
+
* @param {String} CHANNEL_NAME The name of the channel using this fetch util
|
|
47
20
|
*
|
|
21
|
+
* @property {String} options.url - The URL used for the request
|
|
22
|
+
* @property {Object} options.serverOptions - The properties for the request
|
|
23
|
+
* @property {Function} options.mapFn - A method that can parse the data before it is returned
|
|
24
|
+
* @property {String} options.responseType - Default is json
|
|
25
|
+
* @property {Boolean} options.debug - Logs fetch response details before observable completes
|
|
26
|
+
* @property {Boolean} options.disableSanitize - Allows trusted fetch data to skip sanitization
|
|
48
27
|
*
|
|
28
|
+
* @returns The fetched response parsed by the configured parameters.
|
|
49
29
|
*/
|
|
50
30
|
|
|
51
|
-
constructor(options, subscriber, testMode, CHANNEL_NAME) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const testSubscriber = (p) => console.log('FETCH RETURNED ', p)
|
|
31
|
+
constructor(options = {}, subscriber, testMode, CHANNEL_NAME) {
|
|
32
|
+
const testSubscriber = p => console.log('FETCH RETURNED ', p)
|
|
55
33
|
|
|
56
34
|
this._mapFn = ChannelFetchUtil.setMapFn(options)
|
|
57
35
|
this._url = ChannelFetchUtil.setUrl(options)
|
|
58
36
|
this._responseType = ChannelFetchUtil.setResponseType(options)
|
|
59
37
|
this._serverOptions = ChannelFetchUtil.setServerOptions(options)
|
|
60
38
|
this._subscriber = subscriber !== undefined ? subscriber : testSubscriber
|
|
61
|
-
this.debug = options
|
|
39
|
+
this.debug = options?.debug === true
|
|
40
|
+
this.disableSanitize = options?.disableSanitize === true
|
|
62
41
|
this.channelName = CHANNEL_NAME
|
|
63
42
|
|
|
64
43
|
const fetchProps = {
|
|
@@ -66,8 +45,10 @@ export class ChannelFetchUtil {
|
|
|
66
45
|
url: this.url,
|
|
67
46
|
serverOptions: this.serverOptions,
|
|
68
47
|
responseType: this.responseType,
|
|
48
|
+
disableSanitize: this.disableSanitize,
|
|
69
49
|
debug: this.debug
|
|
70
50
|
}
|
|
51
|
+
|
|
71
52
|
if (testMode !== true) {
|
|
72
53
|
ChannelFetchUtil.startWindowFetch(fetchProps, this._subscriber, this.channelName)
|
|
73
54
|
}
|
|
@@ -75,59 +56,189 @@ export class ChannelFetchUtil {
|
|
|
75
56
|
|
|
76
57
|
static startWindowFetch(props, subscriber, channelName) {
|
|
77
58
|
const { mapFn, url, serverOptions, responseType, debug } = props
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return (data) => {
|
|
85
|
-
const disableSanitize = props?.disableSanitize === true
|
|
86
|
-
|
|
87
|
-
if (disableSanitize) {
|
|
88
|
-
const env = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV)
|
|
89
|
-
? process.env.NODE_ENV
|
|
90
|
-
: 'development'
|
|
91
|
-
|
|
92
|
-
const msg = `SPYNE SECURITY WARNING — Sanitization disabled for fetch from ${url}. Data will enter the reactive stream unsanitized. Only use disableSanitize=true for trusted or controlled sources.`
|
|
93
|
-
|
|
94
|
-
if (env === 'production') {
|
|
95
|
-
throw new Error(`SpyneJS security policy violation: disableSanitize=true is not allowed in production (${url}).`)
|
|
96
|
-
} else {
|
|
97
|
-
console.warn(
|
|
98
|
-
`%c${msg}`,
|
|
99
|
-
'color:#f33;font-weight:bold;'
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const sanitizedData = disableSanitize ? data : sanitizeData(data)
|
|
105
|
-
return mapMethod(sanitizedData, metadata)
|
|
106
|
-
}
|
|
59
|
+
|
|
60
|
+
const metadata = {
|
|
61
|
+
channelName,
|
|
62
|
+
url,
|
|
63
|
+
responseType,
|
|
64
|
+
serverOptions
|
|
107
65
|
}
|
|
108
66
|
|
|
109
|
-
|
|
67
|
+
const tapLogDebug = p => {
|
|
68
|
+
console.log('DEBUG FETCH :', p, metadata)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tapLog = debug === true ? tap(tapLogDebug) : tap(() => {})
|
|
72
|
+
|
|
73
|
+
const mapWrapper = mapMethod => data => {
|
|
74
|
+
const disableSanitize = props?.disableSanitize === true
|
|
75
|
+
|
|
76
|
+
// Fetched data is remote and untrusted: always sanitize in 'app' mode,
|
|
77
|
+
// regardless of the application's authoring/richtext posture.
|
|
78
|
+
const sanitizedData = disableSanitize ? data : sanitizeData(data, { mode: 'app' })
|
|
79
|
+
|
|
80
|
+
return mapMethod(sanitizedData, metadata)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response$ = from(window.fetch(url, serverOptions)).pipe(
|
|
84
|
+
mergeMap(response =>
|
|
85
|
+
from(ChannelFetchUtil.parseResponse(response, metadata))
|
|
86
|
+
),
|
|
87
|
+
|
|
88
|
+
tapLog,
|
|
110
89
|
|
|
111
|
-
|
|
112
|
-
.pipe(tap(tapLog), flatMap(r => from(r[responseType]())),
|
|
113
|
-
map(mapWrapper(mapFn)),
|
|
114
|
-
publish())
|
|
90
|
+
map(mapWrapper(mapFn)),
|
|
115
91
|
|
|
116
|
-
|
|
92
|
+
catchError(error =>
|
|
93
|
+
of(ChannelFetchUtil.createFetchErrorPayload(error, metadata))
|
|
94
|
+
),
|
|
95
|
+
|
|
96
|
+
share()
|
|
97
|
+
)
|
|
117
98
|
|
|
118
99
|
response$.subscribe(subscriber)
|
|
119
100
|
}
|
|
120
101
|
|
|
102
|
+
static async parseResponse(response, metadata) {
|
|
103
|
+
const { responseType } = metadata
|
|
104
|
+
|
|
105
|
+
const status = response.status
|
|
106
|
+
const statusText = response.statusText
|
|
107
|
+
const contentType = response.headers?.get?.('content-type') || ''
|
|
108
|
+
|
|
109
|
+
const responseMetadata = {
|
|
110
|
+
...metadata,
|
|
111
|
+
status,
|
|
112
|
+
statusText,
|
|
113
|
+
contentType,
|
|
114
|
+
ok: response.ok
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (response.ok !== true) {
|
|
118
|
+
const rawBody = await ChannelFetchUtil.safeReadText(response)
|
|
119
|
+
|
|
120
|
+
throw ChannelFetchUtil.createFetchError({
|
|
121
|
+
errorType: 'FETCH_HTTP_ERROR',
|
|
122
|
+
message: `Fetch request failed with status ${status} ${statusText}`,
|
|
123
|
+
metadata: responseMetadata,
|
|
124
|
+
rawBody
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (responseType === 'json') {
|
|
129
|
+
return ChannelFetchUtil.parseJsonResponse(response, responseMetadata)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (responseType === 'text') {
|
|
133
|
+
return response.text()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (responseType === 'blob') {
|
|
137
|
+
return response.blob()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (responseType === 'arrayBuffer') {
|
|
141
|
+
return response.arrayBuffer()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (responseType === 'formData') {
|
|
145
|
+
return response.formData()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw ChannelFetchUtil.createFetchError({
|
|
149
|
+
errorType: 'FETCH_UNSUPPORTED_RESPONSE_TYPE',
|
|
150
|
+
message: `Unsupported fetch responseType "${responseType}"`,
|
|
151
|
+
metadata: responseMetadata
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static async parseJsonResponse(response, metadata) {
|
|
156
|
+
const rawBody = await response.text()
|
|
157
|
+
const trimmedBody = rawBody.trim()
|
|
158
|
+
|
|
159
|
+
if (trimmedBody === '') {
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(trimmedBody)
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw ChannelFetchUtil.createFetchError({
|
|
167
|
+
errorType: 'FETCH_RESPONSE_PARSE_ERROR',
|
|
168
|
+
message: 'Expected JSON but could not parse response body.',
|
|
169
|
+
metadata,
|
|
170
|
+
rawBody,
|
|
171
|
+
originalError: error
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static createFetchError({ errorType, message, metadata, rawBody = '', originalError }) {
|
|
177
|
+
const error = new Error(message)
|
|
178
|
+
|
|
179
|
+
error.isChannelFetchError = true
|
|
180
|
+
error.errorType = errorType
|
|
181
|
+
error.metadata = metadata
|
|
182
|
+
error.rawBody = rawBody
|
|
183
|
+
error.originalError = originalError
|
|
184
|
+
|
|
185
|
+
return error
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static createFetchErrorPayload(error, metadata) {
|
|
189
|
+
const fetchMetadata = error?.metadata || metadata || {}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
isChannelFetchError: true,
|
|
193
|
+
isError: true,
|
|
194
|
+
error: true,
|
|
195
|
+
errorType: error?.errorType || 'FETCH_UNKNOWN_ERROR',
|
|
196
|
+
message: error?.message || 'Unknown ChannelFetchUtil error',
|
|
197
|
+
|
|
198
|
+
channelName: fetchMetadata.channelName,
|
|
199
|
+
url: fetchMetadata.url,
|
|
200
|
+
responseType: fetchMetadata.responseType,
|
|
201
|
+
status: fetchMetadata.status,
|
|
202
|
+
statusText: fetchMetadata.statusText,
|
|
203
|
+
contentType: fetchMetadata.contentType,
|
|
204
|
+
ok: fetchMetadata.ok,
|
|
205
|
+
|
|
206
|
+
rawBodyPreview: ChannelFetchUtil.truncateBody(error?.rawBody),
|
|
207
|
+
|
|
208
|
+
originalErrorMessage: error?.originalError?.message
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static truncateBody(body = '', maxLength = 500) {
|
|
213
|
+
if (typeof body !== 'string') {
|
|
214
|
+
return ''
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return body.length > maxLength
|
|
218
|
+
? `${body.slice(0, maxLength)}...`
|
|
219
|
+
: body
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static async safeReadText(response) {
|
|
223
|
+
try {
|
|
224
|
+
return await response.text()
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return ''
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
121
230
|
static setMapFn(opts) {
|
|
122
|
-
const getFn = compose(defaultTo(
|
|
231
|
+
const getFn = compose(defaultTo(p => p), prop('mapFn'))
|
|
123
232
|
return getFn(opts)
|
|
124
233
|
}
|
|
125
234
|
|
|
126
235
|
static setUrl(opts) {
|
|
127
236
|
const url = prop('url', opts)
|
|
237
|
+
|
|
128
238
|
if (url === undefined) {
|
|
129
239
|
console.warn('SPYNE WARNING: URL is undefined for data channel')
|
|
130
240
|
}
|
|
241
|
+
|
|
131
242
|
return url
|
|
132
243
|
}
|
|
133
244
|
|
|
@@ -152,16 +263,47 @@ export class ChannelFetchUtil {
|
|
|
152
263
|
}
|
|
153
264
|
|
|
154
265
|
static stringifyBodyIfItExists(obj) {
|
|
155
|
-
const
|
|
266
|
+
const body = obj?.body
|
|
267
|
+
|
|
268
|
+
const shouldStringifyBody =
|
|
269
|
+
body !== null &&
|
|
270
|
+
typeof body === 'object' &&
|
|
271
|
+
body.constructor === Object
|
|
272
|
+
|
|
273
|
+
if (shouldStringifyBody !== true) {
|
|
274
|
+
return obj
|
|
275
|
+
}
|
|
156
276
|
|
|
157
|
-
|
|
277
|
+
const headers = new Headers(obj.headers || {})
|
|
278
|
+
|
|
279
|
+
if (headers.has('Content-Type') !== true) {
|
|
280
|
+
headers.set('Content-Type', 'application/json')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
...obj,
|
|
285
|
+
headers,
|
|
286
|
+
body: JSON.stringify(body)
|
|
287
|
+
}
|
|
158
288
|
}
|
|
159
289
|
|
|
160
290
|
static updateMethodWhenBodyExists(opts) {
|
|
161
|
-
const hasBody =
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
291
|
+
const hasBody = opts?.body !== undefined
|
|
292
|
+
const method = opts?.method || 'GET'
|
|
293
|
+
|
|
294
|
+
if (hasBody && method.toUpperCase() === 'GET') {
|
|
295
|
+
console.warn(
|
|
296
|
+
'SPYNE WARNING: Fetch body was provided with method GET. Changing method to POST.',
|
|
297
|
+
opts
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
...opts,
|
|
302
|
+
method: 'POST'
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return opts
|
|
165
307
|
}
|
|
166
308
|
|
|
167
309
|
static setServerOptions(opts) {
|
|
@@ -178,26 +320,30 @@ export class ChannelFetchUtil {
|
|
|
178
320
|
'integrity',
|
|
179
321
|
'keepalive'
|
|
180
322
|
], opts)
|
|
323
|
+
|
|
181
324
|
let mergedOptions = mergeDeepRight(ChannelFetchUtil.baseOptions(), options)
|
|
325
|
+
|
|
182
326
|
mergedOptions = ChannelFetchUtil.updateMethodWhenBodyExists(mergedOptions)
|
|
183
327
|
mergedOptions = ChannelFetchUtil.stringifyBodyIfItExists(mergedOptions)
|
|
328
|
+
|
|
184
329
|
return mergedOptions
|
|
185
330
|
}
|
|
186
331
|
|
|
187
332
|
static baseOptions() {
|
|
188
333
|
return {
|
|
189
|
-
method: 'GET',
|
|
334
|
+
method: 'GET',
|
|
335
|
+
|
|
190
336
|
headers: new Headers({
|
|
191
337
|
Accept: 'application/json, text/plain, */*'
|
|
192
338
|
}),
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
referrerPolicy: 'no-referrer-when-downgrade',
|
|
199
|
-
integrity: '',
|
|
200
|
-
keepalive: false
|
|
339
|
+
|
|
340
|
+
mode: 'cors',
|
|
341
|
+
credentials: 'same-origin',
|
|
342
|
+
cache: 'default',
|
|
343
|
+
redirect: 'follow',
|
|
344
|
+
referrerPolicy: 'no-referrer-when-downgrade',
|
|
345
|
+
integrity: '',
|
|
346
|
+
keepalive: false
|
|
201
347
|
}
|
|
202
348
|
}
|
|
203
349
|
}
|