Add 2 KB padding comment after headers to push past proxy buffer threshold, enable TCP_NODELAY on the socket, and remove erroneous chunked_transfer_encoding off from Nginx that caused full response buffering.
145 lines
3.7 KiB
TypeScript
145 lines
3.7 KiB
TypeScript
import { Router } from 'express';
|
|
import multer from 'multer';
|
|
import { asyncHandler } from '../utils';
|
|
import { importStatement, isValidationError } from '../services/import';
|
|
import {
|
|
convertPdfToStatementStreaming,
|
|
isPdfConversionError,
|
|
} from '../services/pdfToStatement';
|
|
|
|
const upload = multer({
|
|
storage: multer.memoryStorage(),
|
|
limits: { fileSize: 15 * 1024 * 1024 },
|
|
});
|
|
|
|
function isPdfFile(file: { mimetype: string; originalname: string }): boolean {
|
|
const name = file.originalname.toLowerCase();
|
|
return (
|
|
file.mimetype === 'application/pdf' ||
|
|
name.endsWith('.pdf')
|
|
);
|
|
}
|
|
|
|
function isJsonFile(file: { mimetype: string; originalname: string }): boolean {
|
|
const name = file.originalname.toLowerCase();
|
|
return (
|
|
file.mimetype === 'application/json' ||
|
|
name.endsWith('.json')
|
|
);
|
|
}
|
|
|
|
function sseWrite(res: import('express').Response, data: Record<string, unknown>) {
|
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
}
|
|
|
|
/**
|
|
* Send a 2 KB comment block to push past any proxy buffering threshold.
|
|
* Nginx and other reverse proxies often buffer the first few KB before
|
|
* starting to stream — this padding forces the initial flush.
|
|
*/
|
|
function ssePadding(res: import('express').Response) {
|
|
res.write(`: ${' '.repeat(2048)}\n\n`);
|
|
}
|
|
|
|
const router = Router();
|
|
|
|
router.post(
|
|
'/statement',
|
|
upload.single('file'),
|
|
asyncHandler(async (req, res) => {
|
|
const file = req.file;
|
|
if (!file) {
|
|
res.status(400).json({
|
|
error: 'BAD_REQUEST',
|
|
message: 'Файл не загружен',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!isPdfFile(file) && !isJsonFile(file)) {
|
|
res.status(400).json({
|
|
error: 'BAD_REQUEST',
|
|
message: 'Допустимы только файлы PDF или JSON',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isPdfFile(file)) {
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
res.socket?.setNoDelay(true);
|
|
res.flushHeaders();
|
|
ssePadding(res);
|
|
|
|
try {
|
|
const converted = await convertPdfToStatementStreaming(
|
|
file.buffer,
|
|
(stage, progress, message) => {
|
|
sseWrite(res, { stage, progress, message });
|
|
},
|
|
);
|
|
|
|
if (isPdfConversionError(converted)) {
|
|
sseWrite(res, {
|
|
stage: 'error',
|
|
message: converted.message,
|
|
});
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const result = await importStatement(converted);
|
|
if (isValidationError(result)) {
|
|
sseWrite(res, {
|
|
stage: 'error',
|
|
message: (result as { message: string }).message,
|
|
});
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
sseWrite(res, {
|
|
stage: 'done',
|
|
progress: 100,
|
|
result,
|
|
});
|
|
} catch (err) {
|
|
console.error('SSE import error:', err);
|
|
sseWrite(res, {
|
|
stage: 'error',
|
|
message: 'Внутренняя ошибка сервера',
|
|
});
|
|
}
|
|
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// JSON files — synchronous response as before
|
|
let body: unknown;
|
|
try {
|
|
body = JSON.parse(file.buffer.toString('utf-8'));
|
|
} catch {
|
|
res.status(400).json({
|
|
error: 'BAD_REQUEST',
|
|
message: 'Некорректный JSON-файл',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const result = await importStatement(body);
|
|
if (isValidationError(result)) {
|
|
res.status((result as { status: number }).status).json({
|
|
error: (result as { error: string }).error,
|
|
message: (result as { message: string }).message,
|
|
});
|
|
return;
|
|
}
|
|
res.json(result);
|
|
}),
|
|
);
|
|
|
|
export default router;
|