1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 01:14:36 +00:00

Compare commits

...

12 Commits

Author SHA1 Message Date
Yury Shkoda
7a02c8ad34 Merge pull request #131 from sasjs/issue-124
fix(session): add internal SAS error handler
2020-10-14 14:03:58 +03:00
Yury Shkoda
331d9b0010 fix(session): add internal SAS error handler 2020-10-14 12:53:59 +03:00
Yury Shkoda
fa87111f4a Merge pull request #126 from sasjs/issue-124
fix(context): fixed 'getExecutableContexts' method
2020-10-07 17:53:31 +03:00
Yury Shkoda
94967b0f6c fix(context): fixed 'getExecutableContexts' method 2020-10-07 17:25:47 +03:00
Krishna Acondy
bd8012fe3e fix(*): revert to older version of isomorphic-fetch 2020-10-03 18:19:06 +01:00
Krishna Acondy
fa531b34fd Merge pull request #120 from sasjs/session-manager-debug
fix(debug): propagate debug value from SASjs config
2020-10-03 17:41:35 +01:00
Krishna Acondy
354443c98b fix(debug): propagate debug value from SASjs config 2020-10-03 16:53:00 +01:00
Krishna Acondy
ee30ab195f Merge pull request #115 from sasjs/issue-114
chore(error-message): updated error message for forbidden request
2020-10-01 09:10:35 +01:00
Yury Shkoda
02c1712d22 chore(error-message): updated error message for forbidden request 2020-10-01 09:49:24 +03:00
Krishna Acondy
37def7a956 Merge pull request #111 from sasjs/dependabot/npm_and_yarn/isomorphic-fetch-3.0.0
chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
2020-09-29 20:03:06 +01:00
Krishna Acondy
653e3d05e0 Merge branch 'master' into dependabot/npm_and_yarn/isomorphic-fetch-3.0.0 2020-09-29 19:55:08 +01:00
dependabot-preview[bot]
e2ea3f4ddc chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
Bumps [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) from 2.2.1 to 3.0.0.
- [Release notes](https://github.com/matthew-andrews/isomorphic-fetch/releases)
- [Commits](https://github.com/matthew-andrews/isomorphic-fetch/compare/v2.2.1...v3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-25 23:51:17 +00:00
5 changed files with 321 additions and 219 deletions

25
package-lock.json generated
View File

@@ -3704,11 +3704,21 @@
"dev": true
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"requires": {
"iconv-lite": "~0.4.13"
"iconv-lite": "^0.6.2"
},
"dependencies": {
"iconv-lite": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
}
}
},
"end-of-stream": {
@@ -4967,6 +4977,7 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@@ -16070,9 +16081,9 @@
}
},
"whatwg-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz",
"integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ=="
},
"whatwg-mimetype": {
"version": "2.3.0",

View File

@@ -38,6 +38,7 @@ export class SASViyaApiClient {
private csrfToken: CsrfToken | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
@@ -45,6 +46,15 @@ export class SASViyaApiClient {
)
private folderMap = new Map<string, Job[]>()
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
this.sessionManager.debug = value
}
/**
* Returns a list of jobs in the currently set root folder.
*/
@@ -135,42 +145,50 @@ export class SASViyaApiClient {
const promises = contextsList.map((context: any) => {
const linesOfCode = ['%put &=sysuserid;']
return this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
false,
null,
true
).catch(() => null)
return () =>
this.executeScript(
`test-${context.name}`,
linesOfCode,
context.name,
accessToken,
null,
true
).catch((err) => err)
})
const results = await Promise.all(promises)
let results: any[] = []
for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => {
if (result) {
let sysUserId = ''
if (result && result.body && result.body.details) {
try {
const resultParsed = JSON.parse(result.body.details)
if (result.log) {
const sysUserIdLog = result.log
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (resultParsed && resultParsed.body) {
let sysUserId = ''
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
const sysUserIdLog = resultParsed.body
.split('\n')
.find((line: string) => line.startsWith('SYSUSERID='))
if (sysUserIdLog) {
sysUserId = sysUserIdLog.replace('SYSUSERID=', '')
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
}
} catch (error) {
throw error
}
executableContexts.push({
createdBy: contextsList[index].createdBy,
id: contextsList[index].id,
name: contextsList[index].name,
version: contextsList[index].version,
attributes: {
sysUserId
}
})
}
})
@@ -318,7 +336,9 @@ export class SASViyaApiClient {
originalContext = await this.getComputeContextByName(
contextName,
accessToken
).catch((_) => {})
).catch((err) => {
throw err
})
// Try to find context by id, when context name has been changed.
if (!originalContext) {
@@ -404,7 +424,6 @@ export class SASViyaApiClient {
* @param contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse.
* @param silent - optional flag to disable logging.
* @param data - execution data.
* @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
@@ -414,12 +433,9 @@ export class SASViyaApiClient {
linesOfCode: string[],
contextName: string,
accessToken?: string,
silent = false,
data = null,
debug = false,
expectWebout = false
): Promise<any> {
silent = !debug
try {
const headers: any = {
'Content-Type': 'application/json'
@@ -430,7 +446,12 @@ export class SASViyaApiClient {
}
let executionSessionId: string
const session = await this.sessionManager.getSession(accessToken)
const session = await this.sessionManager
.getSession(accessToken)
.catch((err) => {
throw err
})
executionSessionId = session!.id
const jobArguments: { [key: string]: any } = {
@@ -442,7 +463,7 @@ export class SASViyaApiClient {
_OMITTEXTLOG: true
}
if (debug) {
if (this.debug) {
jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131
@@ -469,7 +490,9 @@ export class SASViyaApiClient {
if (data) {
if (JSON.stringify(data).includes(';')) {
files = await this.uploadTables(data, accessToken)
files = await this.uploadTables(data, accessToken).catch((err) => {
throw err
})
jobVariables['_webin_file_count'] = files.length
@@ -500,9 +523,11 @@ export class SASViyaApiClient {
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
postJobRequest
)
).catch((err) => {
throw err
})
if (!silent) {
if (this.debug) {
console.log(`Job has been submitted for '${fileName}'.`)
console.log(
`You can monitor the job progress at '${this.serverUrl}${
@@ -511,32 +536,33 @@ export class SASViyaApiClient {
)
}
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
silent
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
{ headers }
)
).catch((err) => {
throw err
})
let jobResult
let log
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) {
if (this.debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`,
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
}
if (jobStatus === 'failed' || jobStatus === 'error') {
@@ -562,9 +588,13 @@ export class SASViyaApiClient {
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
.catch((err) => {
throw err
})
return Promise.reject(
new ErrorResponse('Job execution failed', {
@@ -580,7 +610,11 @@ export class SASViyaApiClient {
})
}
await this.sessionManager.clearSession(executionSessionId, accessToken)
await this.sessionManager
.clearSession(executionSessionId, accessToken)
.catch((err) => {
throw err
})
return { result: jobResult?.result, log }
} catch (e) {
@@ -590,9 +624,7 @@ export class SASViyaApiClient {
linesOfCode,
contextName,
accessToken,
silent,
data,
debug
data
)
} else {
throw e
@@ -991,9 +1023,7 @@ export class SASViyaApiClient {
linesToExecute,
contextName,
accessToken,
true,
data,
debug,
true
)
}
@@ -1125,12 +1155,7 @@ export class SASViyaApiClient {
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest
)
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
true
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers }
@@ -1195,8 +1220,7 @@ export class SASViyaApiClient {
private async pollJobState(
postedJob: any,
etag: string | null,
accessToken?: string,
silent = false
accessToken?: string
) {
const MAX_POLL_COUNT = 1000
const POLL_INTERVAL = 100
@@ -1235,7 +1259,7 @@ export class SASViyaApiClient {
postedJobState === 'pending'
) {
if (stateLink) {
if (!silent) {
if (this.debug) {
console.log('Polling job status... \n')
}
const { result: jobState } = await this.request<string>(
@@ -1247,7 +1271,7 @@ export class SASViyaApiClient {
)
postedJobState = jobState.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state: ${postedJobState}\n`)
}
pollCount++
@@ -1263,49 +1287,6 @@ export class SASViyaApiClient {
})
}
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
) {
let sessionState = session.state
let pollCount = 0
const headers: any = {
'Content-Type': 'application/json',
'If-None-Match': etag
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (stateLink) {
if (!silent) {
console.log('Polling session status... \n')
}
const { result: state } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers
},
'text'
)
sessionState = state.trim()
if (!silent) {
console.log(`Current state: ${sessionState}\n`)
}
pollCount++
resolve(sessionState)
}
} else {
resolve(sessionState)
}
})
}
private async uploadTables(data: any, accessToken?: string) {
const uploadedFiles = []
const headers: any = {

View File

@@ -209,9 +209,7 @@ export default class SASjs {
fileName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
sessionId = '',
silent = false
accessToken?: string
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
@@ -220,9 +218,7 @@ export default class SASjs {
linesOfCode,
contextName,
accessToken,
silent,
null,
this.sasjsConfig.debug
null
)
}
@@ -410,6 +406,9 @@ export default class SASjs {
*/
public setDebugState(value: boolean) {
this.sasjsConfig.debug = value
if (this.sasViyaApiClient) {
this.sasViyaApiClient.debug = value
}
}
/**
@@ -635,6 +634,7 @@ export default class SASjs {
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl)
}
@@ -1352,6 +1352,8 @@ export default class SASjs {
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
this.sasViyaApiClient.debug = this.sasjsConfig.debug
}
if (this.sasjsConfig.serverType === ServerType.SAS9) {
if (this.sas9ApiClient)

View File

@@ -2,6 +2,12 @@ import { Session, Context, CsrfToken } from './types'
import { asyncForEach, makeRequest, isUrl } from './utils'
const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
let RETRY_COUNT: number = 0
const INTERNAL_SAS_ERROR = {
status: 304,
message: 'Not Modified'
}
export class SessionManager {
constructor(
@@ -15,22 +21,34 @@ export class SessionManager {
private sessions: Session[] = []
private currentContext: Context | null = null
private csrfToken: CsrfToken | null = null
private _debug: boolean = false
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
}
async getSession(accessToken?: string) {
await this.createSessions(accessToken)
this.createAndWaitForSession(accessToken)
await this.createAndWaitForSession(accessToken)
const session = this.sessions.pop()
const secondsSinceSessionCreation =
(new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
1000
if (
!session!.attributes ||
secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
) {
await this.createSessions(accessToken)
const freshSession = this.sessions.pop()
return freshSession
}
return session
}
@@ -39,22 +57,37 @@ export class SessionManager {
method: 'DELETE',
headers: this.getHeaders(accessToken)
}
return await this.request<Session>(
`${this.serverUrl}/compute/sessions/${id}`,
deleteSessionRequest
).then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id)
})
)
.then(() => {
this.sessions = this.sessions.filter((s) => s.id !== id)
})
.catch((err) => {
throw err
})
}
private async createSessions(accessToken?: string) {
if (!this.sessions.length) {
if (!this.currentContext) {
await this.setCurrentContext(accessToken)
await this.setCurrentContext(accessToken).catch((err) => {
throw err
})
}
await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
const createdSession = await this.createAndWaitForSession(accessToken)
const createdSession = await this.createAndWaitForSession(
accessToken
).catch((err) => {
throw err
})
this.sessions.push(createdSession)
}).catch((err) => {
throw err
})
}
}
@@ -64,13 +97,18 @@ export class SessionManager {
method: 'POST',
headers: this.getHeaders(accessToken)
}
const { result: createdSession, etag } = await this.request<Session>(
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
createSessionRequest
)
).catch((err) => {
throw err
})
await this.waitForSession(createdSession, etag, accessToken)
this.sessions.push(createdSession)
return createdSession
}
@@ -80,6 +118,8 @@ export class SessionManager {
items: Context[]
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
headers: this.getHeaders(accessToken)
}).catch((err) => {
throw err
})
const contextsList =
@@ -98,6 +138,8 @@ export class SessionManager {
}
this.currentContext = currentContext
Promise.resolve()
}
}
@@ -105,6 +147,7 @@ export class SessionManager {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
@@ -115,8 +158,7 @@ export class SessionManager {
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
accessToken?: string
) {
let sessionState = session.state
const headers: any = {
@@ -124,24 +166,41 @@ export class SessionManager {
'If-None-Match': etag
}
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (stateLink) {
if (!silent) {
if (this.debug) {
console.log('Polling session status... \n') // ?
}
const { result: state } = await this.request<string>(
const { result: state } = await this.requestSessionStatus<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers
},
'text'
)
).catch((err) => {
throw err
})
sessionState = state.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state is '${sessionState}'\n`)
}
// There is an internal error present in SAS Viya 3.5
// Retry to wait for a session status in such case of SAS internal error
if (
sessionState === INTERNAL_SAS_ERROR.message &&
RETRY_COUNT < RETRY_LIMIT
) {
RETRY_COUNT++
resolve(this.waitForSession(session, etag, accessToken))
}
resolve(sessionState)
}
} else {
@@ -161,6 +220,7 @@ export class SessionManager {
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
@@ -169,6 +229,36 @@ export class SessionManager {
this.setCsrfToken(token)
},
contentType
)
).catch((err) => {
throw err
})
}
private async requestSessionStatus<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
return await makeRequest<T>(
url,
options,
(token) => {
this.csrfToken = token
this.setCsrfToken(token)
},
contentType
).catch((err) => {
if (err.status === INTERNAL_SAS_ERROR.status)
return { result: INTERNAL_SAS_ERROR.message }
throw err
})
}
}

View File

@@ -2,7 +2,7 @@ import { CsrfToken } from '../types'
import { needsRetry } from './needsRetry'
let retryCount: number = 0
let retryLimit: number = 5
const retryLimit: number = 5
export async function makeRequest<T>(
url: string,
@@ -18,57 +18,118 @@ export async function makeRequest<T>(
: (res: Response) => res.text()
let etag = null
const result = await fetch(url, request).then(async (response) => {
if (response.redirected && response.url.includes('SASLogon/login')) {
return Promise.reject({ status: 401 })
}
if (!response.ok) {
if (response.status === 403) {
const tokenHeader = response.headers.get('X-CSRF-HEADER')
const result = await fetch(url, request)
.then(async (response) => {
if (response.redirected && response.url.includes('SASLogon/login')) {
return Promise.reject({ status: 401 })
}
if (tokenHeader) {
const token = response.headers.get(tokenHeader)
callback({
headerName: tokenHeader,
value: token || ''
if (!response.ok) {
if (response.status === 403) {
const tokenHeader = response.headers.get('X-CSRF-HEADER')
if (tokenHeader) {
const token = response.headers.get(tokenHeader)
callback({
headerName: tokenHeader,
value: token || ''
})
retryRequest = {
...request,
headers: { ...request.headers, [tokenHeader]: token }
}
return await fetch(url, retryRequest).then((res) => {
etag = res.headers.get('ETag')
return responseTransform(res)
})
} else {
let body: any = await response.text().catch((err) => {
throw err
})
try {
body = JSON.parse(body)
body.message = `Forbidden. Check your permissions and user groups, and also the scopes granted when registering your CLIENT_ID. ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
return Promise.reject({ status: response.status, body })
}
} else {
let body: any = await response.text().catch((err) => {
throw err
})
retryRequest = {
...request,
headers: { ...request.headers, [tokenHeader]: token }
if (needsRetry(body)) {
if (retryCount < retryLimit) {
retryCount++
let retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
).catch((err) => {
throw err
})
retryCount = 0
etag = retryResponse.etag
return retryResponse.result
} else {
retryCount = 0
throw new Error('Request retry limit exceeded')
}
}
return fetch(url, retryRequest).then((res) => {
etag = res.headers.get('ETag')
return responseTransform(res)
})
} else {
let body: any = await response.text()
if (response.status === 401) {
try {
body = JSON.parse(body)
try {
body = JSON.parse(body)
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
body.message || ''
}`
body.message = `Forbidden. Check your permissions and user groups. ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
body = JSON.stringify(body)
} catch (_) {}
}
return Promise.reject({ status: response.status, body })
}
} else {
let body: any = await response.text()
if (response.status === 204) {
return Promise.resolve()
}
const responseTransformed = await responseTransform(response).catch(
(err) => {
throw err
}
)
let responseText = ''
if (needsRetry(body)) {
if (typeof responseTransformed === 'string') {
responseText = responseTransformed
} else {
responseText = JSON.stringify(responseTransformed)
}
if (needsRetry(responseText)) {
if (retryCount < retryLimit) {
retryCount++
let retryResponse = await makeRequest(
const retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
)
).catch((err) => {
throw err
})
retryCount = 0
etag = retryResponse.etag
@@ -80,57 +141,14 @@ export async function makeRequest<T>(
}
}
if (response.status === 401) {
try {
body = JSON.parse(body)
etag = response.headers.get('ETag')
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
body.message || ''
}`
body = JSON.stringify(body)
} catch (_) {}
}
return Promise.reject({ status: response.status, body })
return responseTransformed
}
} else {
if (response.status === 204) {
return Promise.resolve()
}
const responseTransformed = await responseTransform(response)
let responseText = ''
if (typeof responseTransformed === 'string') {
responseText = responseTransformed
} else {
responseText = JSON.stringify(responseTransformed)
}
if (needsRetry(responseText)) {
if (retryCount < retryLimit) {
retryCount++
const retryResponse = await makeRequest(
url,
retryRequest || request,
callback,
contentType
)
retryCount = 0
etag = retryResponse.etag
return retryResponse.result
} else {
retryCount = 0
throw new Error('Request retry limit exceeded')
}
}
etag = response.headers.get('ETag')
return responseTransformed
}
})
})
.catch((err) => {
throw err
})
return { result, etag }
}