diff --git a/src/request/RequestClient.ts b/src/request/RequestClient.ts index cccb274..de87541 100644 --- a/src/request/RequestClient.ts +++ b/src/request/RequestClient.ts @@ -79,22 +79,33 @@ export class RequestClient implements HttpClient { } public resetInMemoryAuthState() { + const logger = process.logger || console + const clearedCookies: string[] = [] + this.clearCsrfTokens() if (typeof localStorage !== 'undefined') { this.clearLocalStorageTokens() } if (typeof document !== 'undefined') { - this.clearAllCookies() + clearedCookies.push(...this.clearAllCookies()) } + + logger.warn('[resetInMemoryAuthState] cleared', { + cookies: clearedCookies, + localStorage: typeof localStorage !== 'undefined' + }) } - private clearAllCookies() { + private clearAllCookies(): string[] { const cookies = document.cookie.split(';') + const cleared: string[] = [] for (const cookie of cookies) { const name = cookie.split('=')[0].trim() if (!name) continue document.cookie = `${name}=; Max-Age=0; Path=/;` + cleared.push(name) } + return cleared } public getBaseUrl() { @@ -374,9 +385,14 @@ export class RequestClient implements HttpClient { const csrfTokenKey = Object.keys(params).find((k) => k?.toLowerCase().includes('csrf') ) + const logger = process.logger || console + if (csrfTokenKey) { this.csrfToken.value = params[csrfTokenKey] this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token' + logger.warn('[authorize] CSRF from form', { + headerName: this.csrfToken.headerName + }) } const formData = new FormData() @@ -391,15 +407,23 @@ export class RequestClient implements HttpClient { throw new Error('Auth Form URL is null or undefined.') } + logger.warn('[authorize] posting to', { authUrl }) + return await this.httpClient .post(authUrl, formData, { responseType: 'text', headers: { Accept: '*/*', 'Content-Type': 'text/plain' } }) - .then((res) => res.data) + .then((res) => { + logger.warn('[authorize] success', { status: res.status }) + return res.data + }) .catch((error) => { - const logger = process.logger || console - logger.error(error) + logger.error('[authorize] failed', { + code: error?.code, + status: error?.response?.status, + message: error?.message + }) }) } @@ -598,9 +622,16 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} protected parseAndSetCsrfToken = (response: AxiosResponse) => { const token = this.parseCsrfToken(response) + const logger = process.logger || console if (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) { + const logger = process.logger || console + logger.warn(`[handleError] ${step}`, details || '') + } + protected handleError = async ( e: any, callback: any, @@ -627,7 +663,19 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} ) => { 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) { + this.logHandleError('AuthorizeError — fetching confirmUrl', { + confirmUrl: e.confirmUrl + }) const res = await this.httpClient .get(e.confirmUrl, { responseType: 'text', @@ -637,13 +685,24 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} 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) => { throw prefixMessage(err, 'Error while authorizing request. ') }) } + this.logHandleError('AuthorizeError — retrying callback') return await callback().catch((err: any) => { + this.logHandleError('AuthorizeError — callback failed', { + errorType: err?.constructor?.name, + code: err?.code, + message: err?.message + }) throw prefixMessage( err, 'Error while executing callback in handleError. ' @@ -652,12 +711,14 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} } if (e instanceof LoginRequiredError) { + this.logHandleError('LoginRequiredError — clearing CSRF and re-throwing') this.clearCsrfTokens() throw e } if (e instanceof InvalidSASjsCsrfError) { + this.logHandleError('InvalidSASjsCsrfError — re-fetching CSRF cookie') // Fetching root and creating CSRF cookie await this.httpClient .get('/', { @@ -669,13 +730,22 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} response.data )?.[1] + this.logHandleError( + 'InvalidSASjsCsrfError — cookie found: ' + !!cookie + ) if (cookie) document.cookie = cookie }) .catch((err) => { throw prefixMessage(err, 'Error while re-fetching CSRF token.') }) + this.logHandleError('InvalidSASjsCsrfError — retrying callback') return await callback().catch((err: any) => { + this.logHandleError('InvalidSASjsCsrfError — callback failed', { + errorType: err?.constructor?.name, + code: err?.code, + message: err?.message + }) throw prefixMessage( err, 'Error while executing callback in handleError. ' @@ -686,8 +756,20 @@ ${resHeaders}${parsedResBody ? `\n\n${parsedResBody}` : ''} if (response?.status === 403 || response?.status === 449) { 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) => { + this.logHandleError('403/449 — callback failed', { + errorType: err?.constructor?.name, + code: err?.code, + message: err?.message + }) throw prefixMessage( err, '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 } else if (response?.status === 404) { 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. // Wipe in-memory auth state so the retry either succeeds // or surfaces a clean LoginRequiredError. + this.logHandleError('ERR_NETWORK — clearing all auth state and retrying') this.resetInMemoryAuthState() this.isRecoveringFromNetworkError = true try { return await callback() - } catch { + } catch (retryErr: any) { // Retry also failed — session is dead, surface LoginRequiredError // 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() } finally { this.isRecoveringFromNetworkError = false } } + this.logHandleError('unhandled — throwing as-is', { + message: e?.message, + code: e?.code + }) if (e.message) throw e else throw prefixMessage(e, 'Error while handling error. ') }