logs para averiaguar el fallo de subida de archivos desde la app

This commit is contained in:
abiandev
2026-07-29 12:59:29 +02:00
parent 73c1416f7a
commit c093c3f7c2
6 changed files with 219 additions and 25 deletions
+70
View File
@@ -55,6 +55,76 @@ app.use((req, res, next) => {
contentType.toLowerCase().startsWith('multipart/form-data');
if (isMultipartFormData) {
const startedAt = process.hrtime.bigint();
const baseLogPayload = {
request_id: requestId,
method: req.method,
path: req.originalUrl || req.url,
ip: req.ip || null,
content_type: contentType,
content_length: Number.parseInt(req.get('content-length'), 10) || null,
has_authorization_header: Boolean(authorizationHeader),
user_agent: String(req.get('user-agent') || '').slice(0, 255) || null
};
let responseFinished = false;
appendPostLog({
event: 'upload_request_started',
...baseLogPayload
});
res.once('finish', () => {
responseFinished = true;
const parserStatus = req.uploadDiagnostics?.parser_status || 'not_reached';
let failureStage = null;
if (res.statusCode >= 400) {
if (parserStatus === 'rejected') {
failureStage = 'multipart_parser';
} else if (parserStatus === 'parsed') {
failureStage = 'controller_or_persistence';
} else {
failureStage = 'authentication_rate_limit_or_route';
}
}
appendPostLog({
event: 'upload_request_finished',
...baseLogPayload,
status_code: res.statusCode,
duration_ms: Number(
(Number(process.hrtime.bigint() - startedAt) / 1e6).toFixed(2)
),
request_complete: req.complete,
outcome: res.statusCode < 400 ? 'success' : 'error',
failure_stage: failureStage,
upload: req.uploadDiagnostics || {
parser_status: 'not_reached'
}
});
});
res.once('close', () => {
if (responseFinished) {
return;
}
appendPostLog({
event: 'upload_request_interrupted',
...baseLogPayload,
status_code: res.statusCode,
duration_ms: Number(
(Number(process.hrtime.bigint() - startedAt) / 1e6).toFixed(2)
),
request_complete: req.complete,
outcome: 'interrupted',
failure_stage: 'transport_or_client_disconnect',
upload: req.uploadDiagnostics || {
parser_status: 'not_reached'
}
});
});
return next();
}
+22
View File
@@ -1,5 +1,10 @@
const multer = require('multer');
const path = require('path');
const {
beginUploadParsing,
markUploadParsed,
markUploadRejected
} = require('../utils/uploadDiagnostics');
const MAX_DRIVER_LICENSE_SIZE_BYTES = 5 * 1024 * 1024;
const FRONT_FILE_FIELD = 'carnet_conducir_frontal';
@@ -40,14 +45,31 @@ const internalUpload = multer({
});
const uploadDriverLicense = (req, res, next) => {
const flow = 'driver_license';
beginUploadParsing(req, flow);
internalUpload.fields([
{ name: FRONT_FILE_FIELD, maxCount: 1 },
{ name: BACK_FILE_FIELD, maxCount: 1 }
])(req, res, (error) => {
if (!error) {
const uploadedFiles = [
...(Array.isArray(req.files?.[FRONT_FILE_FIELD])
? req.files[FRONT_FILE_FIELD]
: []),
...(Array.isArray(req.files?.[BACK_FILE_FIELD])
? req.files[BACK_FILE_FIELD]
: [])
];
markUploadParsed(req, {
flow,
files: uploadedFiles
});
return next();
}
markUploadRejected(req, { flow, error });
if (error instanceof multer.MulterError) {
if (error.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
+14
View File
@@ -2,6 +2,11 @@ const crypto = require('crypto');
const fs = require('fs');
const multer = require('multer');
const path = require('path');
const {
beginUploadParsing,
markUploadParsed,
markUploadRejected
} = require('../utils/uploadDiagnostics');
const MAX_PROFILE_PHOTO_SIZE_BYTES = 5 * 1024 * 1024;
const PROFILE_UPLOADS_DIR = path.resolve(__dirname, '..', '..', 'uploads', 'profile');
@@ -57,11 +62,20 @@ const internalUpload = multer({
});
const uploadProfilePhoto = (req, res, next) => {
const flow = 'profile_photo';
beginUploadParsing(req, flow);
internalUpload.single('foto_perfil')(req, res, (error) => {
if (!error) {
markUploadParsed(req, {
flow,
files: req.file ? [req.file] : []
});
return next();
}
markUploadRejected(req, { flow, error });
if (error instanceof multer.MulterError) {
if (error.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'Archivo demasiado grande. Maximo 5MB.' });
+26 -15
View File
@@ -4,11 +4,15 @@ const multer = require('multer');
const path = require('path');
const {
getTripStatusUploadsDir,
getTripStatusFallbackUploadsDir,
replicateUploadedFilesToRemote,
removeUploadedTripStatusFiles
} = require('../services/tripStatusPhotoStorage');
const { appendPostLog } = require('../utils/postLog');
const {
beginUploadParsing,
markUploadParsed,
markUploadRejected
} = require('../utils/uploadDiagnostics');
const MAX_TRIP_STATUS_PHOTO_SIZE_BYTES = 15 * 1024 * 1024;
const MAX_TRIP_STATUS_FILES = 5;
@@ -28,21 +32,9 @@ const getTripDirectorySegment = (req) => {
const getTripStatusUploadsTripDir = (req) =>
path.join(getTripStatusUploadsDir(), getTripDirectorySegment(req));
const getTripStatusFallbackUploadsTripDir = (req) =>
path.join(getTripStatusFallbackUploadsDir(), getTripDirectorySegment(req));
const ensureTripStatusUploadsDir = (req) => {
const primaryTripDir = getTripStatusUploadsTripDir(req);
try {
fs.mkdirSync(primaryTripDir, { recursive: true });
return primaryTripDir;
} catch (primaryError) {
const fallbackTripDir = getTripStatusFallbackUploadsTripDir(req);
fs.mkdirSync(fallbackTripDir, { recursive: true });
return fallbackTripDir;
}
fs.mkdirSync(getTripStatusUploadsTripDir(req), { recursive: true });
};
const getExtensionFromMimeType = (mimeType) => {
@@ -68,7 +60,8 @@ const getExtensionFromMimeType = (mimeType) => {
const storage = multer.diskStorage({
destination: (req, file, cb) => {
try {
cb(null, ensureTripStatusUploadsDir(req));
ensureTripStatusUploadsDir(req);
cb(null, getTripStatusUploadsTripDir(req));
} catch (error) {
cb(error);
}
@@ -96,6 +89,12 @@ const internalUpload = multer({
});
const uploadTripStatusPhotos = (req, res, next) => {
const flow = 'trip_status_photos';
const storageMode = String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
.trim()
.toLowerCase();
beginUploadParsing(req, flow);
internalUpload.fields([
{ name: 'fotos', maxCount: MAX_TRIP_STATUS_FILES },
{ name: 'fotos[]', maxCount: MAX_TRIP_STATUS_FILES }
@@ -104,6 +103,11 @@ const uploadTripStatusPhotos = (req, res, next) => {
const uploadedFiles = collectUploadedTripStatusFiles(req);
const authorizationHeader = req.get('authorization');
markUploadParsed(req, {
flow,
files: uploadedFiles,
storageMode
});
appendPostLog({
request_id: req.requestId || null,
method: req.method,
@@ -127,9 +131,16 @@ const uploadTripStatusPhotos = (req, res, next) => {
tripId: req.params?.id,
files: uploadedFiles
});
markUploadParsed(req, {
flow,
files: uploadedFiles,
storageMode
});
return next();
}
markUploadRejected(req, { flow, error });
if (error instanceof multer.MulterError) {
return res.status(400).json({
success: false,
+19 -10
View File
@@ -6,11 +6,6 @@ const DEFAULT_SFTP_PORT = 22;
let sftpClientFactoryOverride = null;
const PRIMARY_TRIP_STATUS_UPLOAD_DIR =
'/var/www/vhosts/gestion.abianservice.com/httpdocs/produccion/app/fotos_estado_react_native/trips/status';
const FALLBACK_TRIP_STATUS_UPLOAD_DIR =
path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status');
const resolveUploadDirCandidate = (uploadDir) =>
path.isAbsolute(uploadDir)
? uploadDir
@@ -42,10 +37,19 @@ const selectUploadDirCandidate = (uploadDirs) =>
.sort((left, right) => left.score - right.score || left.index - right.index)[0]?.path;
const getTripStatusUploadsDir = () => {
return PRIMARY_TRIP_STATUS_UPLOAD_DIR;
};
const configuredUploadDir = String(process.env.TRIP_STATUS_UPLOAD_DIR || '').trim();
const getTripStatusFallbackUploadsDir = () => FALLBACK_TRIP_STATUS_UPLOAD_DIR;
if (configuredUploadDir) {
const configuredUploadDirs = configuredUploadDir
.split(';')
.map((uploadDir) => uploadDir.trim())
.filter(Boolean);
return selectUploadDirCandidate(configuredUploadDirs);
}
return path.resolve(__dirname, '..', '..', 'uploads', 'trips', 'status');
};
const getTripStatusPhotoStorageMode = () =>
String(process.env.TRIP_STATUS_PHOTO_STORAGE_MODE || 'local')
@@ -205,7 +209,7 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
file.tripStatusTripId = tripDirectorySegment;
}
await withSftpClient(
const remoteOperationSucceeded = await withSftpClient(
async (client, sftpConfig) => {
await ensureRemoteTripDirectory(client, {
remoteBaseDir: sftpConfig.remoteBaseDir,
@@ -238,6 +242,12 @@ const replicateUploadedFilesToRemote = async ({ tripId, files }) => {
logContext: 'replicate_upload'
}
);
if (shouldUseRemoteStorage() && !remoteOperationSucceeded) {
for (const file of normalizedFiles) {
file.tripStatusRemoteUploaded = false;
}
}
};
const removeRemoteFiles = async (remoteFilePaths, { logContext }) => {
@@ -389,7 +399,6 @@ const __resetSftpClientFactoryForTests = () => {
module.exports = {
getTripStatusUploadsDir,
getTripStatusFallbackUploadsDir,
replicateUploadedFilesToRemote,
removeUploadedTripStatusFiles,
removeStatusPhotosByName,
+68
View File
@@ -0,0 +1,68 @@
const path = require('path');
const getBodyFieldNames = (req) =>
Object.keys(req.body || {}).sort();
const getFileMetadata = (file) => {
const originalExtension = path.extname(String(file?.originalname || '')).toLowerCase();
let remoteUploadStatus = null;
if (file?.tripStatusRemoteUploaded === true) {
remoteUploadStatus = 'success';
} else if (file?.tripStatusRemoteUploaded === false) {
remoteUploadStatus = 'failed';
}
return {
field_name: file?.fieldname || null,
original_extension: originalExtension || null,
mimetype: file?.mimetype || null,
size_bytes: Number.isFinite(file?.size) ? file.size : null,
stored_filename: file?.filename || null,
local_file_created: Boolean(file?.path),
remote_upload_status: remoteUploadStatus
};
};
const beginUploadParsing = (req, flow) => {
req.uploadDiagnostics = {
flow,
parser: 'multer',
parser_status: 'started'
};
};
const markUploadParsed = (req, { flow, files, storageMode = null }) => {
const normalizedFiles = Array.isArray(files) ? files : [];
req.uploadDiagnostics = {
flow,
parser: 'multer',
parser_status: 'parsed',
body_fields: getBodyFieldNames(req),
file_count: normalizedFiles.length,
files: normalizedFiles.map(getFileMetadata),
storage_mode: storageMode
};
};
const markUploadRejected = (req, { flow, error }) => {
req.uploadDiagnostics = {
flow,
parser: 'multer',
parser_status: 'rejected',
body_fields: getBodyFieldNames(req),
error: {
type: error?.constructor?.name || 'Error',
code: error?.code || error?.message || null,
field_name: error?.field || null,
message: String(error?.message || 'Unknown upload error').slice(0, 500)
}
};
};
module.exports = {
beginUploadParsing,
markUploadParsed,
markUploadRejected
};