@things-factory/integration-base 9.0.35 → 9.0.36
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 +10 -1
- package/dist-server/engine/connector/headless-connector.js.map +1 -1
- package/dist-server/engine/task/headless-scrap.js +117 -58
- package/dist-server/engine/task/headless-scrap.js.map +1 -1
- package/dist-server/engine/task/utils/headless-request-with-recovery.js +52 -6
- package/dist-server/engine/task/utils/headless-request-with-recovery.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
@@ -148,7 +148,16 @@ class HeadlessConnector {
|
|
148
148
|
if (!loginInfo.loginRequired) {
|
149
149
|
return true;
|
150
150
|
}
|
151
|
-
//
|
151
|
+
// URL 기반 로그인 리디렉션 체크
|
152
|
+
const currentUrl = page.url();
|
153
|
+
if (currentUrl.includes(loginInfo.loginPagePath) ||
|
154
|
+
currentUrl.includes('/login') ||
|
155
|
+
currentUrl.includes('/signin') ||
|
156
|
+
currentUrl.includes('/auth')) {
|
157
|
+
connection_manager_1.ConnectionManager.logger.info(`Redirected to login page: ${currentUrl}`);
|
158
|
+
return false;
|
159
|
+
}
|
160
|
+
// 로그인 폼 요소 기반 체크
|
152
161
|
const needsLogin = await page.evaluate(() => {
|
153
162
|
// 로그인 페이지 특정 요소들이 있는지 확인
|
154
163
|
const loginIndicators = ['#username', '#password', '.login-form', '[type="password"]'];
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"headless-connector.js","sourceRoot":"","sources":["../../../server/engine/connector/headless-connector.ts"],"names":[],"mappings":"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","sourcesContent":["import { ConnectionManager } from '../connection-manager'\nimport { Connector } from '../types'\nimport { getHeadlessPool } from '../resource-pool/headless-pool'\nimport { Browser, Page } from 'puppeteer'\n\n/*\n Functionality of the headless-connector:\n - Provides a mechanism to acquire an active session page.\n - Performs login when necessary to obtain valid cookies.\n - Applies these cookies to the page for session management.\n - Pages are acquired from the `headlessPool`, which manages browser instances.\n - During the login process, pages from the pool are used for actions like form filling and navigation.\n - Valid cookies are saved after login and reused for subsequent page acquisitions.\n - Users must explicitly release the page after use through the `releasePage` method.\n - Released pages are returned to the `headlessPool` for reuse.\n*/\n\nexport class HeadlessConnector implements Connector {\n async ready(connectionConfigs) {\n await Promise.all(connectionConfigs.map(this.connect.bind(this)))\n ConnectionManager.logger.info('headless-connector connections are ready')\n }\n\n async connect(connection) {\n const {\n endpoint: uri = '1',\n params: {\n username = '',\n password = '',\n loginPagePath = '/login',\n loginApiUrl = null,\n usernameSelector = '#username',\n passwordSelector = '#password',\n submitSelector = '#submit',\n successSelector = null,\n shadowDomSelectors = '', // Comma separated shadow DOM selectors\n timeout = 15000, // Default timeout for operations\n retries = 3 // Default number of retries for login or page actions\n } = {}\n } = connection\n\n const loginInfo = {\n loginRequired: Boolean(username), // Determine if login is required\n username,\n password,\n loginPagePath,\n loginApiUrl,\n timeout,\n retries,\n loginSelectors: {\n usernameSelector,\n passwordSelector,\n submitSelector,\n successSelector,\n shadowDomSelectors: shadowDomSelectors\n .split(',')\n .map(selector => selector.trim())\n .filter(Boolean)\n }\n }\n\n async function acquireBrowser() {\n try {\n const pool = getHeadlessPool()\n const browser = await pool.acquire()\n return browser\n } catch (error) {\n ConnectionManager.logger.error('Failed to acquire browser:', error)\n throw error\n }\n }\n\n async function releaseBrowser(browser: Browser) {\n try {\n const pool = getHeadlessPool()\n await pool.release(browser)\n } catch (error) {\n ConnectionManager.logger.error('Failed to release browser:', error)\n }\n }\n\n ConnectionManager.addConnectionInstance(connection, {\n endpoint: connection.endpoint,\n params: connection.params,\n acquireSessionPage: async () => {\n const browser = await acquireBrowser()\n let page\n let cookies = connection.cookies\n\n try {\n page = await browser.newPage()\n await this.setupPage(page, uri, timeout)\n\n if (loginInfo.loginRequired) {\n // 먼저 기본 페이지로 이동\n await page.goto(uri, { waitUntil: 'networkidle2', timeout })\n\n // 현재 세션의 Authorization 헤더 확인\n const headers = await page.evaluate(() => {\n return fetch(window.location.href, { method: 'GET' })\n .then(response => response.headers.get('Authorization'))\n .catch(() => null)\n })\n\n if (headers) {\n ConnectionManager.logger.info('User is already logged in, skipping login process.')\n return page\n }\n\n if (cookies && isCookieValid(cookies)) {\n await this.applyCookiesAndVerifySession(page, cookies, loginInfo)\n return page\n }\n\n await this.performLogin(page, uri, loginInfo)\n cookies = await page.cookies()\n connection.cookies = cookies\n } else {\n // 로그인이 필요하지 않은 경우에도 기본 페이지로 이동\n await page.goto(uri, { waitUntil: 'networkidle2', timeout })\n }\n\n return page\n } catch (error) {\n ConnectionManager.logger.error('Failed to acquire session page:', error)\n throw error\n }\n },\n releasePage: async (page: Page) => {\n try {\n if (page) {\n const browser = page.browser()\n await page.close()\n await releaseBrowser(browser)\n }\n } catch (error) {\n ConnectionManager.logger.error('Failed to release page:', error)\n }\n },\n // 세션 회복을 위한 재로그인 메서드\n reAuthenticateSession: async () => {\n ConnectionManager.logger.info(`Re-authenticating session for connection: ${connection.name}`)\n \n const browser = await acquireBrowser()\n let page\n\n try {\n page = await browser.newPage()\n await this.setupPage(page, uri, loginInfo.timeout)\n \n if (loginInfo.loginRequired) {\n await this.performLogin(page, uri, loginInfo)\n const newCookies = await page.cookies()\n connection.cookies = newCookies\n ConnectionManager.logger.info(`Session re-authenticated successfully for connection: ${connection.name}`)\n }\n \n return page\n } catch (error) {\n ConnectionManager.logger.error(`Failed to re-authenticate session for connection: ${connection.name}`, error)\n if (page) {\n await page.close()\n }\n await releaseBrowser(browser)\n throw error\n }\n },\n // 세션 유효성 검사\n validateSession: async (page: Page) => {\n try {\n if (!loginInfo.loginRequired) {\n return true\n }\n\n // 간단한 세션 검사 - 현재 페이지에서 인증이 필요한지 확인\n const needsLogin = await page.evaluate(() => {\n // 로그인 페이지 특정 요소들이 있는지 확인\n const loginIndicators = ['#username', '#password', '.login-form', '[type=\"password\"]']\n return loginIndicators.some(selector => document.querySelector(selector))\n })\n\n return !needsLogin\n } catch (error) {\n ConnectionManager.logger.warn(`Session validation failed for connection: ${connection.name}`, error)\n return false\n }\n },\n acquireBrowser,\n releaseBrowser\n })\n\n ConnectionManager.logger.info(\n `headless-connector connection(${connection.name}:${connection.endpoint}) is connected`\n )\n }\n\n async setupPage(page, uri, timeout) {\n await page.setRequestInterception(true)\n\n await page.setBypassCSP(true)\n\n await page.setUserAgent(\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.204 Safari/537.36'\n )\n\n await page.evaluateOnNewDocument(() => {\n Object.defineProperty(navigator, 'webdriver', { get: () => false })\n })\n\n await page.setExtraHTTPHeaders({\n 'Upgrade-Insecure-Requests': '0',\n 'accept-language': 'en-US,en;q=0.9',\n referer: uri\n })\n\n await page.setDefaultNavigationTimeout(timeout)\n\n page.on('console', async msg => {\n ConnectionManager.logger.info(`[browser ${msg.type()}] ${msg.text()}`)\n })\n\n page.on('request', request => {\n if (request.isInterceptResolutionHandled()) {\n return\n }\n\n const resourceType = request.resourceType()\n\n // fetch(), XMLHttpRequest 등의 API 호출은 항상 허용\n if (resourceType === 'fetch' || resourceType === 'xhr') {\n request.continue()\n return\n }\n\n const headers = request.headers()\n if (headers['access-control-request-headers']) {\n const filteredHeaders = headers['access-control-request-headers']\n .split(',')\n .map(header => header.trim())\n .filter(header => header.toLowerCase() !== 'upgrade-insecure-requests')\n\n if (filteredHeaders.length > 0) {\n headers['access-control-request-headers'] = filteredHeaders.join(', ')\n } else {\n delete headers['access-control-request-headers']\n }\n }\n\n if (headers['upgrade-insecure-requests']) {\n delete headers['upgrade-insecure-requests']\n }\n\n // 이미지, 스타일시트, 폰트는 차단하되 API 요청은 허용\n if (['image', 'stylesheet', 'font'].includes(resourceType)) {\n request.abort()\n } else {\n request.continue()\n }\n })\n\n page.on('requestfailed', request => {\n try {\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 if (request.postData()) {\n console.log(`- Post Data: ${request.postData()}`)\n }\n } catch (error) {\n console.error('Error in requestfailed handler:', error)\n }\n })\n }\n\n async applyCookiesAndVerifySession(page, cookies, loginInfo) {\n await page.setCookie(...cookies)\n await page.reload({ waitUntil: 'networkidle2', timeout: loginInfo.timeout })\n\n if (loginInfo.loginRequired && loginInfo.loginSelectors.successSelector) {\n const success = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.successSelector\n )\n if (!success) {\n throw new Error('Session invalid, login required')\n }\n }\n }\n\n async performLogin(page, uri, loginInfo) {\n for (let attempt = 1; attempt <= loginInfo.retries; attempt++) {\n try {\n await page.goto(`${uri}${loginInfo.loginPagePath}`, { waitUntil: 'networkidle2', timeout: loginInfo.timeout })\n\n const usernameInput = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.usernameSelector\n )\n const passwordInput = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.passwordSelector\n )\n const submitButton = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.submitSelector\n )\n\n if (!usernameInput || !passwordInput || !submitButton) {\n throw new Error('Failed to locate input elements in shadow DOM')\n }\n\n await usernameInput.type(loginInfo.username)\n await passwordInput.type(loginInfo.password)\n\n var loginApiUrl =\n loginInfo.loginApiUrl ||\n (await page.evaluate(button => {\n const form = button.closest('form')\n return form ? form.action : null\n }, submitButton))\n\n if (loginApiUrl && !loginApiUrl.startsWith('http')) {\n loginApiUrl = `${uri}${loginApiUrl}`\n }\n\n if (!loginApiUrl) {\n throw new Error('❌ Unable to detect login API URL!')\n }\n\n console.log('✅ Detected login API URL:', loginApiUrl)\n\n const [response] = await Promise.all([\n page.waitForResponse(response => {\n const url = response.url()\n const method = response.request().method()\n\n return url === loginApiUrl && method === 'POST' && [200, 201, 204, 302, 304].includes(response.status())\n }),\n page.evaluate(button => button.click(), submitButton),\n page.waitForNavigation({ waitUntil: 'networkidle2', timeout: loginInfo.timeout }) // 로그인 후 리디렉션 감지\n ])\n\n if (response) {\n const status = response.status()\n if (status >= 200 && status < 400) {\n ConnectionManager.logger.info(`Login successful with status code: ${status}`)\n return\n } else if (status >= 400) {\n throw new Error(`Login failed with status code: ${status}`)\n }\n } else {\n throw new Error('No response received during login')\n }\n\n if (loginInfo.loginSelectors.successSelector) {\n const success = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.successSelector\n )\n if (!success) {\n throw new Error('Login failed: Success selector not found')\n }\n }\n } catch (error) {\n ConnectionManager.logger.warn(`Login attempt ${attempt} failed:`, error)\n\n try {\n await page.screenshot({ path: `logs/login-failure-attempt-${attempt}.png` })\n } catch (error) {\n ConnectionManager.logger.error('Failed to capture screenshot:', error)\n }\n\n if (attempt === loginInfo.retries) {\n throw new Error(`Login failed after ${loginInfo.retries} attempts`)\n }\n }\n }\n }\n\n async resolveShadowDom(page, shadowSelectors, targetSelector) {\n let context\n\n if (!shadowSelectors || shadowSelectors.length === 0) {\n // No Shadow DOM path; use document root as the context\n context = page.mainFrame() // Puppeteer uses frames to represent document\n return context.$(targetSelector) // Search directly in the document root\n }\n\n context = page // Start with the page as the context\n for (const selector of shadowSelectors) {\n const shadowHost = await context.$(selector)\n if (!shadowHost) {\n throw new Error(`Shadow host not found: ${selector}`)\n }\n context = await page.evaluateHandle(host => host.shadowRoot, shadowHost)\n }\n return context.evaluateHandle((shadowRoot, selector) => shadowRoot.querySelector(selector), targetSelector)\n }\n\n async disconnect(connection) {\n ConnectionManager.removeConnectionInstance(connection)\n ConnectionManager.logger.info(`headless-connector connection(${connection.name}) is disconnected`)\n }\n\n get parameterSpec() {\n return [\n {\n type: 'string',\n name: 'username',\n label: 'username',\n useDomainAttribute: true\n },\n {\n type: 'secret',\n name: 'password',\n label: 'password',\n useDomainAttribute: true\n },\n {\n type: 'string',\n name: 'loginPagePath',\n label: 'login-page-path'\n },\n {\n type: 'string',\n name: 'loginApiUrl',\n label: 'login-api-url'\n },\n {\n type: 'string',\n name: 'usernameSelector',\n label: 'username-selector'\n },\n {\n type: 'string',\n name: 'passwordSelector',\n label: 'password-selector'\n },\n {\n type: 'string',\n name: 'submitSelector',\n label: 'submit-selector'\n },\n {\n type: 'string',\n name: 'successSelector',\n label: 'success-selector'\n },\n {\n type: 'string',\n name: 'shadowDomSelectors',\n label: 'shadow-dom-selectors'\n },\n {\n type: 'number',\n name: 'timeout',\n label: 'timeout',\n value: 15000\n },\n {\n type: 'number',\n name: 'retries',\n label: 'maximum-retries',\n value: 3\n }\n ]\n }\n\n get taskPrefixes() {\n return ['headless']\n }\n\n get description() {\n return 'Headless Pool Connector with login capabilities'\n }\n\n get help() {\n return 'integration/connector/headless-connector'\n }\n}\n\nConnectionManager.registerConnector('headless-connector', new HeadlessConnector())\n\nfunction isCookieValid(cookies) {\n if (!cookies || cookies.length === 0) return false\n const now = Date.now() / 1000 // Current time in seconds\n return cookies.some(cookie => cookie.expires && cookie.expires > now)\n}\n"]}
|
1
|
+
{"version":3,"file":"headless-connector.js","sourceRoot":"","sources":["../../../server/engine/connector/headless-connector.ts"],"names":[],"mappings":"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","sourcesContent":["import { ConnectionManager } from '../connection-manager'\nimport { Connector } from '../types'\nimport { getHeadlessPool } from '../resource-pool/headless-pool'\nimport { Browser, Page } from 'puppeteer'\n\n/*\n Functionality of the headless-connector:\n - Provides a mechanism to acquire an active session page.\n - Performs login when necessary to obtain valid cookies.\n - Applies these cookies to the page for session management.\n - Pages are acquired from the `headlessPool`, which manages browser instances.\n - During the login process, pages from the pool are used for actions like form filling and navigation.\n - Valid cookies are saved after login and reused for subsequent page acquisitions.\n - Users must explicitly release the page after use through the `releasePage` method.\n - Released pages are returned to the `headlessPool` for reuse.\n*/\n\nexport class HeadlessConnector implements Connector {\n async ready(connectionConfigs) {\n await Promise.all(connectionConfigs.map(this.connect.bind(this)))\n ConnectionManager.logger.info('headless-connector connections are ready')\n }\n\n async connect(connection) {\n const {\n endpoint: uri = '1',\n params: {\n username = '',\n password = '',\n loginPagePath = '/login',\n loginApiUrl = null,\n usernameSelector = '#username',\n passwordSelector = '#password',\n submitSelector = '#submit',\n successSelector = null,\n shadowDomSelectors = '', // Comma separated shadow DOM selectors\n timeout = 15000, // Default timeout for operations\n retries = 3 // Default number of retries for login or page actions\n } = {}\n } = connection\n\n const loginInfo = {\n loginRequired: Boolean(username), // Determine if login is required\n username,\n password,\n loginPagePath,\n loginApiUrl,\n timeout,\n retries,\n loginSelectors: {\n usernameSelector,\n passwordSelector,\n submitSelector,\n successSelector,\n shadowDomSelectors: shadowDomSelectors\n .split(',')\n .map(selector => selector.trim())\n .filter(Boolean)\n }\n }\n\n async function acquireBrowser() {\n try {\n const pool = getHeadlessPool()\n const browser = await pool.acquire()\n return browser\n } catch (error) {\n ConnectionManager.logger.error('Failed to acquire browser:', error)\n throw error\n }\n }\n\n async function releaseBrowser(browser: Browser) {\n try {\n const pool = getHeadlessPool()\n await pool.release(browser)\n } catch (error) {\n ConnectionManager.logger.error('Failed to release browser:', error)\n }\n }\n\n ConnectionManager.addConnectionInstance(connection, {\n endpoint: connection.endpoint,\n params: connection.params,\n acquireSessionPage: async () => {\n const browser = await acquireBrowser()\n let page\n let cookies = connection.cookies\n\n try {\n page = await browser.newPage()\n await this.setupPage(page, uri, timeout)\n\n if (loginInfo.loginRequired) {\n // 먼저 기본 페이지로 이동\n await page.goto(uri, { waitUntil: 'networkidle2', timeout })\n\n // 현재 세션의 Authorization 헤더 확인\n const headers = await page.evaluate(() => {\n return fetch(window.location.href, { method: 'GET' })\n .then(response => response.headers.get('Authorization'))\n .catch(() => null)\n })\n\n if (headers) {\n ConnectionManager.logger.info('User is already logged in, skipping login process.')\n return page\n }\n\n if (cookies && isCookieValid(cookies)) {\n await this.applyCookiesAndVerifySession(page, cookies, loginInfo)\n return page\n }\n\n await this.performLogin(page, uri, loginInfo)\n cookies = await page.cookies()\n connection.cookies = cookies\n } else {\n // 로그인이 필요하지 않은 경우에도 기본 페이지로 이동\n await page.goto(uri, { waitUntil: 'networkidle2', timeout })\n }\n\n return page\n } catch (error) {\n ConnectionManager.logger.error('Failed to acquire session page:', error)\n throw error\n }\n },\n releasePage: async (page: Page) => {\n try {\n if (page) {\n const browser = page.browser()\n await page.close()\n await releaseBrowser(browser)\n }\n } catch (error) {\n ConnectionManager.logger.error('Failed to release page:', error)\n }\n },\n // 세션 회복을 위한 재로그인 메서드\n reAuthenticateSession: async () => {\n ConnectionManager.logger.info(`Re-authenticating session for connection: ${connection.name}`)\n \n const browser = await acquireBrowser()\n let page\n\n try {\n page = await browser.newPage()\n await this.setupPage(page, uri, loginInfo.timeout)\n \n if (loginInfo.loginRequired) {\n await this.performLogin(page, uri, loginInfo)\n const newCookies = await page.cookies()\n connection.cookies = newCookies\n ConnectionManager.logger.info(`Session re-authenticated successfully for connection: ${connection.name}`)\n }\n \n return page\n } catch (error) {\n ConnectionManager.logger.error(`Failed to re-authenticate session for connection: ${connection.name}`, error)\n if (page) {\n await page.close()\n }\n await releaseBrowser(browser)\n throw error\n }\n },\n // 세션 유효성 검사\n validateSession: async (page: Page) => {\n try {\n if (!loginInfo.loginRequired) {\n return true\n }\n\n // URL 기반 로그인 리디렉션 체크\n const currentUrl = page.url()\n if (currentUrl.includes(loginInfo.loginPagePath) || \n currentUrl.includes('/login') || \n currentUrl.includes('/signin') || \n currentUrl.includes('/auth')) {\n ConnectionManager.logger.info(`Redirected to login page: ${currentUrl}`)\n return false\n }\n\n // 로그인 폼 요소 기반 체크\n const needsLogin = await page.evaluate(() => {\n // 로그인 페이지 특정 요소들이 있는지 확인\n const loginIndicators = ['#username', '#password', '.login-form', '[type=\"password\"]']\n return loginIndicators.some(selector => document.querySelector(selector))\n })\n\n return !needsLogin\n } catch (error) {\n ConnectionManager.logger.warn(`Session validation failed for connection: ${connection.name}`, error)\n return false\n }\n },\n acquireBrowser,\n releaseBrowser\n })\n\n ConnectionManager.logger.info(\n `headless-connector connection(${connection.name}:${connection.endpoint}) is connected`\n )\n }\n\n async setupPage(page, uri, timeout) {\n await page.setRequestInterception(true)\n\n await page.setBypassCSP(true)\n\n await page.setUserAgent(\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.204 Safari/537.36'\n )\n\n await page.evaluateOnNewDocument(() => {\n Object.defineProperty(navigator, 'webdriver', { get: () => false })\n })\n\n await page.setExtraHTTPHeaders({\n 'Upgrade-Insecure-Requests': '0',\n 'accept-language': 'en-US,en;q=0.9',\n referer: uri\n })\n\n await page.setDefaultNavigationTimeout(timeout)\n\n page.on('console', async msg => {\n ConnectionManager.logger.info(`[browser ${msg.type()}] ${msg.text()}`)\n })\n\n page.on('request', request => {\n if (request.isInterceptResolutionHandled()) {\n return\n }\n\n const resourceType = request.resourceType()\n\n // fetch(), XMLHttpRequest 등의 API 호출은 항상 허용\n if (resourceType === 'fetch' || resourceType === 'xhr') {\n request.continue()\n return\n }\n\n const headers = request.headers()\n if (headers['access-control-request-headers']) {\n const filteredHeaders = headers['access-control-request-headers']\n .split(',')\n .map(header => header.trim())\n .filter(header => header.toLowerCase() !== 'upgrade-insecure-requests')\n\n if (filteredHeaders.length > 0) {\n headers['access-control-request-headers'] = filteredHeaders.join(', ')\n } else {\n delete headers['access-control-request-headers']\n }\n }\n\n if (headers['upgrade-insecure-requests']) {\n delete headers['upgrade-insecure-requests']\n }\n\n // 이미지, 스타일시트, 폰트는 차단하되 API 요청은 허용\n if (['image', 'stylesheet', 'font'].includes(resourceType)) {\n request.abort()\n } else {\n request.continue()\n }\n })\n\n page.on('requestfailed', request => {\n try {\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 if (request.postData()) {\n console.log(`- Post Data: ${request.postData()}`)\n }\n } catch (error) {\n console.error('Error in requestfailed handler:', error)\n }\n })\n }\n\n async applyCookiesAndVerifySession(page, cookies, loginInfo) {\n await page.setCookie(...cookies)\n await page.reload({ waitUntil: 'networkidle2', timeout: loginInfo.timeout })\n\n if (loginInfo.loginRequired && loginInfo.loginSelectors.successSelector) {\n const success = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.successSelector\n )\n if (!success) {\n throw new Error('Session invalid, login required')\n }\n }\n }\n\n async performLogin(page, uri, loginInfo) {\n for (let attempt = 1; attempt <= loginInfo.retries; attempt++) {\n try {\n await page.goto(`${uri}${loginInfo.loginPagePath}`, { waitUntil: 'networkidle2', timeout: loginInfo.timeout })\n\n const usernameInput = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.usernameSelector\n )\n const passwordInput = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.passwordSelector\n )\n const submitButton = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.submitSelector\n )\n\n if (!usernameInput || !passwordInput || !submitButton) {\n throw new Error('Failed to locate input elements in shadow DOM')\n }\n\n await usernameInput.type(loginInfo.username)\n await passwordInput.type(loginInfo.password)\n\n var loginApiUrl =\n loginInfo.loginApiUrl ||\n (await page.evaluate(button => {\n const form = button.closest('form')\n return form ? form.action : null\n }, submitButton))\n\n if (loginApiUrl && !loginApiUrl.startsWith('http')) {\n loginApiUrl = `${uri}${loginApiUrl}`\n }\n\n if (!loginApiUrl) {\n throw new Error('❌ Unable to detect login API URL!')\n }\n\n console.log('✅ Detected login API URL:', loginApiUrl)\n\n const [response] = await Promise.all([\n page.waitForResponse(response => {\n const url = response.url()\n const method = response.request().method()\n\n return url === loginApiUrl && method === 'POST' && [200, 201, 204, 302, 304].includes(response.status())\n }),\n page.evaluate(button => button.click(), submitButton),\n page.waitForNavigation({ waitUntil: 'networkidle2', timeout: loginInfo.timeout }) // 로그인 후 리디렉션 감지\n ])\n\n if (response) {\n const status = response.status()\n if (status >= 200 && status < 400) {\n ConnectionManager.logger.info(`Login successful with status code: ${status}`)\n return\n } else if (status >= 400) {\n throw new Error(`Login failed with status code: ${status}`)\n }\n } else {\n throw new Error('No response received during login')\n }\n\n if (loginInfo.loginSelectors.successSelector) {\n const success = await this.resolveShadowDom(\n page,\n loginInfo.loginSelectors.shadowDomSelectors,\n loginInfo.loginSelectors.successSelector\n )\n if (!success) {\n throw new Error('Login failed: Success selector not found')\n }\n }\n } catch (error) {\n ConnectionManager.logger.warn(`Login attempt ${attempt} failed:`, error)\n\n try {\n await page.screenshot({ path: `logs/login-failure-attempt-${attempt}.png` })\n } catch (error) {\n ConnectionManager.logger.error('Failed to capture screenshot:', error)\n }\n\n if (attempt === loginInfo.retries) {\n throw new Error(`Login failed after ${loginInfo.retries} attempts`)\n }\n }\n }\n }\n\n async resolveShadowDom(page, shadowSelectors, targetSelector) {\n let context\n\n if (!shadowSelectors || shadowSelectors.length === 0) {\n // No Shadow DOM path; use document root as the context\n context = page.mainFrame() // Puppeteer uses frames to represent document\n return context.$(targetSelector) // Search directly in the document root\n }\n\n context = page // Start with the page as the context\n for (const selector of shadowSelectors) {\n const shadowHost = await context.$(selector)\n if (!shadowHost) {\n throw new Error(`Shadow host not found: ${selector}`)\n }\n context = await page.evaluateHandle(host => host.shadowRoot, shadowHost)\n }\n return context.evaluateHandle((shadowRoot, selector) => shadowRoot.querySelector(selector), targetSelector)\n }\n\n async disconnect(connection) {\n ConnectionManager.removeConnectionInstance(connection)\n ConnectionManager.logger.info(`headless-connector connection(${connection.name}) is disconnected`)\n }\n\n get parameterSpec() {\n return [\n {\n type: 'string',\n name: 'username',\n label: 'username',\n useDomainAttribute: true\n },\n {\n type: 'secret',\n name: 'password',\n label: 'password',\n useDomainAttribute: true\n },\n {\n type: 'string',\n name: 'loginPagePath',\n label: 'login-page-path'\n },\n {\n type: 'string',\n name: 'loginApiUrl',\n label: 'login-api-url'\n },\n {\n type: 'string',\n name: 'usernameSelector',\n label: 'username-selector'\n },\n {\n type: 'string',\n name: 'passwordSelector',\n label: 'password-selector'\n },\n {\n type: 'string',\n name: 'submitSelector',\n label: 'submit-selector'\n },\n {\n type: 'string',\n name: 'successSelector',\n label: 'success-selector'\n },\n {\n type: 'string',\n name: 'shadowDomSelectors',\n label: 'shadow-dom-selectors'\n },\n {\n type: 'number',\n name: 'timeout',\n label: 'timeout',\n value: 15000\n },\n {\n type: 'number',\n name: 'retries',\n label: 'maximum-retries',\n value: 3\n }\n ]\n }\n\n get taskPrefixes() {\n return ['headless']\n }\n\n get description() {\n return 'Headless Pool Connector with login capabilities'\n }\n\n get help() {\n return 'integration/connector/headless-connector'\n }\n}\n\nConnectionManager.registerConnector('headless-connector', new HeadlessConnector())\n\nfunction isCookieValid(cookies) {\n if (!cookies || cookies.length === 0) return false\n const now = Date.now() / 1000 // Current time in seconds\n return cookies.some(cookie => cookie.expires && cookie.expires > now)\n}\n"]}
|
@@ -5,80 +5,133 @@ const task_registry_1 = require("../task-registry");
|
|
5
5
|
const connection_manager_1 = require("../connection-manager");
|
6
6
|
async function HeadlessScrap(step, { logger, data, domain }) {
|
7
7
|
const { connection: connectionName, params: stepOptions } = step;
|
8
|
-
const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout } = stepOptions || {};
|
8
|
+
const { headers: requestHeaders, path, selectors = [], waitForSelectors, waitForTimeout, maxRetries = 2 } = stepOptions || {};
|
9
9
|
const connection = await connection_manager_1.ConnectionManager.getConnectionInstanceByName(domain, connectionName);
|
10
10
|
if (!connection) {
|
11
11
|
throw new Error(`Connection '${connectionName}' is not established.`);
|
12
12
|
}
|
13
|
-
const { endpoint, params: connectionParams, acquireSessionPage, releasePage } = connection;
|
13
|
+
const { endpoint, params: connectionParams, acquireSessionPage, releasePage, validateSession, reAuthenticateSession } = connection;
|
14
|
+
const loginPagePath = connectionParams?.loginPagePath || '/login';
|
14
15
|
const headers = {
|
15
16
|
...requestHeaders
|
16
17
|
};
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
console.log(`-
|
18
|
+
let page = null;
|
19
|
+
let lastError = null;
|
20
|
+
// 재시도 로직 추가
|
21
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
22
|
+
try {
|
23
|
+
page = await acquireSessionPage();
|
24
|
+
page.on('console', async (msg) => {
|
25
|
+
console.log(`[browser ${msg.type()}] ${msg.text()}`);
|
26
|
+
});
|
27
|
+
page.on('requestfailed', request => {
|
28
|
+
console.log('Request failed:');
|
29
|
+
console.log(`- URL: ${request.url()}`);
|
30
|
+
console.log(`- Method: ${request.method()}`);
|
31
|
+
console.log(`- Failure Text: ${request.failure()?.errorText}`);
|
32
|
+
console.log(`- Headers:`, request.headers());
|
33
|
+
// POST 데이터 (필요한 경우)
|
34
|
+
if (request.postData()) {
|
35
|
+
console.log(`- Post Data: ${request.postData()}`);
|
36
|
+
}
|
37
|
+
});
|
38
|
+
// 302 리디렉션 감지 추가
|
39
|
+
page.on('response', response => {
|
40
|
+
if ([301, 302, 307, 308].includes(response.status())) {
|
41
|
+
const location = response.headers()['location'] || '';
|
42
|
+
if (location.includes(loginPagePath) ||
|
43
|
+
location.includes('/login') ||
|
44
|
+
location.includes('/signin') ||
|
45
|
+
location.includes('/auth')) {
|
46
|
+
logger.warn(`Login redirect detected: ${location}`);
|
47
|
+
throw new Error(`Redirected to login page: ${location}`);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
});
|
51
|
+
// 첫 번째 시도가 아니면 세션 검증
|
52
|
+
if (attempt > 0 && validateSession) {
|
53
|
+
const isSessionValid = await validateSession(page);
|
54
|
+
if (!isSessionValid) {
|
55
|
+
logger.warn(`Session invalid for connection '${connectionName}', attempting re-authentication`);
|
56
|
+
await releasePage(page);
|
57
|
+
page = await reAuthenticateSession();
|
58
|
+
}
|
59
|
+
}
|
60
|
+
await page.setExtraHTTPHeaders(headers);
|
61
|
+
await page.goto(new url_1.URL(path, endpoint), { waitUntil: 'networkidle2' });
|
62
|
+
// 페이지 로드 후 로그인 페이지로 리디렉션되었는지 확인
|
63
|
+
const currentUrl = page.url();
|
64
|
+
if (currentUrl.includes(loginPagePath) ||
|
65
|
+
currentUrl.includes('/login') ||
|
66
|
+
currentUrl.includes('/signin') ||
|
67
|
+
currentUrl.includes('/auth')) {
|
68
|
+
throw new Error(`Page redirected to login: ${currentUrl}`);
|
31
69
|
}
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
70
|
+
// waitForSelectors, waitForTimeout 처리 추가
|
71
|
+
if (waitForSelectors) {
|
72
|
+
try {
|
73
|
+
await page.waitForFunction(selectorsString => {
|
74
|
+
const selectors = selectorsString.split(',').map(s => s.trim());
|
75
|
+
return selectors.every(selector => {
|
76
|
+
const el = document.querySelector(selector);
|
77
|
+
return el && el.textContent && el.textContent.trim().length > 0;
|
78
|
+
});
|
79
|
+
}, { timeout: waitForTimeout ? Number(waitForTimeout) : 10000 }, waitForSelectors // 콤마로 구분된 셀렉터 문자열
|
80
|
+
);
|
81
|
+
}
|
82
|
+
catch (e) {
|
83
|
+
logger.error(`waitForSelectors(${waitForSelectors}) 값이 모두 채워지지 않음:`, e);
|
84
|
+
throw e;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
else if (waitForTimeout) {
|
88
|
+
await page.waitForTimeout(Number(waitForTimeout));
|
89
|
+
}
|
90
|
+
const result = {};
|
91
|
+
for (const selector of selectors) {
|
92
|
+
const { text, value } = selector;
|
93
|
+
result[text] = await page.$$eval(value, elements => {
|
94
|
+
return elements.map(element => {
|
95
|
+
if (element instanceof HTMLInputElement ||
|
96
|
+
element instanceof HTMLTextAreaElement ||
|
97
|
+
element instanceof HTMLSelectElement) {
|
98
|
+
return element.value;
|
99
|
+
}
|
100
|
+
else {
|
101
|
+
return element.textContent?.trim();
|
102
|
+
}
|
43
103
|
});
|
44
|
-
}
|
45
|
-
);
|
104
|
+
});
|
46
105
|
}
|
47
|
-
|
48
|
-
|
49
|
-
|
106
|
+
// 성공시 페이지 릴리즈 후 결과 반환
|
107
|
+
if (page) {
|
108
|
+
await releasePage(page);
|
50
109
|
}
|
110
|
+
return {
|
111
|
+
data: result
|
112
|
+
};
|
51
113
|
}
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
return element.textContent?.trim();
|
67
|
-
}
|
68
|
-
});
|
69
|
-
});
|
114
|
+
catch (error) {
|
115
|
+
lastError = error;
|
116
|
+
logger.error(`HeadlessScrap attempt ${attempt + 1} failed:`, error);
|
117
|
+
if (page) {
|
118
|
+
await releasePage(page);
|
119
|
+
page = null;
|
120
|
+
}
|
121
|
+
// 로그인 관련 에러나 복구 가능한 에러가 아니거나 마지막 재시도면 에러 발생
|
122
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
123
|
+
const isRecoverableError = errorMessage.includes('login') || errorMessage.includes('redirect') || errorMessage.includes('unauthorized') || errorMessage.includes('forbidden') || errorMessage.includes('session');
|
124
|
+
if (!isRecoverableError || attempt === maxRetries) {
|
125
|
+
throw error;
|
126
|
+
}
|
127
|
+
logger.info(`Retrying HeadlessScrap... (${attempt + 2}/${maxRetries + 1})`);
|
70
128
|
}
|
71
|
-
return {
|
72
|
-
data: result
|
73
|
-
};
|
74
|
-
}
|
75
|
-
catch (e) {
|
76
|
-
logger.error('Error in HeadlessScrap:', e);
|
77
|
-
throw e;
|
78
129
|
}
|
79
|
-
|
130
|
+
// 모든 재시도가 실패한 경우
|
131
|
+
if (page) {
|
80
132
|
await releasePage(page);
|
81
133
|
}
|
134
|
+
throw lastError || new Error('HeadlessScrap failed after all retry attempts');
|
82
135
|
}
|
83
136
|
HeadlessScrap.parameterSpec = [
|
84
137
|
{
|
@@ -105,6 +158,12 @@ HeadlessScrap.parameterSpec = [
|
|
105
158
|
type: 'string',
|
106
159
|
name: 'waitForTimeout',
|
107
160
|
label: 'wait-for-timeout'
|
161
|
+
},
|
162
|
+
{
|
163
|
+
type: 'number',
|
164
|
+
name: 'maxRetries',
|
165
|
+
label: 'maximum-retries',
|
166
|
+
value: 2
|
108
167
|
}
|
109
168
|
];
|
110
169
|
HeadlessScrap.help = 'integration/task/headless-scrap';
|
@@ -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 } = 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 } = connection\n\n const headers = {\n ...requestHeaders\n }\n\n const page = await acquireSessionPage()\n\n try {\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 await page.setExtraHTTPHeaders(headers)\n await page.goto(new URL(path, endpoint), { waitUntil: 'networkidle2' })\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 return {\n data: result\n }\n } catch (e) {\n logger.error('Error in HeadlessScrap:', e)\n throw e\n } finally {\n await releasePage(page)\n }\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\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'\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"]}
|
@@ -18,7 +18,8 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
18
18
|
if (!connection) {
|
19
19
|
throw new Error(`Connection '${connectionName}' is not established.`);
|
20
20
|
}
|
21
|
-
const { endpoint, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection;
|
21
|
+
const { endpoint, params: connectionParams, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection;
|
22
|
+
const loginPagePath = connectionParams?.loginPagePath || '/login';
|
22
23
|
let page = null;
|
23
24
|
let lastError = null;
|
24
25
|
// 재시도 로직
|
@@ -78,16 +79,43 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
78
79
|
requestOptions.body = typeof requestBody === 'string' ? requestBody : JSON.stringify(requestBody);
|
79
80
|
}
|
80
81
|
}
|
81
|
-
// fetch 요청 실행 - try-catch로 네트워크 에러 처리
|
82
|
-
const response = await page.evaluate(async (urlString, opts) => {
|
82
|
+
// fetch 요청 실행 - try-catch로 네트워크 에러 처리
|
83
|
+
const response = await page.evaluate(async (urlString, opts, loginPagePath) => {
|
83
84
|
try {
|
84
|
-
const response = await fetch(urlString, opts);
|
85
|
+
const response = await fetch(urlString, { ...opts, redirect: 'manual' });
|
85
86
|
const result = {
|
86
87
|
ok: response.ok,
|
87
88
|
status: response.status,
|
88
89
|
statusText: response.statusText,
|
89
90
|
headers: Object.fromEntries(response.headers.entries())
|
90
91
|
};
|
92
|
+
// 302 리디렉션 감지 - 로그인 페이지로의 리디렉션 체크
|
93
|
+
if ([301, 302, 307, 308].includes(response.status)) {
|
94
|
+
const location = response.headers.get('location') || '';
|
95
|
+
// connection의 loginPagePath와 일반적인 로그인 경로들을 체크
|
96
|
+
if (location.includes(loginPagePath) ||
|
97
|
+
location.includes('/login') ||
|
98
|
+
location.includes('/signin') ||
|
99
|
+
location.includes('/auth')) {
|
100
|
+
return {
|
101
|
+
...result,
|
102
|
+
error: `Redirected to login page: ${location}`,
|
103
|
+
redirectedToLogin: true,
|
104
|
+
location,
|
105
|
+
data: null
|
106
|
+
};
|
107
|
+
}
|
108
|
+
// 다른 리디렉션은 follow
|
109
|
+
const redirectResponse = await fetch(urlString, opts);
|
110
|
+
return {
|
111
|
+
ok: redirectResponse.ok,
|
112
|
+
status: redirectResponse.status,
|
113
|
+
statusText: redirectResponse.statusText,
|
114
|
+
headers: Object.fromEntries(redirectResponse.headers.entries()),
|
115
|
+
data: redirectResponse.ok ? await redirectResponse.json().catch(() => redirectResponse.text()).catch(() => null) : null,
|
116
|
+
error: redirectResponse.ok ? null : `HTTP ${redirectResponse.status}: ${redirectResponse.statusText}`
|
117
|
+
};
|
118
|
+
}
|
91
119
|
if (!response.ok) {
|
92
120
|
return {
|
93
121
|
...result,
|
@@ -120,7 +148,7 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
120
148
|
networkError: true
|
121
149
|
};
|
122
150
|
}
|
123
|
-
}, url.toString(), requestOptions);
|
151
|
+
}, url.toString(), requestOptions, loginPagePath);
|
124
152
|
// 네트워크 에러 체크 (fetch 자체가 실패한 경우)
|
125
153
|
if (response.networkError) {
|
126
154
|
if (attempt < maxRetries) {
|
@@ -138,6 +166,23 @@ async function executeHeadlessRequestWithRecovery(connectionName, options, conte
|
|
138
166
|
throw new Error(`Network error after ${maxRetries + 1} attempts: ${response.error}`);
|
139
167
|
}
|
140
168
|
}
|
169
|
+
// 로그인 리디렉션 감지 처리
|
170
|
+
if (response.redirectedToLogin) {
|
171
|
+
if (attempt < maxRetries) {
|
172
|
+
logger.warn(`Login redirect detected: ${response.location}, re-authenticating... (${attempt + 1}/${maxRetries + 1})`);
|
173
|
+
if (page) {
|
174
|
+
await releasePage(page);
|
175
|
+
page = null;
|
176
|
+
}
|
177
|
+
continue;
|
178
|
+
}
|
179
|
+
else {
|
180
|
+
if (page) {
|
181
|
+
await releasePage(page);
|
182
|
+
}
|
183
|
+
throw new Error(`Login redirect after ${maxRetries + 1} attempts: ${response.error}`);
|
184
|
+
}
|
185
|
+
}
|
141
186
|
// 세션 타임아웃 관련 에러 체크
|
142
187
|
if (!response.ok && isSessionTimeoutError(response.status)) {
|
143
188
|
if (attempt < maxRetries) {
|
@@ -214,7 +259,8 @@ function isRecoverableError(error) {
|
|
214
259
|
'session',
|
215
260
|
'authentication',
|
216
261
|
'login',
|
217
|
-
'expired'
|
262
|
+
'expired',
|
263
|
+
'redirected to login' // 로그인 리디렉션 추가
|
218
264
|
];
|
219
265
|
return recoverableErrorKeywords.some(keyword => errorMessage.includes(keyword));
|
220
266
|
}
|
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"file":"headless-request-with-recovery.js","sourceRoot":"","sources":["../../../../server/engine/task/utils/headless-request-with-recovery.ts"],"names":[],"mappings":";;AAiBA,gFAmNC;AApOD,iDAA8C;AAC9C,iEAA4D;AAa5D;;GAEG;AACI,KAAK,UAAU,kCAAkC,CACtD,cAAsB,EACtB,OAA+B,EAC/B,OAAgD;IAEhD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAA;IACxC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,GAAG,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,OAAO,CAAA;IAExG,+BAA+B;IAC/B,IAAI,WAAW,GAAG,IAAI,CAAA;IACtB,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,WAAW,GAAG,IAAA,cAAM,EAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACtC,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,sCAAiB,CAAC,2BAA2B,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC9F,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,eAAe,cAAc,uBAAuB,CAAC,CAAA;IACvE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,WAAW,EAAE,qBAAqB,EAAE,eAAe,EAAE,GAAG,UAAU,CAAA;IAExG,IAAI,IAAI,GAAG,IAAI,CAAA;IACf,IAAI,SAAS,GAAG,IAAI,CAAA;IAEpB,SAAS;IACT,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,SAAS;YACT,IAAI,GAAG,MAAM,kBAAkB,EAAE,CAAA;YAEjC,uBAAuB;YACvB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;YAC7B,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAA;YAC7C,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;gBACzC,MAAM,CAAC,IAAI,CAAC,gCAAgC,YAAY,EAAE,CAAC,CAAA;gBAC3D,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAA;YAC9D,CAAC;YAED,qBAAqB;YACrB,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAChB,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAA;gBAClD,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,MAAM,CAAC,IAAI,CAAC,mCAAmC,cAAc,iCAAiC,CAAC,CAAA;oBAC/F,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;oBACvB,IAAI,GAAG,MAAM,qBAAqB,EAAE,CAAA;gBACtC,CAAC;YACH,CAAC;YAED,SAAS;YACT,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;YACnC,IAAI,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;gBACnD,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;oBACrC,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;wBAChE,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;oBACxD,CAAC;gBACH,CAAC,CAAC,CAAA;YACJ,CAAC;YAED,WAAW;YACX,MAAM,cAAc,GAAQ;gBAC1B,MAAM;gBACN,OAAO,EAAE;oBACP,GAAG,OAAO;iBACX;gBACD,WAAW,EAAE,SAAS;aACvB,CAAA;YAED,wBAAwB;YACxB,IAAI,WAAW,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC7D,IAAI,WAAW,EAAE,CAAC;oBAChB,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,WAAW,CAAA;oBACpD,QAAQ,WAAW,EAAE,CAAC;wBACpB,KAAK,YAAY;4BACf,cAAc,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;4BACjD,MAAK;wBACP,KAAK,kBAAkB,CAAC;wBACxB;4BACE,cAAc,CAAC,IAAI,GAAG,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;4BACjG,MAAK;oBACT,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAA;oBAC3D,cAAc,CAAC,IAAI,GAAG,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;gBACnG,CAAC;YACH,CAAC;YAED,sCAAsC;YACtC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAClC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE;gBACxB,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;oBAE7C,MAAM,MAAM,GAAG;wBACb,EAAE,EAAE,QAAQ,CAAC,EAAE;wBACf,MAAM,EAAE,QAAQ,CAAC,MAAM;wBACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;wBAC/B,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;qBACxD,CAAA;oBAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;wBACjB,OAAO;4BACL,GAAG,MAAM;4BACT,KAAK,EAAE,QAAQ,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,UAAU,EAAE;4BACxD,IAAI,EAAE,IAAI;yBACX,CAAA;oBACH,CAAC;oBAED,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAA;oBAE9D,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;wBAC7C,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;oBACxC,CAAC;yBAAM,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;wBACzC,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;oBACxC,CAAC;yBAAM,CAAC;wBACN,+BAA+B;wBAC/B,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAAA;oBACvB,CAAC;oBAED,OAAO,MAAM,CAAA;gBACf,CAAC;gBAAC,OAAO,UAAU,EAAE,CAAC;oBACpB,gBAAgB;oBAChB,OAAO;wBACL,EAAE,EAAE,KAAK;wBACT,MAAM,EAAE,CAAC;wBACT,UAAU,EAAE,eAAe;wBAC3B,OAAO,EAAE,EAAE;wBACX,KAAK,EAAE,UAAU,CAAC,OAAO,IAAI,iBAAiB;wBAC9C,IAAI,EAAE,IAAI;wBACV,YAAY,EAAE,IAAI;qBACnB,CAAA;gBACH,CAAC;YACH,CAAC,EACD,GAAG,CAAC,QAAQ,EAAE,EACd,cAAc,CACf,CAAA;YAED,gCAAgC;YAChC,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;gBAC1B,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;oBACzB,MAAM,CAAC,IAAI,CAAC,2BAA2B,QAAQ,CAAC,KAAK,kBAAkB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;oBACxG,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;wBACvB,IAAI,GAAG,IAAI,CAAA;oBACb,CAAC;oBACD,SAAQ;gBACV,CAAC;qBAAM,CAAC;oBACN,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;oBACzB,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAU,GAAG,CAAC,cAAc,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;gBACtF,CAAC;YACH,CAAC;YAED,mBAAmB;YACnB,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3D,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;oBACzB,MAAM,CAAC,IAAI,CAAC,6BAA6B,QAAQ,CAAC,MAAM,mBAAmB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;oBAC5G,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;wBACvB,IAAI,GAAG,IAAI,CAAA;oBACb,CAAC;oBACD,SAAQ;gBACV,CAAC;qBAAM,CAAC;oBACN,IAAI,IAAI,EAAE,CAAC;wBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;oBACzB,CAAC;oBACD,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,GAAG,CAAC,cAAc,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;gBACxF,CAAC;YACH,CAAC;YAED,aAAa;YACb,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;YACjC,CAAC;YAED,uBAAuB;YACvB,MAAM,MAAM,GAAG;gBACb,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;aAC1B,CAAA;YAED,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;YAED,OAAO,MAAM,CAAA;QAEf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,GAAG,KAAK,CAAA;YACjB,MAAM,CAAC,KAAK,CAAC,4BAA4B,OAAO,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;YAEtE,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;gBACvB,IAAI,GAAG,IAAI,CAAA;YACb,CAAC;YAED,gCAAgC;YAChC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBACzD,MAAM,KAAK,CAAA;YACb,CAAC;YAED,MAAM,CAAC,IAAI,CAAC,wBAAwB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,WAAW,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IACD,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;AACzE,CAAC;AAED;;GAEG;AACH,SAAS,qBAAqB,CAAC,UAAkB;IAC/C,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;AACxC,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,KAAU;IACpC,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IAEvD,iCAAiC;IACjC,MAAM,wBAAwB,GAAG;QAC/B,iBAAiB;QACjB,eAAe;QACf,mBAAmB;QACnB,kBAAkB;QAClB,SAAS;QACT,WAAW;QACX,cAAc;QACd,WAAW;QACX,SAAS;QACT,gBAAgB;QAChB,OAAO;QACP,SAAS;KACV,CAAA;IAED,OAAO,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;AACjF,CAAC","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, acquireSessionPage, releasePage, reAuthenticateSession, validateSession } = connection\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) => {\n try {\n const response = await fetch(urlString, opts)\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 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 )\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.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 ]\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 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}"]}
|