mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-03 10:40:06 +00:00
875 lines
24 KiB
TypeScript
875 lines
24 KiB
TypeScript
import {
|
||
AxiosError,
|
||
AxiosInstance,
|
||
AxiosRequestConfig,
|
||
AxiosRequestHeaders,
|
||
AxiosResponse
|
||
} from 'axios'
|
||
import * as https from 'https'
|
||
import { CsrfToken } from '..'
|
||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||
import {
|
||
AuthorizeError,
|
||
LoginRequiredError,
|
||
NotFoundError,
|
||
InternalServerError,
|
||
JobExecutionError,
|
||
CertificateError
|
||
} from '../types/errors'
|
||
import { SASjsRequest, HttpClient, VerboseMode } from '../types'
|
||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||
import { prefixMessage } from '@sasjs/utils/error'
|
||
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||
import {
|
||
parseGeneratedCode,
|
||
parseSourceCode,
|
||
createAxiosInstance
|
||
} from '../utils'
|
||
import { InvalidSASjsCsrfError } from '../types/errors/InvalidSASjsCsrfError'
|
||
import { inspect } from 'util'
|
||
|
||
export class RequestClient implements HttpClient {
|
||
private requests: SASjsRequest[] = []
|
||
private requestsLimit: number = 10
|
||
private httpInterceptor?: number
|
||
private verboseMode: VerboseMode = false
|
||
|
||
protected csrfToken: CsrfToken = { headerName: '', value: '' }
|
||
protected fileUploadCsrfToken: CsrfToken | undefined
|
||
protected httpClient!: AxiosInstance
|
||
|
||
constructor(
|
||
protected baseUrl: string,
|
||
httpsAgentOptions?: https.AgentOptions,
|
||
requestsLimit?: number,
|
||
verboseMode?: VerboseMode
|
||
) {
|
||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||
|
||
if (requestsLimit) this.requestsLimit = requestsLimit
|
||
|
||
if (verboseMode) {
|
||
this.setVerboseMode(verboseMode)
|
||
this.enableVerboseMode()
|
||
}
|
||
}
|
||
|
||
public setConfig(baseUrl: string, httpsAgentOptions?: https.AgentOptions) {
|
||
this.createHttpClient(baseUrl, httpsAgentOptions)
|
||
}
|
||
|
||
public saveLocalStorageToken(accessToken: string, refreshToken: string) {
|
||
localStorage.setItem('accessToken', accessToken)
|
||
localStorage.setItem('refreshToken', refreshToken)
|
||
}
|
||
|
||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
|
||
}
|
||
|
||
public clearCsrfTokens() {
|
||
this.csrfToken = { headerName: '', value: '' }
|
||
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
||
}
|
||
|
||
public clearLocalStorageTokens() {
|
||
localStorage.setItem('accessToken', '')
|
||
localStorage.setItem('refreshToken', '')
|
||
}
|
||
|
||
public getBaseUrl() {
|
||
return this.httpClient.defaults.baseURL || ''
|
||
}
|
||
|
||
/**
|
||
* this method returns all requests, an array of SASjsRequest type
|
||
* @returns SASjsRequest[]
|
||
*/
|
||
public getRequests = () => this.requests
|
||
|
||
/**
|
||
* this method clears the requests array, i.e set to empty
|
||
*/
|
||
public clearRequests = () => {
|
||
this.requests = []
|
||
}
|
||
|
||
/**
|
||
* this method appends the response from sasjs request to requests array
|
||
* @param response - response from sasjs request
|
||
* @param program - name of program
|
||
* @param debug - a boolean that indicates whether debug was enabled or not
|
||
*/
|
||
public appendRequest(response: any, program: string, debug: boolean) {
|
||
let sourceCode = ''
|
||
let generatedCode = ''
|
||
let sasWork = null
|
||
|
||
if (debug) {
|
||
if (response?.log) {
|
||
sourceCode = parseSourceCode(response.log)
|
||
generatedCode = parseGeneratedCode(response.log)
|
||
|
||
if (response?.result) {
|
||
sasWork = response.result.WORK
|
||
} else {
|
||
sasWork = response.log
|
||
}
|
||
} else if (response?.result) {
|
||
// We parse only if it's a string, otherwise it would throw error
|
||
if (typeof response.result === 'string') {
|
||
sourceCode = parseSourceCode(response.result)
|
||
generatedCode = parseGeneratedCode(response.result)
|
||
}
|
||
|
||
sasWork = response.result.WORK
|
||
}
|
||
}
|
||
|
||
const stringifiedResult =
|
||
typeof response?.result === 'string'
|
||
? response?.result
|
||
: JSON.stringify(response?.result, null, 2)
|
||
|
||
this.requests.push({
|
||
logFile: response?.log || stringifiedResult || response,
|
||
serviceLink: program,
|
||
timestamp: new Date(),
|
||
sourceCode,
|
||
generatedCode,
|
||
SASWORK: sasWork
|
||
})
|
||
|
||
if (this.requests.length > this.requestsLimit) {
|
||
this.requests.splice(0, 1)
|
||
}
|
||
}
|
||
|
||
public async get<T>(
|
||
url: string,
|
||
accessToken: string | undefined,
|
||
contentType: string = 'application/json',
|
||
overrideHeaders: { [key: string]: string | number } = {},
|
||
debug: boolean = false
|
||
): Promise<{ result: T; etag: string; status: number }> {
|
||
const headers = {
|
||
...this.getHeaders(accessToken, contentType),
|
||
...overrideHeaders
|
||
}
|
||
|
||
const requestConfig: AxiosRequestConfig = {
|
||
headers,
|
||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||
withXSRFToken: true
|
||
}
|
||
|
||
if (contentType === 'text/plain') {
|
||
requestConfig.transformResponse = undefined
|
||
}
|
||
|
||
return this.httpClient
|
||
.get<T>(url, requestConfig)
|
||
.then((response) => {
|
||
throwIfError(response)
|
||
|
||
return this.parseResponse<T>(response)
|
||
})
|
||
.catch(async (e: any) => {
|
||
return await this.handleError(
|
||
e,
|
||
() =>
|
||
this.get<T>(url, accessToken, contentType, overrideHeaders).catch(
|
||
(err) => {
|
||
throw prefixMessage(
|
||
err,
|
||
'Error while executing handle error callback. '
|
||
)
|
||
}
|
||
),
|
||
debug
|
||
)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @param contentType Newer version of Axios is more strict so if you don't
|
||
* set the contentType to `form data` while sending a FormData object
|
||
* application/json will be used by default, axios won’t treat it as FormData.
|
||
* Instead, it serializes data as JSON—resulting in a payload like
|
||
* {"sometable":{}} and we lose the multipart/form-data formatting.
|
||
*/
|
||
public async post<T>(
|
||
url: string,
|
||
data: any,
|
||
accessToken: string | undefined,
|
||
contentType = 'application/json',
|
||
overrideHeaders: { [key: string]: string | number } = {},
|
||
additionalSettings: { [key: string]: string | number } = {}
|
||
): Promise<{ result: T; etag: string }> {
|
||
const headers = {
|
||
...this.getHeaders(accessToken, contentType),
|
||
...overrideHeaders
|
||
}
|
||
|
||
return this.httpClient
|
||
.post<T>(url, data, {
|
||
headers,
|
||
withXSRFToken: true,
|
||
...additionalSettings
|
||
})
|
||
.then((response) => {
|
||
throwIfError(response)
|
||
|
||
return this.parseResponse<T>(response)
|
||
})
|
||
.catch(async (e: any) => {
|
||
return await this.handleError(e, () =>
|
||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||
)
|
||
})
|
||
}
|
||
|
||
public async put<T>(
|
||
url: string,
|
||
data: any,
|
||
accessToken: string | undefined,
|
||
overrideHeaders: { [key: string]: string | number } = {}
|
||
): Promise<{ result: T; etag: string }> {
|
||
const headers = {
|
||
...this.getHeaders(accessToken, 'application/json'),
|
||
...overrideHeaders
|
||
}
|
||
|
||
return this.httpClient
|
||
.put<T>(url, data, { headers, withXSRFToken: true })
|
||
.then((response) => {
|
||
throwIfError(response)
|
||
return this.parseResponse<T>(response)
|
||
})
|
||
.catch(async (e: any) => {
|
||
return await this.handleError(e, () =>
|
||
this.put<T>(url, data, accessToken, overrideHeaders)
|
||
)
|
||
})
|
||
}
|
||
|
||
public async delete<T>(
|
||
url: string,
|
||
accessToken?: string
|
||
): Promise<{ result: T; etag: string }> {
|
||
const headers = this.getHeaders(accessToken, 'application/json')
|
||
|
||
return this.httpClient
|
||
.delete<T>(url, { headers, withXSRFToken: true })
|
||
.then((response) => {
|
||
throwIfError(response)
|
||
return this.parseResponse<T>(response)
|
||
})
|
||
.catch(async (e: any) => {
|
||
return await this.handleError(e, () => this.delete<T>(url, accessToken))
|
||
})
|
||
}
|
||
|
||
public async patch<T>(
|
||
url: string,
|
||
data: any = {},
|
||
accessToken?: string
|
||
): Promise<{ result: T; etag: string }> {
|
||
const headers = this.getHeaders(accessToken, 'application/json')
|
||
|
||
return this.httpClient
|
||
.patch<T>(url, data, { headers, withXSRFToken: true })
|
||
.then((response) => {
|
||
throwIfError(response)
|
||
return this.parseResponse<T>(response)
|
||
})
|
||
.catch(async (e: any) => {
|
||
return await this.handleError(e, () =>
|
||
this.patch<T>(url, data, accessToken)
|
||
)
|
||
})
|
||
}
|
||
|
||
public async uploadFile(
|
||
url: string,
|
||
content: string,
|
||
accessToken?: string
|
||
): Promise<any> {
|
||
const headers = this.getHeaders(accessToken, 'application/json')
|
||
|
||
if (this.fileUploadCsrfToken?.value) {
|
||
headers[this.fileUploadCsrfToken.headerName] =
|
||
this.fileUploadCsrfToken.value
|
||
}
|
||
|
||
try {
|
||
const response = await this.httpClient.post(url, content, {
|
||
headers,
|
||
transformRequest: (requestBody) => requestBody
|
||
})
|
||
|
||
return {
|
||
result: response.data,
|
||
etag: response.headers['etag'] as string
|
||
}
|
||
} catch (e: any) {
|
||
const response = e.response as AxiosResponse
|
||
if (response?.status === 403 || response?.status === 449) {
|
||
this.parseAndSetFileUploadCsrfToken(response)
|
||
|
||
if (this.fileUploadCsrfToken) {
|
||
return this.uploadFile(url, content, accessToken)
|
||
}
|
||
throw e
|
||
}
|
||
throw e
|
||
}
|
||
}
|
||
|
||
public authorize = async (response: string) => {
|
||
let authUrl: string | null = null
|
||
const params: any = {}
|
||
|
||
const responseBody = response.split('<body>')[1].split('</body>')[0]
|
||
const bodyElement = document.createElement('div')
|
||
bodyElement.innerHTML = responseBody
|
||
|
||
const form = bodyElement.querySelector('#application_authorization')
|
||
authUrl = form ? this.baseUrl + form.getAttribute('action') : null
|
||
|
||
const inputs: any = form?.querySelectorAll('input')
|
||
|
||
for (const input of inputs) {
|
||
if (input.name === 'user_oauth_approval') {
|
||
input.value = 'true'
|
||
}
|
||
|
||
params[input.name] = input.value
|
||
}
|
||
|
||
const csrfTokenKey = Object.keys(params).find((k) =>
|
||
k?.toLowerCase().includes('csrf')
|
||
)
|
||
if (csrfTokenKey) {
|
||
this.csrfToken.value = params[csrfTokenKey]
|
||
this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
|
||
}
|
||
|
||
const formData = new FormData()
|
||
|
||
for (const key in params) {
|
||
if (params.hasOwnProperty(key)) {
|
||
formData.append(key, params[key])
|
||
}
|
||
}
|
||
|
||
if (!authUrl) {
|
||
throw new Error('Auth Form URL is null or undefined.')
|
||
}
|
||
|
||
return await this.httpClient
|
||
.post(authUrl, formData, {
|
||
responseType: 'text',
|
||
headers: { Accept: '*/*', 'Content-Type': 'text/plain' }
|
||
})
|
||
.then((res) => res.data)
|
||
.catch((error) => {
|
||
const logger = process.logger || console
|
||
logger.error(error)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Adds colors to the string.
|
||
* If verboseMode is set to 'bleached', colors should be disabled
|
||
* @param str - string to be prettified.
|
||
* @returns - prettified string
|
||
*/
|
||
private prettifyString = (str: any) =>
|
||
inspect(str, { colors: this.verboseMode !== 'bleached' })
|
||
|
||
/**
|
||
* Formats HTTP request/response body.
|
||
* @param body - HTTP request/response body.
|
||
* @returns - formatted string.
|
||
*/
|
||
private parseInterceptedBody = (body: any) => {
|
||
if (!body) return ''
|
||
|
||
let parsedBody
|
||
|
||
// Tries to parse body into JSON object.
|
||
if (typeof body === 'string') {
|
||
try {
|
||
parsedBody = JSON.parse(body)
|
||
} catch (error) {
|
||
parsedBody = body
|
||
}
|
||
} else {
|
||
parsedBody = body
|
||
}
|
||
|
||
const bodyLines = this.prettifyString(parsedBody).split('\n')
|
||
|
||
// Leaves first 50 lines
|
||
if (bodyLines.length > 51) {
|
||
bodyLines.splice(50)
|
||
bodyLines.push('...')
|
||
}
|
||
|
||
return bodyLines.join('\n')
|
||
}
|
||
|
||
private handleAxiosResponse = (response: AxiosResponse) => {
|
||
const { status, config, request, data } = response
|
||
|
||
const reqHeaders = request?._header ?? 'Not provided\n'
|
||
const rawHeaders = request?.res?.rawHeaders ?? ['Not provided']
|
||
|
||
const resHeaders = this.formatHeaders(rawHeaders)
|
||
const parsedResBody = this.parseInterceptedBody(data)
|
||
|
||
process.logger?.info(`HTTP Request (first 50 lines):
|
||
${reqHeaders}${this.parseInterceptedBody(config.data)}
|
||
|
||
HTTP Response Code: ${this.prettifyString(status)}
|
||
|
||
HTTP Response (first 50 lines):
|
||
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
||
`)
|
||
|
||
return response
|
||
}
|
||
|
||
private handleAxiosError = (error: AxiosError) => {
|
||
// Message indicating absent value.
|
||
const noValueMessage = 'Not provided'
|
||
const { response, request, config } = error
|
||
|
||
// Fallback request object that can be safely used to form request summary.
|
||
// _header is not present in responses with status 1**
|
||
// rawHeaders are not present in responses with status 1**
|
||
let fallbackRequest = {
|
||
_header: `${noValueMessage}\n`,
|
||
res: { rawHeaders: [noValueMessage] }
|
||
}
|
||
|
||
if (request) {
|
||
fallbackRequest = {
|
||
_header:
|
||
request._header ?? request._currentRequest?._header ?? noValueMessage,
|
||
res: { rawHeaders: request.res?.rawHeaders ?? [noValueMessage] }
|
||
}
|
||
}
|
||
|
||
// Fallback response object that can be safely used to form response summary.
|
||
let fallbackResponse = response || {
|
||
status: noValueMessage,
|
||
request: fallbackRequest,
|
||
config: config || {
|
||
data: noValueMessage,
|
||
headers: {} as AxiosRequestHeaders
|
||
},
|
||
data: noValueMessage
|
||
}
|
||
|
||
const { status, request: req, data: resData } = fallbackResponse
|
||
const { _header: reqHeaders, res } = req
|
||
|
||
const resHeaders = this.formatHeaders(res.rawHeaders)
|
||
const parsedResBody = this.parseInterceptedBody(resData)
|
||
|
||
process.logger?.info(`HTTP Request (first 50 lines):
|
||
${reqHeaders}${this.parseInterceptedBody(config?.data)}
|
||
|
||
HTTP Response Code: ${this.prettifyString(status)}
|
||
|
||
HTTP Response (first 50 lines):
|
||
${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
||
`)
|
||
|
||
return error
|
||
}
|
||
|
||
// Converts an array of strings into a single string with the following format:
|
||
// <headerName>: <headerValue>
|
||
private formatHeaders = (rawHeaders: string[]): string => {
|
||
return rawHeaders.reduce((acc, value, i) => {
|
||
if (i % 2 === 0) {
|
||
acc += `${i === 0 ? '' : '\n'}${value}`
|
||
} else {
|
||
acc += `: ${value}`
|
||
}
|
||
return acc
|
||
}, '')
|
||
}
|
||
|
||
/**
|
||
* Sets verbose mode.
|
||
* @param verboseMode - value of the verbose mode, can be true, false or bleached(without extra colors).
|
||
*/
|
||
public setVerboseMode = (verboseMode: VerboseMode) => {
|
||
this.verboseMode = verboseMode
|
||
|
||
if (this.verboseMode) this.enableVerboseMode()
|
||
else this.disableVerboseMode()
|
||
}
|
||
|
||
/**
|
||
* Turns on verbose mode to log every HTTP response.
|
||
* @param successCallBack - function that should be triggered on every HTTP response with the status 2**.
|
||
* @param errorCallBack - function that should be triggered on every HTTP response with the status different from 2**.
|
||
*/
|
||
public enableVerboseMode = (
|
||
successCallBack = this.handleAxiosResponse,
|
||
errorCallBack = this.handleAxiosError
|
||
) => {
|
||
this.httpInterceptor = this.httpClient.interceptors.response.use(
|
||
successCallBack,
|
||
errorCallBack
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Turns off verbose mode to log every HTTP response.
|
||
*/
|
||
public disableVerboseMode = () => {
|
||
if (this.httpInterceptor) {
|
||
this.httpClient.interceptors.response.eject(this.httpInterceptor)
|
||
}
|
||
}
|
||
|
||
protected getHeaders = (
|
||
accessToken: string | undefined,
|
||
contentType: string
|
||
) => {
|
||
const headers: any = {}
|
||
|
||
if (contentType !== 'application/x-www-form-urlencoded') {
|
||
headers['Content-Type'] = contentType
|
||
}
|
||
|
||
if (contentType === 'application/json') {
|
||
headers.Accept = 'application/json'
|
||
} else {
|
||
headers.Accept = '*/*'
|
||
}
|
||
if (accessToken) {
|
||
headers.Authorization = `Bearer ${accessToken}`
|
||
}
|
||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||
}
|
||
|
||
return headers
|
||
}
|
||
|
||
protected parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
||
const token = this.parseCsrfToken(response)
|
||
|
||
if (token) {
|
||
this.fileUploadCsrfToken = token
|
||
}
|
||
}
|
||
|
||
protected parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||
const token = this.parseCsrfToken(response)
|
||
|
||
if (token) {
|
||
this.csrfToken = token
|
||
}
|
||
}
|
||
|
||
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
|
||
const tokenHeader = (
|
||
response.headers['x-csrf-header'] as string
|
||
)?.toLowerCase()
|
||
|
||
if (tokenHeader) {
|
||
const token = response.headers[tokenHeader]
|
||
const csrfToken = {
|
||
headerName: tokenHeader,
|
||
value: token || ''
|
||
}
|
||
|
||
return csrfToken
|
||
}
|
||
}
|
||
|
||
protected handleError = async (
|
||
e: any,
|
||
callback: any,
|
||
debug: boolean = false
|
||
) => {
|
||
const response = e.response as AxiosResponse
|
||
|
||
if (e instanceof AuthorizeError) {
|
||
const res = await this.httpClient
|
||
.get(e.confirmUrl, {
|
||
responseType: 'text',
|
||
headers: { 'Content-Type': 'text/plain', Accept: '*/*' }
|
||
})
|
||
.catch((err) => {
|
||
throw prefixMessage(err, 'Error while getting error confirmUrl. ')
|
||
})
|
||
|
||
if (isAuthorizeFormRequired(res?.data as string)) {
|
||
await this.authorize(res.data as string).catch((err) => {
|
||
throw prefixMessage(err, 'Error while authorizing request. ')
|
||
})
|
||
}
|
||
|
||
return await callback().catch((err: any) => {
|
||
throw prefixMessage(
|
||
err,
|
||
'Error while executing callback in handleError. '
|
||
)
|
||
})
|
||
}
|
||
|
||
if (e instanceof LoginRequiredError) {
|
||
this.clearCsrfTokens()
|
||
|
||
throw e
|
||
}
|
||
|
||
if (e instanceof InvalidSASjsCsrfError) {
|
||
// Fetching root and creating CSRF cookie
|
||
await this.httpClient
|
||
.get('/', {
|
||
withXSRFToken: true
|
||
})
|
||
.then((response) => {
|
||
const cookie =
|
||
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||
response.data
|
||
)?.[1]
|
||
|
||
if (cookie) document.cookie = cookie
|
||
})
|
||
.catch((err) => {
|
||
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
||
})
|
||
|
||
return await callback().catch((err: any) => {
|
||
throw prefixMessage(
|
||
err,
|
||
'Error while executing callback in handleError. '
|
||
)
|
||
})
|
||
}
|
||
|
||
if (response?.status === 403 || response?.status === 449) {
|
||
this.parseAndSetCsrfToken(response)
|
||
|
||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||
return await callback().catch((err: any) => {
|
||
throw prefixMessage(
|
||
err,
|
||
'Error while executing callback in handleError. '
|
||
)
|
||
})
|
||
}
|
||
|
||
throw e
|
||
} else if (response?.status === 404) {
|
||
throw new NotFoundError(response.config.url!)
|
||
} else if (response?.status === 502) {
|
||
if (debug) throw new InternalServerError()
|
||
else return
|
||
}
|
||
|
||
if (e.isAxiosError && e.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
||
throw new CertificateError(e.message)
|
||
}
|
||
|
||
if (e.message) throw e
|
||
else throw prefixMessage(e, 'Error while handling error. ')
|
||
}
|
||
|
||
protected parseResponse<T>(response: AxiosResponse<any>) {
|
||
const etag = response?.headers ? response.headers['etag'] : ''
|
||
let parsedResponse
|
||
let includeSAS9Log: boolean = false
|
||
|
||
try {
|
||
if (typeof response.data === 'string') {
|
||
parsedResponse = JSON.parse(response.data)
|
||
} else {
|
||
parsedResponse = response.data
|
||
}
|
||
} catch {
|
||
try {
|
||
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
|
||
} catch {
|
||
parsedResponse = response.data
|
||
}
|
||
|
||
includeSAS9Log = true
|
||
}
|
||
|
||
let responseToReturn: {
|
||
result: T
|
||
etag: any
|
||
log?: string
|
||
status: number
|
||
} = {
|
||
result: parsedResponse as T,
|
||
etag,
|
||
status: response.status
|
||
}
|
||
|
||
if (includeSAS9Log) {
|
||
responseToReturn.log = response.data
|
||
}
|
||
|
||
return responseToReturn
|
||
}
|
||
|
||
private createHttpClient(
|
||
baseUrl: string,
|
||
httpsAgentOptions?: https.AgentOptions
|
||
) {
|
||
const httpsAgent = httpsAgentOptions
|
||
? new https.Agent(httpsAgentOptions)
|
||
: undefined
|
||
|
||
this.httpClient = createAxiosInstance(baseUrl, httpsAgent)
|
||
|
||
this.httpClient.defaults.validateStatus = (status) => {
|
||
return status >= 200 && status <= 401
|
||
}
|
||
}
|
||
}
|
||
|
||
export const throwIfError = (response: AxiosResponse) => {
|
||
switch (response.status) {
|
||
case 400:
|
||
if (
|
||
typeof response.data === 'object' &&
|
||
response.data.error === 'invalid_grant'
|
||
) {
|
||
// In SASVIYA when trying to get access token, if auth code is wrong status code will be 400 so in such case we return login required error.
|
||
throw new LoginRequiredError(response.data)
|
||
}
|
||
|
||
if (
|
||
typeof response.data === 'string' &&
|
||
response.data.toLowerCase() === 'invalid csrf token!'
|
||
) {
|
||
throw new InvalidSASjsCsrfError()
|
||
}
|
||
break
|
||
case 401:
|
||
if (typeof response.data === 'object') {
|
||
throw new LoginRequiredError(response.data)
|
||
} else {
|
||
throw new LoginRequiredError()
|
||
}
|
||
}
|
||
|
||
if (response.data?.entityID?.includes('login')) {
|
||
throw new LoginRequiredError()
|
||
}
|
||
|
||
if (
|
||
typeof response.data === 'string' &&
|
||
isAuthorizeFormRequired(response.data)
|
||
) {
|
||
throw new AuthorizeError(
|
||
'Authorization required',
|
||
response.request.responseURL
|
||
)
|
||
}
|
||
|
||
if (
|
||
typeof response.data === 'string' &&
|
||
isLogInRequired(response.data) &&
|
||
!response.config?.url?.includes('/SASLogon/login')
|
||
) {
|
||
throw new LoginRequiredError()
|
||
}
|
||
if (response.data?.auth_request) {
|
||
const authorizeRequestUrl = response.request.responseURL
|
||
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
|
||
}
|
||
|
||
if (response.config?.url?.includes('sasAuthError')) {
|
||
throw new SAS9AuthError()
|
||
}
|
||
|
||
const error = parseError(response.data as string)
|
||
|
||
if (error) {
|
||
throw error
|
||
}
|
||
}
|
||
|
||
const parseError = (data: string) => {
|
||
if (!data) return null
|
||
|
||
try {
|
||
const responseJson = JSON.parse(data?.replace(/[\n\r]/g, ' '))
|
||
if (responseJson.errorCode && responseJson.message) {
|
||
return new JobExecutionError(
|
||
responseJson.errorCode,
|
||
responseJson.message,
|
||
data?.replace(/[\n\r]/g, ' ')
|
||
)
|
||
}
|
||
} catch (_) {}
|
||
|
||
try {
|
||
const hasError = data?.includes('{"errorCode')
|
||
if (hasError) {
|
||
const parts = data.split('{"errorCode')
|
||
if (parts.length > 1) {
|
||
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
|
||
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
|
||
return new JobExecutionError(
|
||
errorJson.errorCode,
|
||
errorJson.message,
|
||
data?.replace(/[\n\r]/g, '\n')
|
||
)
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
|
||
try {
|
||
const hasError = !!data?.match(/stored process not found: /i)
|
||
if (hasError) {
|
||
const parts = data.split(/stored process not found: /i)
|
||
if (parts.length > 1) {
|
||
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
|
||
const message = storedProcessPath.endsWith('runner')
|
||
? `SASJS runner not found. Here's the link (https://cli.sasjs.io/auth/#sasjs-runner) to the SAS code for registering the SASjs runner`
|
||
: `Stored process not found: ${storedProcessPath}`
|
||
return new JobExecutionError(500, message, '')
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
|
||
try {
|
||
// There are some edge cases in which the SAS mp_abort macro
|
||
// (https://core.sasjs.io/mp__abort_8sas.html) is unable to
|
||
// provide a clean exit. In this case the JSON response will
|
||
// be wrapped in >>weboutBEGIN<< and >>weboutEND<< strings.
|
||
// Therefore, if the first string exists, we won't throw an
|
||
// error just yet (the parser may yet throw one instead)
|
||
const hasError =
|
||
!data?.match(/>>weboutBEGIN<</) &&
|
||
!!data?.match(/Stored Process Error/i) &&
|
||
!!data?.match(/This request completed with errors./i)
|
||
if (hasError) {
|
||
const parts = data.split('<h2>SAS Log</h2>')
|
||
if (parts.length > 1) {
|
||
const log = parts[1].split('<pre>')[1].split('</pre>')[0]
|
||
const message = `This request completed with errors.`
|
||
return new JobExecutionError(500, message, log)
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
|
||
return null
|
||
}
|