1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-04-15 18:33:14 +00:00

Compare commits

...

1 Commits

Author SHA1 Message Date
mulahasanovic
fe5f0e87b7 fix: handle session inactivity expiry 2026-04-15 08:57:34 +02:00
4 changed files with 73 additions and 1 deletions

View File

@@ -375,7 +375,7 @@ export class AuthManager {
*
*/
public async logOut() {
this.requestClient.clearCsrfTokens()
this.requestClient.resetInMemoryAuthState()
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
}

View File

@@ -37,6 +37,7 @@ export class RequestClient implements HttpClient {
protected csrfToken: CsrfToken = { headerName: '', value: '' }
protected fileUploadCsrfToken: CsrfToken | undefined
protected httpClient!: AxiosInstance
private isRecoveringFromNetworkError = false
constructor(
protected baseUrl: string,
@@ -77,6 +78,16 @@ export class RequestClient implements HttpClient {
localStorage.setItem('refreshToken', '')
}
public resetInMemoryAuthState() {
this.clearCsrfTokens()
if (typeof localStorage !== 'undefined') {
this.clearLocalStorageTokens()
}
if (typeof document !== 'undefined') {
document.cookie = 'XSRF-TOKEN=; Max-Age=0; Path=/;'
}
}
public getBaseUrl() {
return this.httpClient.defaults.baseURL || ''
}
@@ -687,6 +698,24 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
throw new CertificateError(e.message)
}
if (
e.isAxiosError &&
!response &&
e.code === 'ERR_NETWORK' &&
!this.isRecoveringFromNetworkError
) {
// Opaque ERR_NETWORK usually means the server rejected stale credentials.
// Wipe in-memory auth state so the retry either succeeds
// or surfaces a clean LoginRequiredError.
this.resetInMemoryAuthState()
this.isRecoveringFromNetworkError = true
try {
return await callback()
} finally {
this.isRecoveringFromNetworkError = false
}
}
if (e.message) throw e
else throw prefixMessage(e, 'Error while handling error. ')
}

View File

@@ -23,6 +23,13 @@ export class Sas9RequestClient extends RequestClient {
}
}
public resetInMemoryAuthState() {
super.resetInMemoryAuthState()
if (this.httpClient.defaults.jar) {
;(this.httpClient.defaults.jar as tough.CookieJar).removeAllCookiesSync()
}
}
public async login(username: string, password: string, jobsPath: string) {
const codeInjectorPath = `/User Folders/${username}/My Folder/sasjs/runner`
if (this.httpClient.defaults.jar) {

View File

@@ -589,6 +589,42 @@ ${resHeaders[0]}: ${resHeaders[1]}${
requestClient['handleError'](error, () => {}, false)
).resolves.toEqual(undefined)
})
it('should clear CSRF and retry once on opaque ERR_NETWORK', async () => {
const networkError = {
isAxiosError: true,
code: 'ERR_NETWORK',
message: 'Network Error'
}
requestClient['csrfToken'] = { headerName: 'h', value: 'v' }
const callback = jest.fn().mockResolvedValue('ok')
await expect(
requestClient['handleError'](networkError, callback)
).resolves.toEqual('ok')
expect(callback).toHaveBeenCalledTimes(1)
expect(requestClient['csrfToken']).toEqual({ headerName: '', value: '' })
})
it('should not loop if retry also fails with ERR_NETWORK', async () => {
const networkError = {
isAxiosError: true,
code: 'ERR_NETWORK',
message: 'Network Error'
}
const innerHandle = jest.fn(() =>
requestClient['handleError'](networkError, () =>
Promise.reject(networkError)
)
)
await expect(
requestClient['handleError'](networkError, innerHandle)
).rejects.toEqual(networkError)
expect(innerHandle).toHaveBeenCalledTimes(1)
})
})
})