@things-factory/integration-base 9.0.36 → 9.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/connector/headless-connector.js +222 -18
- package/dist-server/engine/connector/headless-connector.js.map +1 -1
- package/dist-server/engine/task/headless-scrap.js +44 -22
- package/dist-server/engine/task/headless-scrap.js.map +1 -1
- package/dist-server/engine/task/utils/headless-request-with-recovery.d.ts +4 -0
- package/dist-server/engine/task/utils/headless-request-with-recovery.js +152 -39
- package/dist-server/engine/task/utils/headless-request-with-recovery.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"headless-scrap.js","sourceRoot":"","sources":["../../../server/engine/task/headless-scrap.ts"],"names":[],"mappings":"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","sourcesContent":["import { URL } from 'url'\n\nimport { TaskRegistry } from '../task-registry'\nimport { ConnectionManager } from '../connection-manager'\n\nasync function HeadlessScrap(step, { logger, data, domain }) {\n const { connection: connectionName, params: stepOptions } = step\n const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout, maxRetries = 2 } = stepOptions || {}\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, params: connectionParams, acquireSessionPage, releasePage, validateSession, reAuthenticateSession } = connection\n const loginPagePath = connectionParams?.loginPagePath || '/login'\n\n const headers = {\n ...requestHeaders\n }\n\n let page = null\n let lastError = null\n\n // 재시도 로직 추가\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n page = await acquireSessionPage()\n\n page.on('console', async msg => {\n console.log(`[browser ${msg.type()}] ${msg.text()}`)\n })\n\n page.on('requestfailed', request => {\n console.log('Request failed:')\n console.log(`- URL: ${request.url()}`)\n console.log(`- Method: ${request.method()}`)\n console.log(`- Failure Text: ${request.failure()?.errorText}`)\n console.log(`- Headers:`, request.headers())\n\n // POST 데이터 (필요한 경우)\n if (request.postData()) {\n console.log(`- Post Data: ${request.postData()}`)\n }\n })\n\n // 302 리디렉션 감지 추가\n page.on('response', response => {\n if ([301, 302, 307, 308].includes(response.status())) {\n const location = response.headers()['location'] || ''\n if (location.includes(loginPagePath) || \n location.includes('/login') || \n location.includes('/signin') || \n location.includes('/auth')) {\n logger.warn(`Login redirect detected: ${location}`)\n throw new Error(`Redirected to login page: ${location}`)\n }\n }\n })\n\n // 첫 번째 시도가 아니면 세션 검증\n if (attempt > 0 && validateSession) {\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`)\n await releasePage(page)\n page = await reAuthenticateSession()\n }\n }\n\n await page.setExtraHTTPHeaders(headers)\n await page.goto(new URL(path, endpoint), { waitUntil: 'networkidle2' })\n\n // 페이지 로드 후 로그인 페이지로 리디렉션되었는지 확인\n const currentUrl = page.url()\n if (currentUrl.includes(loginPagePath) || \n currentUrl.includes('/login') || \n currentUrl.includes('/signin') || \n currentUrl.includes('/auth')) {\n throw new Error(`Page redirected to login: ${currentUrl}`)\n }\n\n // waitForSelectors, waitForTimeout 처리 추가\n if (waitForSelectors) {\n try {\n await page.waitForFunction(\n selectorsString => {\n const selectors = selectorsString.split(',').map(s => s.trim())\n return selectors.every(selector => {\n const el = document.querySelector(selector)\n return el && el.textContent && el.textContent.trim().length > 0\n })\n },\n { timeout: waitForTimeout ? Number(waitForTimeout) : 10000 },\n waitForSelectors // 콤마로 구분된 셀렉터 문자열\n )\n } catch (e) {\n logger.error(`waitForSelectors(${waitForSelectors}) 값이 모두 채워지지 않음:`, e)\n throw e\n }\n } else if (waitForTimeout) {\n await page.waitForTimeout(Number(waitForTimeout))\n }\n\n const result = {}\n\n for (const selector of selectors) {\n const { text, value } = selector\n result[text] = await page.$$eval(value, elements => {\n return elements.map(element => {\n if (\n element instanceof HTMLInputElement ||\n element instanceof HTMLTextAreaElement ||\n element instanceof HTMLSelectElement\n ) {\n return element.value\n } else {\n return element.textContent?.trim()\n }\n })\n })\n }\n\n // 성공시 페이지 릴리즈 후 결과 반환\n if (page) {\n await releasePage(page)\n }\n\n return {\n data: result\n }\n\n } catch (error) {\n lastError = error\n logger.error(`HeadlessScrap attempt ${attempt + 1} failed:`, error)\n\n if (page) {\n await releasePage(page)\n page = null\n }\n\n // 로그인 관련 에러나 복구 가능한 에러가 아니거나 마지막 재시도면 에러 발생\n const errorMessage = error.message?.toLowerCase() || ''\n const isRecoverableError = errorMessage.includes('login') || errorMessage.includes('redirect') || errorMessage.includes('unauthorized') || errorMessage.includes('forbidden') || errorMessage.includes('session')\n\n if (!isRecoverableError || attempt === maxRetries) {\n throw error\n }\n\n logger.info(`Retrying HeadlessScrap... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우\n if (page) {\n await releasePage(page)\n }\n throw lastError || new Error('HeadlessScrap failed after all retry attempts')\n}\n\nHeadlessScrap.parameterSpec = [\n {\n type: 'string',\n name: 'path',\n label: 'path'\n },\n {\n type: 'http-headers',\n name: 'headers',\n label: 'headers'\n },\n {\n type: 'options',\n name: 'selectors',\n label: 'selectors'\n },\n {\n type: 'string',\n name: 'waitForSelectors',\n label: 'wait-for-selectors'\n },\n {\n type: 'string',\n name: 'waitForTimeout',\n label: 'wait-for-timeout'\n },\n {\n type: 'number',\n name: 'maxRetries',\n label: 'maximum-retries',\n value: 2\n }\n]\n\nHeadlessScrap.help = 'integration/task/headless-scrap'\n\nTaskRegistry.registerTaskHandler('headless-scrap', HeadlessScrap)\n"]}
|
1
|
+
{"version":3,"file":"headless-scrap.js","sourceRoot":"","sources":["../../../server/engine/task/headless-scrap.ts"],"names":[],"mappings":"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","sourcesContent":["import { URL } from 'url'\n\nimport { TaskRegistry } from '../task-registry'\nimport { ConnectionManager } from '../connection-manager'\nimport { safeReleasePageResource } from './utils/headless-request-with-recovery'\n\nasync function HeadlessScrap(step, { logger, data, domain }) {\n const { connection: connectionName, params: stepOptions } = step\n const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout, maxRetries = 2 } = stepOptions || {}\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, params: connectionParams, acquireSessionPage, releasePage, validateSession, reAuthenticateSession } = connection\n const loginPagePath = connectionParams?.loginPagePath || '/login'\n\n const headers = {\n ...requestHeaders\n }\n\n let page = null\n let pageResource = null // 리소스 추적 객체\n let lastError = null\n\n // 재시도 로직 추가\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n // 페이지 획득\n const sessionResult = await acquireSessionPage()\n \n // reAuthenticateSession의 반환 형태 확인\n if (sessionResult && typeof sessionResult === 'object' && sessionResult.page) {\n pageResource = sessionResult // {page, browser, requiresManualRelease}\n page = sessionResult.page\n } else {\n page = sessionResult\n pageResource = { page, requiresManualRelease: false }\n }\n\n page.on('console', async msg => {\n console.log(`[browser ${msg.type()}] ${msg.text()}`)\n })\n\n page.on('requestfailed', request => {\n console.log('Request failed:')\n console.log(`- URL: ${request.url()}`)\n console.log(`- Method: ${request.method()}`)\n console.log(`- Failure Text: ${request.failure()?.errorText}`)\n console.log(`- Headers:`, request.headers())\n\n // POST 데이터 (필요한 경우)\n if (request.postData()) {\n console.log(`- Post Data: ${request.postData()}`)\n }\n })\n\n // 302 리디렉션 감지 추가 - 이벤트 핸들러는 비동기 리소스 정리 불가하므로 단순 로깅만\n page.on('response', response => {\n try {\n if ([301, 302, 307, 308].includes(response.status())) {\n const location = response.headers()['location'] || ''\n if (location.includes(loginPagePath) || \n location.includes('/login') || \n location.includes('/signin') || \n location.includes('/auth')) {\n logger.warn(`Login redirect detected during response: ${location}`)\n // Note: Cannot throw from event handler - will be caught in main flow\n }\n }\n } catch (eventError) {\n logger.error('Error in response event handler:', eventError)\n }\n })\n\n // 첫 번째 시도가 아니면 세션 검증\n if (attempt > 0 && validateSession) {\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n \n const reauthResult = await reAuthenticateSession()\n if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {\n pageResource = reauthResult\n page = reauthResult.page\n } else {\n page = reauthResult\n pageResource = { page, requiresManualRelease: false }\n }\n }\n }\n\n await page.setExtraHTTPHeaders(headers)\n await page.goto(new URL(path, endpoint), { waitUntil: 'networkidle2' })\n\n // 페이지 로드 후 로그인 페이지로 리디렉션되었는지 확인\n const currentUrl = page.url()\n if (currentUrl.includes(loginPagePath) || \n currentUrl.includes('/login') || \n currentUrl.includes('/signin') || \n currentUrl.includes('/auth')) {\n throw new Error(`Page redirected to login: ${currentUrl}`)\n }\n\n // waitForSelectors, waitForTimeout 처리 추가\n if (waitForSelectors) {\n try {\n await page.waitForFunction(\n selectorsString => {\n const selectors = selectorsString.split(',').map(s => s.trim())\n return selectors.every(selector => {\n const el = document.querySelector(selector)\n return el && el.textContent && el.textContent.trim().length > 0\n })\n },\n { timeout: waitForTimeout ? Number(waitForTimeout) : 10000 },\n waitForSelectors // 콤마로 구분된 셀렉터 문자열\n )\n } catch (e) {\n logger.error(`waitForSelectors(${waitForSelectors}) 값이 모두 채워지지 않음:`, e)\n throw e\n }\n } else if (waitForTimeout) {\n await page.waitForTimeout(Number(waitForTimeout))\n }\n\n const result = {}\n\n for (const selector of selectors) {\n const { text, value } = selector\n result[text] = await page.$$eval(value, elements => {\n return elements.map(element => {\n if (\n element instanceof HTMLInputElement ||\n element instanceof HTMLTextAreaElement ||\n element instanceof HTMLSelectElement\n ) {\n return element.value\n } else {\n return element.textContent?.trim()\n }\n })\n })\n }\n\n // 성공시 페이지 릴리즈 후 결과 반환\n await safeReleasePageResource(pageResource, releasePage, logger)\n\n return {\n data: result\n }\n\n } catch (error) {\n lastError = error\n logger.error(`HeadlessScrap attempt ${attempt + 1} failed:`, error)\n\n await safeReleasePageResource(pageResource, releasePage, logger)\n page = null\n pageResource = null\n\n // 로그인 관련 에러나 복구 가능한 에러가 아니거나 마지막 재시도면 에러 발생\n const errorMessage = error.message?.toLowerCase() || ''\n const isRecoverableError = errorMessage.includes('login') || errorMessage.includes('redirect') || errorMessage.includes('unauthorized') || errorMessage.includes('forbidden') || errorMessage.includes('session')\n\n if (!isRecoverableError || attempt === maxRetries) {\n throw error\n }\n\n logger.info(`Retrying HeadlessScrap... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우 - 혹시 남은 리소스가 있으면 정리\n if (pageResource) {\n await safeReleasePageResource(pageResource, releasePage, logger)\n }\n throw lastError || new Error('HeadlessScrap failed after all retry attempts')\n}\n\nHeadlessScrap.parameterSpec = [\n {\n type: 'string',\n name: 'path',\n label: 'path'\n },\n {\n type: 'http-headers',\n name: 'headers',\n label: 'headers'\n },\n {\n type: 'options',\n name: 'selectors',\n label: 'selectors'\n },\n {\n type: 'string',\n name: 'waitForSelectors',\n label: 'wait-for-selectors'\n },\n {\n type: 'string',\n name: 'waitForTimeout',\n label: 'wait-for-timeout'\n },\n {\n type: 'number',\n name: 'maxRetries',\n label: 'maximum-retries',\n value: 2\n }\n]\n\nHeadlessScrap.help = 'integration/task/headless-scrap'\n\nTaskRegistry.registerTaskHandler('headless-scrap', HeadlessScrap)\n"]}
|
@@ -16,3 +16,7 @@ export declare function executeHeadlessRequestWithRecovery(connectionName: strin
|
|
16
16
|
data: any;
|
17
17
|
domain: any;
|
18
18
|
}): Promise<any>;
|
19
|
+
/**
|
20
|
+
* Safely release page resource with comprehensive error handling
|
21
|
+
*/
|
22
|
+
export declare function safeReleasePageResource(pageResource: any, releasePage: Function, logger: any): Promise<void>;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
3
|
exports.executeHeadlessRequestWithRecovery = executeHeadlessRequestWithRecovery;
|
4
|
+
exports.safeReleasePageResource = safeReleasePageResource;
|
4
5
|
const utils_1 = require("@things-factory/utils");
|
5
6
|
const connection_manager_1 = require("../../connection-manager");
|
6
7
|
/**
|
@@ -21,12 +22,22 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
21
22
|
const { endpoint, params: connectionParams, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection;
|
22
23
|
const loginPagePath = connectionParams?.loginPagePath || '/login';
|
23
24
|
let page = null;
|
25
|
+
let pageResource = null; // 리소스 추적 객체
|
24
26
|
let lastError = null;
|
25
27
|
// 재시도 로직
|
26
28
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
27
29
|
try {
|
28
30
|
// 페이지 획득
|
29
|
-
|
31
|
+
const sessionResult = await acquireSessionPage();
|
32
|
+
// reAuthenticateSession의 반환 형태 확인
|
33
|
+
if (sessionResult && typeof sessionResult === 'object' && sessionResult.page) {
|
34
|
+
pageResource = sessionResult; // {page, browser, requiresManualRelease}
|
35
|
+
page = sessionResult.page;
|
36
|
+
}
|
37
|
+
else {
|
38
|
+
page = sessionResult;
|
39
|
+
pageResource = { page, requiresManualRelease: false };
|
40
|
+
}
|
30
41
|
// 페이지가 올바른 도메인에 있는지 확인
|
31
42
|
const currentUrl = page.url();
|
32
43
|
const targetDomain = new URL(endpoint).origin;
|
@@ -34,13 +45,30 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
34
45
|
logger.info(`Navigating to target domain: ${targetDomain}`);
|
35
46
|
await page.goto(targetDomain, { waitUntil: 'networkidle2' });
|
36
47
|
}
|
37
|
-
//
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
48
|
+
// 세션 검증 (모든 시도에서 수행)
|
49
|
+
const isSessionValid = await validateSession(page);
|
50
|
+
if (!isSessionValid) {
|
51
|
+
logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication (attempt: ${attempt + 1})`);
|
52
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
53
|
+
try {
|
54
|
+
const reauthResult = await reAuthenticateSession();
|
55
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
56
|
+
pageResource = reauthResult;
|
57
|
+
page = reauthResult.page;
|
58
|
+
}
|
59
|
+
else {
|
60
|
+
page = reauthResult;
|
61
|
+
pageResource = { page, requiresManualRelease: false };
|
62
|
+
}
|
63
|
+
logger.info(`Re-authentication successful for connection '${connectionName}'`);
|
64
|
+
}
|
65
|
+
catch (reauthError) {
|
66
|
+
logger.error(`Re-authentication failed for connection '${connectionName}':`, reauthError);
|
67
|
+
// 재인증 실패 시 이번 시도는 실패로 처리하고 다음 시도로 넘어감
|
68
|
+
if (attempt === maxRetries) {
|
69
|
+
throw new Error(`Re-authentication failed after ${maxRetries + 1} attempts: ${reauthError.message}`);
|
70
|
+
}
|
71
|
+
continue;
|
44
72
|
}
|
45
73
|
}
|
46
74
|
// URL 구성
|
@@ -153,50 +181,77 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
153
181
|
if (response.networkError) {
|
154
182
|
if (attempt < maxRetries) {
|
155
183
|
logger.warn(`Network error detected: ${response.error}, retrying... (${attempt + 1}/${maxRetries + 1})`);
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
}
|
184
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
185
|
+
page = null;
|
186
|
+
pageResource = null;
|
160
187
|
continue;
|
161
188
|
}
|
162
189
|
else {
|
163
|
-
|
164
|
-
await releasePage(page);
|
165
|
-
}
|
190
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
166
191
|
throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`);
|
167
192
|
}
|
168
193
|
}
|
169
194
|
// 로그인 리디렉션 감지 처리
|
170
195
|
if (response.redirectedToLogin) {
|
171
196
|
if (attempt < maxRetries) {
|
172
|
-
logger.warn(`Login redirect detected: ${response.location}, re-
|
173
|
-
|
174
|
-
|
175
|
-
|
197
|
+
logger.warn(`Login redirect detected: ${response.location}, performing re-authentication... (${attempt + 1}/${maxRetries + 1})`);
|
198
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
199
|
+
// CRITICAL: 실제 재인증 수행
|
200
|
+
try {
|
201
|
+
const reauthResult = await reAuthenticateSession();
|
202
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
203
|
+
pageResource = reauthResult;
|
204
|
+
page = reauthResult.page;
|
205
|
+
}
|
206
|
+
else {
|
207
|
+
page = reauthResult;
|
208
|
+
pageResource = { page, requiresManualRelease: false };
|
209
|
+
}
|
210
|
+
logger.info(`Re-authentication successful after login redirect for connection '${connectionName}'`);
|
211
|
+
}
|
212
|
+
catch (reauthError) {
|
213
|
+
logger.error(`Re-authentication failed after login redirect for connection '${connectionName}':`, reauthError);
|
214
|
+
if (attempt === maxRetries) {
|
215
|
+
throw new Error(`Re-authentication failed after login redirect: ${reauthError.message}`);
|
216
|
+
}
|
217
|
+
continue;
|
176
218
|
}
|
177
219
|
continue;
|
178
220
|
}
|
179
221
|
else {
|
180
|
-
|
181
|
-
await releasePage(page);
|
182
|
-
}
|
222
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
183
223
|
throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${response.error}`);
|
184
224
|
}
|
185
225
|
}
|
186
226
|
// 세션 타임아웃 관련 에러 체크
|
187
227
|
if (!response.ok && isSessionTimeoutError(response.status)) {
|
188
228
|
if (attempt < maxRetries) {
|
189
|
-
logger.warn(`Session timeout detected (${response.status}),
|
190
|
-
|
191
|
-
|
192
|
-
|
229
|
+
logger.warn(`Session timeout detected (${response.status}), performing re-authentication... (${attempt + 1}/${maxRetries + 1})`);
|
230
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
231
|
+
// CRITICAL: 실제 재인증 수행
|
232
|
+
try {
|
233
|
+
const reauthResult = await reAuthenticateSession();
|
234
|
+
if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {
|
235
|
+
pageResource = reauthResult;
|
236
|
+
page = reauthResult.page;
|
237
|
+
}
|
238
|
+
else {
|
239
|
+
page = reauthResult;
|
240
|
+
pageResource = { page, requiresManualRelease: false };
|
241
|
+
}
|
242
|
+
logger.info(`Re-authentication successful after session timeout for connection '${connectionName}'`);
|
243
|
+
}
|
244
|
+
catch (reauthError) {
|
245
|
+
logger.error(`Re-authentication failed after session timeout for connection '${connectionName}':`, reauthError);
|
246
|
+
if (attempt === maxRetries) {
|
247
|
+
throw new Error(`Re-authentication failed after session timeout: ${reauthError.message}`);
|
248
|
+
}
|
249
|
+
continue;
|
193
250
|
}
|
194
251
|
continue;
|
195
252
|
}
|
196
253
|
else {
|
197
|
-
|
198
|
-
await releasePage(page);
|
199
|
-
}
|
254
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
200
255
|
throw new Error(`Session timeout after ${maxRetries + 1} attempts: ${response.error}`);
|
201
256
|
}
|
202
257
|
}
|
@@ -210,18 +265,15 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
210
265
|
status: response.status,
|
211
266
|
headers: response.headers
|
212
267
|
};
|
213
|
-
|
214
|
-
await releasePage(page);
|
215
|
-
}
|
268
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
216
269
|
return result;
|
217
270
|
}
|
218
271
|
catch (error) {
|
219
272
|
lastError = error;
|
220
273
|
logger.error(`Headless request attempt ${attempt + 1} failed:`, error);
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
}
|
274
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
275
|
+
page = null;
|
276
|
+
pageResource = null;
|
225
277
|
// 세션 관련 에러가 아니거나 마지막 재시도면 에러 발생
|
226
278
|
if (!isRecoverableError(error) || attempt === maxRetries) {
|
227
279
|
throw error;
|
@@ -229,9 +281,9 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
229
281
|
logger.info(`Retrying request... (${attempt + 2}/${maxRetries + 1})`);
|
230
282
|
}
|
231
283
|
}
|
232
|
-
// 모든 재시도가 실패한 경우 - 혹시 남은
|
233
|
-
if (
|
234
|
-
await releasePage
|
284
|
+
// 모든 재시도가 실패한 경우 - 혹시 남은 리소스가 있으면 정리
|
285
|
+
if (pageResource) {
|
286
|
+
await safeReleasePageResource(pageResource, releasePage, logger);
|
235
287
|
}
|
236
288
|
throw lastError || new Error('Request failed after all retry attempts');
|
237
289
|
}
|
@@ -264,4 +316,65 @@ function isRecoverableError(error) {
|
|
264
316
|
];
|
265
317
|
return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword));
|
266
318
|
}
|
319
|
+
/**
|
320
|
+
* Safely release page resource with comprehensive error handling
|
321
|
+
*/
|
322
|
+
async function safeReleasePageResource(pageResource, releasePage, logger) {
|
323
|
+
if (!pageResource) {
|
324
|
+
return;
|
325
|
+
}
|
326
|
+
try {
|
327
|
+
// Handle different pageResource formats
|
328
|
+
if (pageResource.page) {
|
329
|
+
// This is a complex resource object {page, browser, requiresManualRelease}
|
330
|
+
const { page, browser, requiresManualRelease } = pageResource;
|
331
|
+
if (requiresManualRelease && browser) {
|
332
|
+
// Manual release required - close page first, then release browser
|
333
|
+
try {
|
334
|
+
if (page && !page.isClosed()) {
|
335
|
+
await page.close();
|
336
|
+
logger.info('Page closed during manual resource release');
|
337
|
+
}
|
338
|
+
}
|
339
|
+
catch (closeError) {
|
340
|
+
logger.error('Failed to close page during manual release:', closeError);
|
341
|
+
}
|
342
|
+
// Release browser back to pool
|
343
|
+
try {
|
344
|
+
const { getHeadlessPool } = require('../../resource-pool/headless-pool');
|
345
|
+
const pool = getHeadlessPool();
|
346
|
+
await pool.release(browser);
|
347
|
+
logger.info('Browser manually released to pool');
|
348
|
+
}
|
349
|
+
catch (releaseError) {
|
350
|
+
logger.error('Failed to manually release browser to pool:', releaseError);
|
351
|
+
}
|
352
|
+
}
|
353
|
+
else {
|
354
|
+
// Standard release through releasePage
|
355
|
+
await releasePage(page);
|
356
|
+
logger.info('Page released through standard releasePage method');
|
357
|
+
}
|
358
|
+
}
|
359
|
+
else {
|
360
|
+
// Simple page object - use standard release
|
361
|
+
await releasePage(pageResource);
|
362
|
+
logger.info('Simple page resource released');
|
363
|
+
}
|
364
|
+
}
|
365
|
+
catch (error) {
|
366
|
+
logger.error('Critical error during page resource release:', error);
|
367
|
+
// Last resort: try to force close the page if it exists
|
368
|
+
try {
|
369
|
+
const page = pageResource.page || pageResource;
|
370
|
+
if (page && !page.isClosed()) {
|
371
|
+
await page.close();
|
372
|
+
logger.warn('Force closed page as last resort');
|
373
|
+
}
|
374
|
+
}
|
375
|
+
catch (forceCloseError) {
|
376
|
+
logger.error('Failed to force close page:', forceCloseError);
|
377
|
+
}
|
378
|
+
}
|
379
|
+
}
|
267
380
|
//# sourceMappingURL=headless-request-with-recovery.js.map
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"headless-request-with-recovery.js","sourceRoot":"","sources":["../../../../server/engine/task/utils/headless-request-with-recovery.ts"],"names":[],"mappings":"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","sourcesContent":["import { access } from '@things-factory/utils'\nimport { ConnectionManager } from '../../connection-manager'\n\nexport interface HeadlessRequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n path: string\n headers?: Record<string, string>\n body?: any\n queryParams?: Record<string, any>\n maxRetries?: number\n accessor?: string // POST 요청에서 data 접근용\n contentType?: string // POST 요청에서 content-type 지정용\n}\n\n/**\n * 세션 회복 기능을 포함한 headless HTTP 요청 함수\n */\nexport async function executeHeadlessRequestWithRecovery(\n connectionName: string,\n options: HeadlessRequestOptions,\n context: { logger: any; data: any; domain: any }\n): Promise<any> {\n const { logger, data, domain } = context\n const { method, path, headers = {}, body, queryParams, maxRetries = 2, accessor, contentType } = options\n\n // accessor가 있으면 data에서 body 추출\n let requestBody = body\n if (accessor && data) {\n requestBody = access(accessor, data)\n }\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, params: connectionParams, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection\n const loginPagePath = connectionParams?.loginPagePath || '/login'\n\n let page = null\n let lastError = null\n\n // 재시도 로직\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n // 페이지 획득\n page = await acquireSessionPage()\n \n // 페이지가 올바른 도메인에 있는지 확인\n const currentUrl = page.url()\n const targetDomain = new URL(endpoint).origin\n if (!currentUrl.startsWith(targetDomain)) {\n logger.info(`Navigating to target domain: ${targetDomain}`)\n await page.goto(targetDomain, { waitUntil: 'networkidle2' })\n }\n \n // 첫 번째 시도가 아니면 세션 검증\n if (attempt > 0) {\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`)\n await releasePage(page)\n page = await reAuthenticateSession()\n }\n }\n\n // URL 구성\n const url = new URL(path, endpoint)\n if (queryParams && typeof queryParams === 'object') {\n Object.keys(queryParams).forEach(key => {\n if (queryParams[key] !== null && queryParams[key] !== undefined) {\n url.searchParams.append(key, String(queryParams[key]))\n }\n })\n }\n\n // 요청 옵션 구성\n const requestOptions: any = {\n method,\n headers: {\n ...headers\n },\n credentials: 'include'\n }\n\n // Content-Type과 body 처리\n if (requestBody && ['POST', 'PUT', 'PATCH'].includes(method)) {\n if (contentType) {\n requestOptions.headers['content-type'] = contentType\n switch (contentType) {\n case 'text/plain':\n requestOptions.body = JSON.stringify(requestBody)\n break\n case 'application/json':\n default:\n requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)\n break\n }\n } else {\n requestOptions.headers['Content-Type'] = 'application/json'\n requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)\n }\n }\n\n // fetch 요청 실행 - try-catch로 네트워크 에러 처리 \n const response = await page.evaluate(\n async (urlString, opts, loginPagePath) => {\n try {\n const response = await fetch(urlString, { ...opts, redirect: 'manual' })\n \n const result = {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries())\n }\n\n // 302 리디렉션 감지 - 로그인 페이지로의 리디렉션 체크\n if ([301, 302, 307, 308].includes(response.status)) {\n const location = response.headers.get('location') || ''\n // connection의 loginPagePath와 일반적인 로그인 경로들을 체크\n if (location.includes(loginPagePath) || \n location.includes('/login') || \n location.includes('/signin') || \n location.includes('/auth')) {\n return {\n ...result,\n error: `Redirected to login page: ${location}`,\n redirectedToLogin: true,\n location,\n data: null\n }\n }\n // 다른 리디렉션은 follow\n const redirectResponse = await fetch(urlString, opts)\n return {\n ok: redirectResponse.ok,\n status: redirectResponse.status,\n statusText: redirectResponse.statusText,\n headers: Object.fromEntries(redirectResponse.headers.entries()),\n data: redirectResponse.ok ? await redirectResponse.json().catch(() => redirectResponse.text()).catch(() => null) : null,\n error: redirectResponse.ok ? null : `HTTP ${redirectResponse.status}: ${redirectResponse.statusText}`\n }\n }\n\n if (!response.ok) {\n return {\n ...result,\n error: `HTTP ${response.status}: ${response.statusText}`,\n data: null\n }\n }\n\n const contentType = response.headers.get('content-type') || ''\n \n if (contentType.includes('application/json')) {\n result['data'] = await response.json()\n } else if (contentType.includes('text/')) {\n result['data'] = await response.text()\n } else {\n // 응답이 없는 경우 (204 No Content 등)\n result['data'] = null\n }\n\n return result\n } catch (fetchError) {\n // 네트워크 레벨 에러 처리\n return {\n ok: false,\n status: 0,\n statusText: 'Network Error',\n headers: {},\n error: fetchError.message || 'Failed to fetch',\n data: null,\n networkError: true\n }\n }\n },\n url.toString(),\n requestOptions,\n loginPagePath\n )\n\n // 네트워크 에러 체크 (fetch 자체가 실패한 경우)\n if (response.networkError) {\n if (attempt < maxRetries) {\n logger.warn(`Network error detected: ${response.error}, retrying... (${attempt + 1}/${maxRetries + 1})`)\n if (page) {\n await releasePage(page)\n page = null\n }\n continue\n } else {\n if (page) {\n await releasePage(page)\n }\n throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 로그인 리디렉션 감지 처리\n if (response.redirectedToLogin) {\n if (attempt < maxRetries) {\n logger.warn(`Login redirect detected: ${response.location}, re-authenticating... (${attempt + 1}/${maxRetries + 1})`)\n if (page) {\n await releasePage(page)\n page = null\n }\n continue\n } else {\n if (page) {\n await releasePage(page)\n }\n throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 세션 타임아웃 관련 에러 체크\n if (!response.ok && isSessionTimeoutError(response.status)) {\n if (attempt < maxRetries) {\n logger.warn(`Session timeout detected (${response.status}), retrying... (${attempt + 1}/${maxRetries + 1})`)\n if (page) {\n await releasePage(page)\n page = null\n }\n continue\n } else {\n if (page) {\n await releasePage(page)\n }\n throw new Error(`Session timeout after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 기타 HTTP 에러\n if (!response.ok) {\n throw new Error(response.error)\n }\n\n // 성공 시 페이지 릴리즈 후 결과 반환\n const result = {\n data: response.data,\n status: response.status,\n headers: response.headers\n }\n \n if (page) {\n await releasePage(page)\n }\n \n return result\n\n } catch (error) {\n lastError = error\n logger.error(`Headless request attempt ${attempt + 1} failed:`, error)\n\n if (page) {\n await releasePage(page)\n page = null\n }\n\n // 세션 관련 에러가 아니거나 마지막 재시도면 에러 발생\n if (!isRecoverableError(error) || attempt === maxRetries) {\n throw error\n }\n\n logger.info(`Retrying request... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우 - 혹시 남은 페이지가 있으면 정리\n if (page) {\n await releasePage(page)\n }\n throw lastError || new Error('Request failed after all retry attempts')\n}\n\n/**\n * 세션 타임아웃 관련 HTTP 상태 코드인지 확인\n */\nfunction isSessionTimeoutError(statusCode: number): boolean {\n return [401, 403].includes(statusCode)\n}\n\n/**\n * 복구 가능한 에러인지 확인\n */\nfunction isRecoverableError(error: any): boolean {\n const errorMessage = error.message?.toLowerCase() || ''\n \n // 복구 가능한 에러 키워드 (네트워크, 세션/인증 관련)\n const recoverableErrorKeywords = [\n 'failed to fetch',\n 'network error',\n 'connection failed',\n 'connection reset',\n 'timeout',\n 'timed out',\n 'unauthorized',\n 'forbidden', \n 'session',\n 'authentication',\n 'login',\n 'expired',\n 'redirected to login' // 로그인 리디렉션 추가\n ]\n\n return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword))\n}"]}
|
1
|
+
{"version":3,"file":"headless-request-with-recovery.js","sourceRoot":"","sources":["../../../../server/engine/task/utils/headless-request-with-recovery.ts"],"names":[],"mappings":"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","sourcesContent":["import { access } from '@things-factory/utils'\nimport { ConnectionManager } from '../../connection-manager'\n\nexport interface HeadlessRequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n path: string\n headers?: Record<string, string>\n body?: any\n queryParams?: Record<string, any>\n maxRetries?: number\n accessor?: string // POST 요청에서 data 접근용\n contentType?: string // POST 요청에서 content-type 지정용\n}\n\n/**\n * 세션 회복 기능을 포함한 headless HTTP 요청 함수\n */\nexport async function executeHeadlessRequestWithRecovery(\n connectionName: string,\n options: HeadlessRequestOptions,\n context: { logger: any; data: any; domain: any }\n): Promise<any> {\n const { logger, data, domain } = context\n const { method, path, headers = {}, body, queryParams, maxRetries = 2, accessor, contentType } = options\n\n // accessor가 있으면 data에서 body 추출\n let requestBody = body\n if (accessor && data) {\n requestBody = access(accessor, data)\n }\n\n const connection = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n if (!connection) {\n throw new Error(`Connection '${connectionName}' is not established.`)\n }\n\n const { endpoint, params: connectionParams, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection\n const loginPagePath = connectionParams?.loginPagePath || '/login'\n\n let page = null\n let pageResource = null // 리소스 추적 객체\n let lastError = null\n\n // 재시도 로직\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n // 페이지 획득\n const sessionResult = await acquireSessionPage()\n \n // reAuthenticateSession의 반환 형태 확인\n if (sessionResult && typeof sessionResult === 'object' && sessionResult.page) {\n pageResource = sessionResult // {page, browser, requiresManualRelease}\n page = sessionResult.page\n } else {\n page = sessionResult\n pageResource = { page, requiresManualRelease: false }\n }\n \n // 페이지가 올바른 도메인에 있는지 확인\n const currentUrl = page.url()\n const targetDomain = new URL(endpoint).origin\n if (!currentUrl.startsWith(targetDomain)) {\n logger.info(`Navigating to target domain: ${targetDomain}`)\n await page.goto(targetDomain, { waitUntil: 'networkidle2' })\n }\n \n // 세션 검증 (모든 시도에서 수행)\n const isSessionValid = await validateSession(page)\n if (!isSessionValid) {\n logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication (attempt: ${attempt + 1})`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n \n try {\n const reauthResult = await reAuthenticateSession()\n if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {\n pageResource = reauthResult\n page = reauthResult.page\n } else {\n page = reauthResult\n pageResource = { page, requiresManualRelease: false }\n }\n logger.info(`Re-authentication successful for connection '${connectionName}'`)\n } catch (reauthError) {\n logger.error(`Re-authentication failed for connection '${connectionName}':`, reauthError)\n // 재인증 실패 시 이번 시도는 실패로 처리하고 다음 시도로 넘어감\n if (attempt === maxRetries) {\n throw new Error(`Re-authentication failed after ${maxRetries + 1} attempts: ${reauthError.message}`)\n }\n continue\n }\n }\n\n // URL 구성\n const url = new URL(path, endpoint)\n if (queryParams && typeof queryParams === 'object') {\n Object.keys(queryParams).forEach(key => {\n if (queryParams[key] !== null && queryParams[key] !== undefined) {\n url.searchParams.append(key, String(queryParams[key]))\n }\n })\n }\n\n // 요청 옵션 구성\n const requestOptions: any = {\n method,\n headers: {\n ...headers\n },\n credentials: 'include'\n }\n\n // Content-Type과 body 처리\n if (requestBody && ['POST', 'PUT', 'PATCH'].includes(method)) {\n if (contentType) {\n requestOptions.headers['content-type'] = contentType\n switch (contentType) {\n case 'text/plain':\n requestOptions.body = JSON.stringify(requestBody)\n break\n case 'application/json':\n default:\n requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)\n break\n }\n } else {\n requestOptions.headers['Content-Type'] = 'application/json'\n requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody)\n }\n }\n\n // fetch 요청 실행 - try-catch로 네트워크 에러 처리 \n const response = await page.evaluate(\n async (urlString, opts, loginPagePath) => {\n try {\n const response = await fetch(urlString, { ...opts, redirect: 'manual' })\n \n const result = {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries())\n }\n\n // 302 리디렉션 감지 - 로그인 페이지로의 리디렉션 체크\n if ([301, 302, 307, 308].includes(response.status)) {\n const location = response.headers.get('location') || ''\n // connection의 loginPagePath와 일반적인 로그인 경로들을 체크\n if (location.includes(loginPagePath) || \n location.includes('/login') || \n location.includes('/signin') || \n location.includes('/auth')) {\n return {\n ...result,\n error: `Redirected to login page: ${location}`,\n redirectedToLogin: true,\n location,\n data: null\n }\n }\n // 다른 리디렉션은 follow\n const redirectResponse = await fetch(urlString, opts)\n return {\n ok: redirectResponse.ok,\n status: redirectResponse.status,\n statusText: redirectResponse.statusText,\n headers: Object.fromEntries(redirectResponse.headers.entries()),\n data: redirectResponse.ok ? await redirectResponse.json().catch(() => redirectResponse.text()).catch(() => null) : null,\n error: redirectResponse.ok ? null : `HTTP ${redirectResponse.status}: ${redirectResponse.statusText}`\n }\n }\n\n if (!response.ok) {\n return {\n ...result,\n error: `HTTP ${response.status}: ${response.statusText}`,\n data: null\n }\n }\n\n const contentType = response.headers.get('content-type') || ''\n \n if (contentType.includes('application/json')) {\n result['data'] = await response.json()\n } else if (contentType.includes('text/')) {\n result['data'] = await response.text()\n } else {\n // 응답이 없는 경우 (204 No Content 등)\n result['data'] = null\n }\n\n return result\n } catch (fetchError) {\n // 네트워크 레벨 에러 처리\n return {\n ok: false,\n status: 0,\n statusText: 'Network Error',\n headers: {},\n error: fetchError.message || 'Failed to fetch',\n data: null,\n networkError: true\n }\n }\n },\n url.toString(),\n requestOptions,\n loginPagePath\n )\n\n // 네트워크 에러 체크 (fetch 자체가 실패한 경우)\n if (response.networkError) {\n if (attempt < maxRetries) {\n logger.warn(`Network error detected: ${response.error}, retrying... (${attempt + 1}/${maxRetries + 1})`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n page = null\n pageResource = null\n continue\n } else {\n await safeReleasePageResource(pageResource, releasePage, logger)\n throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 로그인 리디렉션 감지 처리\n if (response.redirectedToLogin) {\n if (attempt < maxRetries) {\n logger.warn(`Login redirect detected: ${response.location}, performing re-authentication... (${attempt + 1}/${maxRetries + 1})`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n \n // CRITICAL: 실제 재인증 수행\n try {\n const reauthResult = await reAuthenticateSession()\n if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {\n pageResource = reauthResult\n page = reauthResult.page\n } else {\n page = reauthResult\n pageResource = { page, requiresManualRelease: false }\n }\n logger.info(`Re-authentication successful after login redirect for connection '${connectionName}'`)\n } catch (reauthError) {\n logger.error(`Re-authentication failed after login redirect for connection '${connectionName}':`, reauthError)\n if (attempt === maxRetries) {\n throw new Error(`Re-authentication failed after login redirect: ${reauthError.message}`)\n }\n continue\n }\n continue\n } else {\n await safeReleasePageResource(pageResource, releasePage, logger)\n throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 세션 타임아웃 관련 에러 체크\n if (!response.ok && isSessionTimeoutError(response.status)) {\n if (attempt < maxRetries) {\n logger.warn(`Session timeout detected (${response.status}), performing re-authentication... (${attempt + 1}/${maxRetries + 1})`)\n await safeReleasePageResource(pageResource, releasePage, logger)\n \n // CRITICAL: 실제 재인증 수행\n try {\n const reauthResult = await reAuthenticateSession()\n if (reauthResult && typeof reauthResult === 'object' && reauthResult.page) {\n pageResource = reauthResult\n page = reauthResult.page\n } else {\n page = reauthResult\n pageResource = { page, requiresManualRelease: false }\n }\n logger.info(`Re-authentication successful after session timeout for connection '${connectionName}'`)\n } catch (reauthError) {\n logger.error(`Re-authentication failed after session timeout for connection '${connectionName}':`, reauthError)\n if (attempt === maxRetries) {\n throw new Error(`Re-authentication failed after session timeout: ${reauthError.message}`)\n }\n continue\n }\n continue\n } else {\n await safeReleasePageResource(pageResource, releasePage, logger)\n throw new Error(`Session timeout after ${maxRetries + 1} attempts: ${response.error}`)\n }\n }\n\n // 기타 HTTP 에러\n if (!response.ok) {\n throw new Error(response.error)\n }\n\n // 성공 시 페이지 릴리즈 후 결과 반환\n const result = {\n data: response.data,\n status: response.status,\n headers: response.headers\n }\n \n await safeReleasePageResource(pageResource, releasePage, logger)\n \n return result\n\n } catch (error) {\n lastError = error\n logger.error(`Headless request attempt ${attempt + 1} failed:`, error)\n\n await safeReleasePageResource(pageResource, releasePage, logger)\n page = null\n pageResource = null\n\n // 세션 관련 에러가 아니거나 마지막 재시도면 에러 발생\n if (!isRecoverableError(error) || attempt === maxRetries) {\n throw error\n }\n\n logger.info(`Retrying request... (${attempt + 2}/${maxRetries + 1})`)\n }\n }\n\n // 모든 재시도가 실패한 경우 - 혹시 남은 리소스가 있으면 정리\n if (pageResource) {\n await safeReleasePageResource(pageResource, releasePage, logger)\n }\n throw lastError || new Error('Request failed after all retry attempts')\n}\n\n/**\n * 세션 타임아웃 관련 HTTP 상태 코드인지 확인\n */\nfunction isSessionTimeoutError(statusCode: number): boolean {\n return [401, 403].includes(statusCode)\n}\n\n/**\n * 복구 가능한 에러인지 확인\n */\nfunction isRecoverableError(error: any): boolean {\n const errorMessage = error.message?.toLowerCase() || ''\n \n // 복구 가능한 에러 키워드 (네트워크, 세션/인증 관련)\n const recoverableErrorKeywords = [\n 'failed to fetch',\n 'network error',\n 'connection failed',\n 'connection reset',\n 'timeout',\n 'timed out',\n 'unauthorized',\n 'forbidden', \n 'session',\n 'authentication',\n 'login',\n 'expired',\n 'redirected to login' // 로그인 리디렉션 추가\n ]\n\n return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword))\n}\n\n/**\n * Safely release page resource with comprehensive error handling\n */\nexport async function safeReleasePageResource(pageResource: any, releasePage: Function, logger: any): Promise<void> {\n if (!pageResource) {\n return\n }\n\n try {\n // Handle different pageResource formats\n if (pageResource.page) {\n // This is a complex resource object {page, browser, requiresManualRelease}\n const { page, browser, requiresManualRelease } = pageResource\n \n if (requiresManualRelease && browser) {\n // Manual release required - close page first, then release browser\n try {\n if (page && !page.isClosed()) {\n await page.close()\n logger.info('Page closed during manual resource release')\n }\n } catch (closeError) {\n logger.error('Failed to close page during manual release:', closeError)\n }\n\n // Release browser back to pool\n try {\n const { getHeadlessPool } = require('../../resource-pool/headless-pool')\n const pool = getHeadlessPool()\n await pool.release(browser)\n logger.info('Browser manually released to pool')\n } catch (releaseError) {\n logger.error('Failed to manually release browser to pool:', releaseError)\n }\n } else {\n // Standard release through releasePage\n await releasePage(page)\n logger.info('Page released through standard releasePage method')\n }\n } else {\n // Simple page object - use standard release\n await releasePage(pageResource)\n logger.info('Simple page resource released')\n }\n } catch (error) {\n logger.error('Critical error during page resource release:', error)\n \n // Last resort: try to force close the page if it exists\n try {\n const page = pageResource.page || pageResource\n if (page && !page.isClosed()) {\n await page.close()\n logger.warn('Force closed page as last resort')\n }\n } catch (forceCloseError) {\n logger.error('Failed to force close page:', forceCloseError)\n }\n }\n}"]}
|