mirror of
https://github.com/sasjs/adapter.git
synced 2026-04-21 05:01:31 +00:00
chore: add diagnostic logging to session recovery flow
This commit is contained in:
@@ -79,22 +79,33 @@ export class RequestClient implements HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public resetInMemoryAuthState() {
|
public resetInMemoryAuthState() {
|
||||||
|
const logger = process.logger || console
|
||||||
|
const clearedCookies: string[] = []
|
||||||
|
|
||||||
this.clearCsrfTokens()
|
this.clearCsrfTokens()
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
this.clearLocalStorageTokens()
|
this.clearLocalStorageTokens()
|
||||||
}
|
}
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
this.clearAllCookies()
|
clearedCookies.push(...this.clearAllCookies())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearAllCookies() {
|
logger.warn('[resetInMemoryAuthState] cleared', {
|
||||||
|
cookies: clearedCookies,
|
||||||
|
localStorage: typeof localStorage !== 'undefined'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearAllCookies(): string[] {
|
||||||
const cookies = document.cookie.split(';')
|
const cookies = document.cookie.split(';')
|
||||||
|
const cleared: string[] = []
|
||||||
for (const cookie of cookies) {
|
for (const cookie of cookies) {
|
||||||
const name = cookie.split('=')[0].trim()
|
const name = cookie.split('=')[0].trim()
|
||||||
if (!name) continue
|
if (!name) continue
|
||||||
document.cookie = `${name}=; Max-Age=0; Path=/;`
|
document.cookie = `${name}=; Max-Age=0; Path=/;`
|
||||||
|
cleared.push(name)
|
||||||
}
|
}
|
||||||
|
return cleared
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBaseUrl() {
|
public getBaseUrl() {
|
||||||
@@ -374,9 +385,14 @@ export class RequestClient implements HttpClient {
|
|||||||
const csrfTokenKey = Object.keys(params).find((k) =>
|
const csrfTokenKey = Object.keys(params).find((k) =>
|
||||||
k?.toLowerCase().includes('csrf')
|
k?.toLowerCase().includes('csrf')
|
||||||
)
|
)
|
||||||
|
const logger = process.logger || console
|
||||||
|
|
||||||
if (csrfTokenKey) {
|
if (csrfTokenKey) {
|
||||||
this.csrfToken.value = params[csrfTokenKey]
|
this.csrfToken.value = params[csrfTokenKey]
|
||||||
this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
|
this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
|
||||||
|
logger.warn('[authorize] CSRF from form', {
|
||||||
|
headerName: this.csrfToken.headerName
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
@@ -391,15 +407,23 @@ export class RequestClient implements HttpClient {
|
|||||||
throw new Error('Auth Form URL is null or undefined.')
|
throw new Error('Auth Form URL is null or undefined.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.warn('[authorize] posting to', { authUrl })
|
||||||
|
|
||||||
return await this.httpClient
|
return await this.httpClient
|
||||||
.post(authUrl, formData, {
|
.post(authUrl, formData, {
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
headers: { Accept: '*/*', 'Content-Type': 'text/plain' }
|
headers: { Accept: '*/*', 'Content-Type': 'text/plain' }
|
||||||
})
|
})
|
||||||
.then((res) => res.data)
|
.then((res) => {
|
||||||
|
logger.warn('[authorize] success', { status: res.status })
|
||||||
|
return res.data
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const logger = process.logger || console
|
logger.error('[authorize] failed', {
|
||||||
logger.error(error)
|
code: error?.code,
|
||||||
|
status: error?.response?.status,
|
||||||
|
message: error?.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,9 +622,16 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
|
|
||||||
protected parseAndSetCsrfToken = (response: AxiosResponse) => {
|
protected parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||||||
const token = this.parseCsrfToken(response)
|
const token = this.parseCsrfToken(response)
|
||||||
|
const logger = process.logger || console
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
this.csrfToken = token
|
this.csrfToken = token
|
||||||
|
logger.warn('[parseAndSetCsrfToken] set', {
|
||||||
|
headerName: token.headerName,
|
||||||
|
hasValue: !!token.value
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.warn('[parseAndSetCsrfToken] no token found in response')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +651,11 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private logHandleError(step: string, details?: Record<string, any>) {
|
||||||
|
const logger = process.logger || console
|
||||||
|
logger.warn(`[handleError] ${step}`, details || '')
|
||||||
|
}
|
||||||
|
|
||||||
protected handleError = async (
|
protected handleError = async (
|
||||||
e: any,
|
e: any,
|
||||||
callback: any,
|
callback: any,
|
||||||
@@ -627,7 +663,19 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
) => {
|
) => {
|
||||||
const response = e.response as AxiosResponse
|
const response = e.response as AxiosResponse
|
||||||
|
|
||||||
|
this.logHandleError('entered', {
|
||||||
|
errorType: e?.constructor?.name,
|
||||||
|
code: e?.code,
|
||||||
|
status: response?.status,
|
||||||
|
url: e?.config?.url || response?.config?.url,
|
||||||
|
hasResponse: !!response,
|
||||||
|
isRecovering: this.isRecoveringFromNetworkError
|
||||||
|
})
|
||||||
|
|
||||||
if (e instanceof AuthorizeError) {
|
if (e instanceof AuthorizeError) {
|
||||||
|
this.logHandleError('AuthorizeError — fetching confirmUrl', {
|
||||||
|
confirmUrl: e.confirmUrl
|
||||||
|
})
|
||||||
const res = await this.httpClient
|
const res = await this.httpClient
|
||||||
.get(e.confirmUrl, {
|
.get(e.confirmUrl, {
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
@@ -637,13 +685,24 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
throw prefixMessage(err, 'Error while getting error confirmUrl. ')
|
throw prefixMessage(err, 'Error while getting error confirmUrl. ')
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isAuthorizeFormRequired(res?.data as string)) {
|
const needsAuthorize = isAuthorizeFormRequired(res?.data as string)
|
||||||
|
this.logHandleError(
|
||||||
|
'AuthorizeError — authorize form required: ' + needsAuthorize
|
||||||
|
)
|
||||||
|
|
||||||
|
if (needsAuthorize) {
|
||||||
await this.authorize(res.data as string).catch((err) => {
|
await this.authorize(res.data as string).catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while authorizing request. ')
|
throw prefixMessage(err, 'Error while authorizing request. ')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logHandleError('AuthorizeError — retrying callback')
|
||||||
return await callback().catch((err: any) => {
|
return await callback().catch((err: any) => {
|
||||||
|
this.logHandleError('AuthorizeError — callback failed', {
|
||||||
|
errorType: err?.constructor?.name,
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message
|
||||||
|
})
|
||||||
throw prefixMessage(
|
throw prefixMessage(
|
||||||
err,
|
err,
|
||||||
'Error while executing callback in handleError. '
|
'Error while executing callback in handleError. '
|
||||||
@@ -652,12 +711,14 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e instanceof LoginRequiredError) {
|
if (e instanceof LoginRequiredError) {
|
||||||
|
this.logHandleError('LoginRequiredError — clearing CSRF and re-throwing')
|
||||||
this.clearCsrfTokens()
|
this.clearCsrfTokens()
|
||||||
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e instanceof InvalidSASjsCsrfError) {
|
if (e instanceof InvalidSASjsCsrfError) {
|
||||||
|
this.logHandleError('InvalidSASjsCsrfError — re-fetching CSRF cookie')
|
||||||
// Fetching root and creating CSRF cookie
|
// Fetching root and creating CSRF cookie
|
||||||
await this.httpClient
|
await this.httpClient
|
||||||
.get('/', {
|
.get('/', {
|
||||||
@@ -669,13 +730,22 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
response.data
|
response.data
|
||||||
)?.[1]
|
)?.[1]
|
||||||
|
|
||||||
|
this.logHandleError(
|
||||||
|
'InvalidSASjsCsrfError — cookie found: ' + !!cookie
|
||||||
|
)
|
||||||
if (cookie) document.cookie = cookie
|
if (cookie) document.cookie = cookie
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
throw prefixMessage(err, 'Error while re-fetching CSRF token.')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.logHandleError('InvalidSASjsCsrfError — retrying callback')
|
||||||
return await callback().catch((err: any) => {
|
return await callback().catch((err: any) => {
|
||||||
|
this.logHandleError('InvalidSASjsCsrfError — callback failed', {
|
||||||
|
errorType: err?.constructor?.name,
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message
|
||||||
|
})
|
||||||
throw prefixMessage(
|
throw prefixMessage(
|
||||||
err,
|
err,
|
||||||
'Error while executing callback in handleError. '
|
'Error while executing callback in handleError. '
|
||||||
@@ -686,8 +756,20 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
if (response?.status === 403 || response?.status === 449) {
|
if (response?.status === 403 || response?.status === 449) {
|
||||||
this.parseAndSetCsrfToken(response)
|
this.parseAndSetCsrfToken(response)
|
||||||
|
|
||||||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
const hasToken = !!(this.csrfToken.headerName && this.csrfToken.value)
|
||||||
|
this.logHandleError('403/449 — parsed CSRF from response', {
|
||||||
|
hasToken,
|
||||||
|
headerName: this.csrfToken.headerName
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasToken) {
|
||||||
|
this.logHandleError('403/449 — retrying callback with new CSRF')
|
||||||
return await callback().catch((err: any) => {
|
return await callback().catch((err: any) => {
|
||||||
|
this.logHandleError('403/449 — callback failed', {
|
||||||
|
errorType: err?.constructor?.name,
|
||||||
|
code: err?.code,
|
||||||
|
message: err?.message
|
||||||
|
})
|
||||||
throw prefixMessage(
|
throw prefixMessage(
|
||||||
err,
|
err,
|
||||||
'Error while executing callback in handleError. '
|
'Error while executing callback in handleError. '
|
||||||
@@ -695,6 +777,9 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logHandleError(
|
||||||
|
'403/449 — no CSRF in response, throwing original error'
|
||||||
|
)
|
||||||
throw e
|
throw e
|
||||||
} else if (response?.status === 404) {
|
} else if (response?.status === 404) {
|
||||||
throw new NotFoundError(response.config.url!)
|
throw new NotFoundError(response.config.url!)
|
||||||
@@ -716,19 +801,32 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''}
|
|||||||
// Opaque ERR_NETWORK usually means the server rejected stale credentials.
|
// Opaque ERR_NETWORK usually means the server rejected stale credentials.
|
||||||
// Wipe in-memory auth state so the retry either succeeds
|
// Wipe in-memory auth state so the retry either succeeds
|
||||||
// or surfaces a clean LoginRequiredError.
|
// or surfaces a clean LoginRequiredError.
|
||||||
|
this.logHandleError('ERR_NETWORK — clearing all auth state and retrying')
|
||||||
this.resetInMemoryAuthState()
|
this.resetInMemoryAuthState()
|
||||||
this.isRecoveringFromNetworkError = true
|
this.isRecoveringFromNetworkError = true
|
||||||
try {
|
try {
|
||||||
return await callback()
|
return await callback()
|
||||||
} catch {
|
} catch (retryErr: any) {
|
||||||
// Retry also failed — session is dead, surface LoginRequiredError
|
// Retry also failed — session is dead, surface LoginRequiredError
|
||||||
// so the app can prompt re-authentication.
|
// so the app can prompt re-authentication.
|
||||||
|
this.logHandleError(
|
||||||
|
'ERR_NETWORK — retry failed, throwing LoginRequiredError',
|
||||||
|
{
|
||||||
|
errorType: retryErr?.constructor?.name,
|
||||||
|
code: retryErr?.code,
|
||||||
|
message: retryErr?.message
|
||||||
|
}
|
||||||
|
)
|
||||||
throw new LoginRequiredError()
|
throw new LoginRequiredError()
|
||||||
} finally {
|
} finally {
|
||||||
this.isRecoveringFromNetworkError = false
|
this.isRecoveringFromNetworkError = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logHandleError('unhandled — throwing as-is', {
|
||||||
|
message: e?.message,
|
||||||
|
code: e?.code
|
||||||
|
})
|
||||||
if (e.message) throw e
|
if (e.message) throw e
|
||||||
else throw prefixMessage(e, 'Error while handling error. ')
|
else throw prefixMessage(e, 'Error while handling error. ')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user