api(parser): stream heartbeat to avoid 504s; disable proxy buffering headers
This commit is contained in:
@ -19,6 +19,55 @@ const pseudoRandomInRange = (key: string, min: number, max: number) => {
|
||||
export const maxDuration = 300; // 300 секунд = 5 минут
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Унифицированный ответ JSON с отключением буферизации (важно для Nginx)
|
||||
const jsonNoBuffering = (data: any, init?: { status?: number; headers?: HeadersInit }) => {
|
||||
const headers = new Headers(init?.headers || {});
|
||||
// Отключаем прокси-буферизацию и просим не кэшировать
|
||||
headers.set('X-Accel-Buffering', 'no');
|
||||
headers.set('Cache-Control', 'no-store');
|
||||
return NextResponse.json(data, { status: init?.status, headers });
|
||||
};
|
||||
|
||||
// Потоковый JSON-ответ с «пульсом», чтобы прокси не роняли соединение по таймауту
|
||||
// Отправляет пробел каждые 15с, затем итоговый JSON и закрывает поток
|
||||
const streamJsonWithHeartbeat = <T>(work: () => Promise<T>) => {
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const heartbeat = setInterval(() => {
|
||||
// Отправляем «пинг», чтобы прокси видели активность апстрима
|
||||
controller.enqueue(encoder.encode(' '));
|
||||
}, 15000);
|
||||
|
||||
work()
|
||||
.then((result) => {
|
||||
controller.enqueue(encoder.encode(JSON.stringify(result)));
|
||||
clearInterval(heartbeat);
|
||||
controller.close();
|
||||
})
|
||||
.catch((err) => {
|
||||
const errorPayload = {
|
||||
error: 'Внутренняя ошибка во время обработки запроса',
|
||||
details: String(err),
|
||||
};
|
||||
controller.enqueue(encoder.encode(JSON.stringify(errorPayload)));
|
||||
clearInterval(heartbeat);
|
||||
controller.close();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-Accel-Buffering': 'no', // важно для Nginx, чтобы не буферизировал стрим
|
||||
'Cache-Control': 'no-store',
|
||||
'Connection': 'keep-alive',
|
||||
// Поддержка chunked по умолчанию (без Content-Length)
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Функция для расчета реальной позиции с учетом страницы
|
||||
const calculatePosition = (
|
||||
position: number | string,
|
||||
@ -54,7 +103,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
// Проверяем наличие обязательных параметров
|
||||
if (!query || !myArticleId) {
|
||||
return NextResponse.json(
|
||||
return jsonNoBuffering(
|
||||
{ error: 'Не указаны все обязательные поля (запрос и артикул)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
@ -256,17 +305,18 @@ export async function POST(req: NextRequest) {
|
||||
};
|
||||
|
||||
// Возвращаем результаты из БД
|
||||
return NextResponse.json(responseData);
|
||||
return jsonNoBuffering(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
// Если в кэше нет данных, делаем новый запрос к Wildberries
|
||||
console.log('Кэшированных данных нет, выполняем парсинг Wildberries');
|
||||
|
||||
// Устанавливаем таймаут для ответа API (всегда используем увеличенный таймаут)
|
||||
// Выполняем тяжелую работу в фоне и стримим «пульс» в ответ
|
||||
const work = async (): Promise<SearchResponse> => {
|
||||
// Устанавливаем предупреждение, если запрос очень долгий
|
||||
const apiTimeout = setTimeout(() => {
|
||||
console.log('Предупреждение: API запрос выполняется дольше ожидаемого, но продолжает работу');
|
||||
}, 90000); // 90 секунд для поиска больших объемов товаров
|
||||
}, 90000);
|
||||
|
||||
try {
|
||||
// Выполняем парсинг с расширенными возможностями
|
||||
@ -278,22 +328,17 @@ export async function POST(req: NextRequest) {
|
||||
maxItems
|
||||
);
|
||||
|
||||
// Очищаем таймаут
|
||||
clearTimeout(apiTimeout);
|
||||
|
||||
// Сохраняем историю запроса в локальное хранилище
|
||||
saveSearchHistory(query, myArticleId, competitorArticleId);
|
||||
|
||||
// Сохраняем данные в PostgreSQL
|
||||
// Сохраняем данные в PostgreSQL (не блокируем общий ответ в случае ошибок)
|
||||
try {
|
||||
// Создаем запись о поисковом запросе
|
||||
const searchQuery = await prisma.searchQuery.create({
|
||||
data: {
|
||||
query: query,
|
||||
},
|
||||
data: { query },
|
||||
});
|
||||
|
||||
// Сохраняем данные о всех товарах
|
||||
for (const product of result.products) {
|
||||
await prisma.product.upsert({
|
||||
where: { article: product.article },
|
||||
@ -314,24 +359,18 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Сохраняем данные о позициях для всех товаров
|
||||
for (const [articleId, cityPositions] of Object.entries(result.positions)) {
|
||||
const productDB = await prisma.product.findUnique({
|
||||
where: { article: articleId },
|
||||
});
|
||||
|
||||
const productDB = await prisma.product.findUnique({ where: { article: articleId } });
|
||||
if (productDB && cityPositions.length > 0) {
|
||||
for (const posData of cityPositions) {
|
||||
const position = posData.position;
|
||||
|
||||
if (position && position > 0) {
|
||||
const page = Math.ceil(position / 100);
|
||||
|
||||
await prisma.position.create({
|
||||
data: {
|
||||
city: posData.city,
|
||||
position: position,
|
||||
page: page,
|
||||
position,
|
||||
page,
|
||||
productId: productDB.id,
|
||||
searchQueryId: searchQuery.id,
|
||||
},
|
||||
@ -341,72 +380,66 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Данные успешно сохранены в базе данных`);
|
||||
console.log('Данные успешно сохранены в базе данных');
|
||||
} catch (dbError) {
|
||||
console.error('Ошибка при сохранении в базу данных:', dbError);
|
||||
// Продолжаем выполнение, даже если сохранение в БД не удалось
|
||||
}
|
||||
|
||||
// Проверяем структуру результата перед возвратом
|
||||
if (!result || !result.products || result.products.length === 0) {
|
||||
console.error('Парсер вернул пустой результат');
|
||||
const fallbackResponse: SearchResponse = {
|
||||
products: [{
|
||||
return {
|
||||
products: [
|
||||
{
|
||||
name: 'Ошибка при получении данных',
|
||||
brand: 'Н/Д',
|
||||
price: 0,
|
||||
article: myArticleId,
|
||||
imageUrl: '/images/no-image.svg',
|
||||
}],
|
||||
},
|
||||
],
|
||||
positions: { [myArticleId]: [] },
|
||||
myArticleId: myArticleId,
|
||||
competitorArticleId: competitorArticleId,
|
||||
};
|
||||
return NextResponse.json(fallbackResponse);
|
||||
myArticleId,
|
||||
competitorArticleId,
|
||||
} satisfies SearchResponse;
|
||||
}
|
||||
|
||||
// Формируем ответ в новом формате
|
||||
const responseData: SearchResponse = {
|
||||
return {
|
||||
products: result.products,
|
||||
positions: result.positions,
|
||||
myArticleId: myArticleId,
|
||||
competitorArticleId: competitorArticleId,
|
||||
};
|
||||
|
||||
// Возвращаем результат
|
||||
return NextResponse.json(responseData);
|
||||
myArticleId,
|
||||
competitorArticleId,
|
||||
} satisfies SearchResponse;
|
||||
} catch (parsingError) {
|
||||
// Очищаем таймаут
|
||||
clearTimeout(apiTimeout);
|
||||
|
||||
console.error('Ошибка при парсинге:', parsingError);
|
||||
|
||||
// Создаем базовый ответ с сообщением об ошибке в новом формате
|
||||
const errorResponse: SearchResponse = {
|
||||
products: [{
|
||||
return {
|
||||
products: [
|
||||
{
|
||||
name: 'Ошибка при получении данных',
|
||||
brand: 'Н/Д',
|
||||
price: 0,
|
||||
article: myArticleId,
|
||||
imageUrl: '/images/no-image.svg',
|
||||
}],
|
||||
},
|
||||
],
|
||||
positions: { [myArticleId]: [] },
|
||||
myArticleId: myArticleId,
|
||||
competitorArticleId: competitorArticleId,
|
||||
myArticleId,
|
||||
competitorArticleId,
|
||||
} satisfies SearchResponse;
|
||||
}
|
||||
};
|
||||
|
||||
return NextResponse.json(errorResponse, { status: 500 });
|
||||
}
|
||||
return streamJsonWithHeartbeat(work);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при парсинге:', error);
|
||||
return NextResponse.json(
|
||||
return jsonNoBuffering(
|
||||
{ error: 'Ошибка при парсинге данных', details: String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обработке запроса:', error);
|
||||
return NextResponse.json(
|
||||
return jsonNoBuffering(
|
||||
{ error: 'Внутренняя ошибка сервера', details: String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
|
Reference in New Issue
Block a user