Add fetchPage() with axios, timeout, and retry logic
Some checks failed
CI/CD Pipeline - Apartment API / Run Linting (pull_request) Successful in 9m39s
CI/CD Pipeline - Apartment API / Run Tests (pull_request) Successful in 9m50s
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 15s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Failing after 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped
Some checks failed
CI/CD Pipeline - Apartment API / Run Linting (pull_request) Successful in 9m39s
CI/CD Pipeline - Apartment API / Run Tests (pull_request) Successful in 9m50s
CI/CD Pipeline - Apartment API / Scan Dependencies (pull_request) Successful in 15s
CI/CD Pipeline - Apartment API / Send Webhook Notification (pull_request) Failing after 2s
CI/CD Pipeline - Apartment API / Build & Push Image (pull_request) Has been skipped
CI/CD Pipeline - Apartment API / Deploy to Production (pull_request) Has been skipped
Implement fetchPage() function for HTTP scraping: - Uses axios for HTTP GET requests - Configurable timeout (default 30s) and User-Agent header - Retry with exponential backoff (1s, 2s, 4s) on 5xx and network errors - Does not retry on 4xx client errors - Logs each attempt with attempt number and error details - Returns HTML string on success, throws after retries exhausted
This commit is contained in:
114
services/scraperService.js
Normal file
114
services/scraperService.js
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Scraper Service
|
||||
*
|
||||
* Core scraper logic including HTTP fetching, HTML parsing,
|
||||
* data transformation, and database operations.
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const config = require('../config/scraper');
|
||||
|
||||
/**
|
||||
* Sleep utility for retry delays
|
||||
* @param {number} ms - Milliseconds to sleep
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an error is retryable
|
||||
* @param {Error} error - Axios error
|
||||
* @returns {boolean} True if should retry
|
||||
*/
|
||||
function isRetryableError(error) {
|
||||
// Network errors (timeout, DNS, connection) don't have a response property
|
||||
if (!error.response) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const status = error.response.status;
|
||||
|
||||
// 5xx server errors are retryable
|
||||
if (status >= 500) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 429 Too Many Requests - do not retry immediately
|
||||
if (status === 429) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4xx client errors - do not retry
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch page HTML with retry logic
|
||||
* @param {string} url - Target URL
|
||||
* @param {Object} logger - Logger instance
|
||||
* @returns {Promise<string>} HTML content
|
||||
* @throws {Error} After all retries exhausted
|
||||
*/
|
||||
async function fetchPage(url, logger) {
|
||||
const { maxRetries, baseDelay, timeout } = config.RETRY_CONFIG;
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
||||
try {
|
||||
logger.info('Fetching page', { url, attempt });
|
||||
|
||||
const response = await axios.get(url, {
|
||||
timeout,
|
||||
headers: {
|
||||
'User-Agent': config.USER_AGENT
|
||||
},
|
||||
maxRedirects: 5,
|
||||
validateStatus: (status) => status < 400 // Accept 2xx and 3xx
|
||||
});
|
||||
|
||||
logger.info('Page fetched successfully', {
|
||||
status: response.status,
|
||||
contentLength: response.data.length
|
||||
});
|
||||
|
||||
return response.data;
|
||||
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
// Determine if error is retryable
|
||||
const isRetryable = isRetryableError(error);
|
||||
|
||||
logger.warn('Fetch attempt failed', {
|
||||
attempt,
|
||||
errorType: error.name,
|
||||
errorMessage: error.message,
|
||||
statusCode: error.response?.status,
|
||||
isRetryable
|
||||
});
|
||||
|
||||
// Don't retry non-retryable errors (4xx)
|
||||
if (!isRetryable) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Don't wait after last attempt
|
||||
if (attempt <= maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
||||
logger.info('Waiting before retry', { delay });
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchPage,
|
||||
// Export for testing
|
||||
isRetryableError,
|
||||
sleep
|
||||
};
|
||||
Reference in New Issue
Block a user