api-save-debugger.js•13.8 kB
#!/usr/bin/env node
/**
* API Save Debugger - Targeted debugging for HTTP 500 errors
* Focuses on actual root causes rather than assumptions
*/
export class APISaveDebugger {
constructor() {
this.debugLog = [];
}
/**
* Comprehensive debugging of API save failures
*/
async debugAPISaveFailure(composerJSON, page, originalError) {
console.error('[API_SAVE_DEBUGGER] Starting comprehensive debugging');
const analysis = {
timestamp: new Date().toISOString(),
originalError: originalError,
checks: {},
recommendations: []
};
// 1. JSON Structure Validation
analysis.checks.jsonStructure = await this.validateJSONStructure(composerJSON);
// 2. Header Completeness Check (your past issue)
analysis.checks.headerCompleteness = this.checkHeaderCompleteness(composerJSON);
// 3. Content Encoding Issues
analysis.checks.contentEncoding = this.checkContentEncoding(composerJSON);
// 4. API Response Analysis
analysis.checks.apiResponse = await this.analyzeAPIResponse(originalError);
// 5. Authentication State
analysis.checks.authentication = await this.checkAuthenticationState(page);
// 6. FormData Structure
analysis.checks.formData = this.validateFormDataStructure(composerJSON);
// 7. Size vs Content Analysis
analysis.checks.sizeAnalysis = this.analyzeSizeVsContent(composerJSON);
// Generate actionable recommendations
analysis.recommendations = this.generateRecommendations(analysis.checks);
return analysis;
}
/**
* 1. Validate JSON structure integrity
*/
async validateJSONStructure(composerJSON) {
const check = {
name: 'JSON Structure Validation',
passed: true,
issues: [],
details: {}
};
try {
// Test JSON serialization/deserialization
const serialized = JSON.stringify(composerJSON);
const parsed = JSON.parse(serialized);
check.details.serializationSize = serialized.length;
check.details.canSerialize = true;
check.details.canDeserialize = true;
// Check for circular references
try {
JSON.stringify(composerJSON);
} catch (error) {
if (error.message.includes('circular')) {
check.passed = false;
check.issues.push('Circular reference detected in JSON structure');
}
}
// Check for undefined values
const hasUndefined = serialized.includes('undefined');
if (hasUndefined) {
check.passed = false;
check.issues.push('Undefined values found in JSON');
}
// Check for NaN values
const hasNaN = serialized.includes('NaN');
if (hasNaN) {
check.passed = false;
check.issues.push('NaN values found in JSON');
}
check.details.structureKeys = Object.keys(composerJSON);
check.details.hasMetadata = !!composerJSON.metadata;
check.details.hasStructure = !!composerJSON.structure;
check.details.widgetCount = composerJSON.structure?.length || 0;
} catch (error) {
check.passed = false;
check.issues.push(`JSON serialization failed: ${error.message}`);
}
return check;
}
/**
* 2. Check header completeness (your past issue)
*/
checkHeaderCompleteness(composerJSON) {
const check = {
name: 'Header Completeness Check',
passed: true,
issues: [],
details: {}
};
// Check for required header fields
const requiredFields = ['name', 'description', 'keywords', 'category', 'duration'];
const metadata = composerJSON.metadata || {};
check.details.metadataPresent = !!composerJSON.metadata;
check.details.metadataKeys = Object.keys(metadata);
requiredFields.forEach(field => {
if (!metadata[field]) {
check.passed = false;
check.issues.push(`Missing required metadata field: ${field}`);
}
});
// Check for incomplete header widget
const headerWidget = composerJSON.structure?.find(w => w.type === 'head-1');
if (headerWidget) {
check.details.hasHeaderWidget = true;
check.details.headerContent = headerWidget;
const requiredHeaderFields = ['category', 'author_name', 'author_office'];
requiredHeaderFields.forEach(field => {
if (!headerWidget[field]) {
check.passed = false;
check.issues.push(`Missing required header field: ${field}`);
}
});
} else {
check.passed = false;
check.issues.push('No header widget (head-1) found');
}
return check;
}
/**
* 3. Check content encoding issues
*/
checkContentEncoding(composerJSON) {
const check = {
name: 'Content Encoding Check',
passed: true,
issues: [],
details: {}
};
const problematicChars = [];
const serialized = JSON.stringify(composerJSON);
// Check for problematic characters
const problematicPatterns = [
{ pattern: /[\x00-\x1F]/, name: 'control characters' },
{ pattern: /[\uFFFE\uFFFF]/, name: 'invalid Unicode' },
{ pattern: /\uFEFF/, name: 'byte order mark' },
{ pattern: /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/, name: 'null/control chars' }
];
problematicPatterns.forEach(({ pattern, name }) => {
if (pattern.test(serialized)) {
check.passed = false;
check.issues.push(`Found ${name} in content`);
problematicChars.push(name);
}
});
// Check for truncated content (your reported issue)
const widgets = composerJSON.structure || [];
widgets.forEach((widget, index) => {
if (widget.type === 'text-1' && widget.text) {
// Check for incomplete HTML tags
const openTags = (widget.text.match(/</g) || []).length;
const closeTags = (widget.text.match(/>/g) || []).length;
if (openTags !== closeTags) {
check.passed = false;
check.issues.push(`Widget ${index} has incomplete HTML tags`);
}
// Check for truncated sentences (more accurate)
if (widget.text.includes('<p>') &&
!widget.text.endsWith('</p>') &&
!widget.text.endsWith('</ul>') &&
!widget.text.endsWith('</ol>') &&
!widget.text.endsWith('</div>') &&
!widget.text.endsWith('</h1>') &&
!widget.text.endsWith('</h2>') &&
!widget.text.endsWith('</h3>')) {
check.passed = false;
check.issues.push(`Widget ${index} may have truncated content`);
}
}
});
check.details.totalSize = serialized.length;
check.details.problematicChars = problematicChars;
check.details.encoding = 'UTF-8';
return check;
}
/**
* 4. Analyze API response details
*/
async analyzeAPIResponse(originalError) {
const check = {
name: 'API Response Analysis',
passed: false,
issues: [],
details: {}
};
if (originalError.status) {
check.details.httpStatus = originalError.status;
check.details.statusText = originalError.statusText;
check.details.responseText = originalError.responseText;
// Analyze HTML error response
if (originalError.responseText && originalError.responseText.includes('<!DOCTYPE html>')) {
check.issues.push('Server returned HTML error page instead of JSON');
// Try to extract error details from HTML
const htmlContent = originalError.responseText;
const titleMatch = htmlContent.match(/<title>([^<]+)<\/title>/i);
const errorMatch = htmlContent.match(/<h1[^>]*>([^<]+)<\/h1>/i);
if (titleMatch) {
check.details.htmlTitle = titleMatch[1];
}
if (errorMatch) {
check.details.htmlError = errorMatch[1];
}
}
// Check for specific error patterns
if (originalError.status === 500) {
check.issues.push('HTTP 500 indicates server processing error, not client request issue');
}
if (originalError.status === 413) {
check.issues.push('HTTP 413 indicates payload too large');
}
if (originalError.status === 400) {
check.issues.push('HTTP 400 indicates malformed request');
}
}
return check;
}
/**
* 5. Check authentication state
*/
async checkAuthenticationState(page) {
const check = {
name: 'Authentication State Check',
passed: true,
issues: [],
details: {}
};
try {
const authData = await page.evaluate(() => {
const activeProject = localStorage.getItem('rdp-composer-active-project');
const userData = localStorage.getItem('rdp-composer-user-data');
return {
hasActiveProject: !!activeProject,
hasUserData: !!userData,
projectDataLength: activeProject?.length || 0,
userDataLength: userData?.length || 0,
currentURL: window.location.href
};
});
check.details = authData;
if (!authData.hasActiveProject) {
check.passed = false;
check.issues.push('Missing active project data in localStorage');
}
if (!authData.hasUserData) {
check.passed = false;
check.issues.push('Missing user data in localStorage');
}
} catch (error) {
check.passed = false;
check.issues.push(`Authentication check failed: ${error.message}`);
}
return check;
}
/**
* 6. Validate FormData structure
*/
validateFormDataStructure(composerJSON) {
const check = {
name: 'FormData Structure Check',
passed: true,
issues: [],
details: {}
};
try {
// Simulate FormData creation
const blob = new Blob([JSON.stringify(composerJSON)], { type: 'application/json' });
const formData = new FormData();
formData.append('file', blob, 'test.rdpcomposer');
check.details.blobSize = blob.size;
check.details.blobType = blob.type;
check.details.formDataCreated = true;
// Check filename validity
const filename = `composition_${Date.now()}_test.rdpcomposer`;
const invalidChars = /[<>:"/\\|?*]/.test(filename);
if (invalidChars) {
check.passed = false;
check.issues.push('Filename contains invalid characters');
}
} catch (error) {
check.passed = false;
check.issues.push(`FormData creation failed: ${error.message}`);
}
return check;
}
/**
* 7. Size vs Content Analysis
*/
analyzeSizeVsContent(composerJSON) {
const check = {
name: 'Size vs Content Analysis',
passed: true,
issues: [],
details: {}
};
const serialized = JSON.stringify(composerJSON);
const sizeKB = (serialized.length / 1024).toFixed(2);
const widgetCount = composerJSON.structure?.length || 0;
check.details.totalSize = serialized.length;
check.details.sizeKB = sizeKB;
check.details.widgetCount = widgetCount;
check.details.averageWidgetSize = widgetCount > 0 ? Math.round(serialized.length / widgetCount) : 0;
// Analyze size distribution
const widgets = composerJSON.structure || [];
const widgetSizes = widgets.map((widget, index) => ({
index,
type: widget.type,
size: JSON.stringify(widget).length
}));
widgetSizes.sort((a, b) => b.size - a.size);
check.details.largestWidgets = widgetSizes.slice(0, 3);
// Check for unusually large widgets
const maxReasonableSize = 5000; // 5KB per widget
const largeWidgets = widgetSizes.filter(w => w.size > maxReasonableSize);
if (largeWidgets.length > 0) {
check.issues.push(`${largeWidgets.length} widgets exceed reasonable size (>5KB)`);
check.details.largeWidgets = largeWidgets;
}
return check;
}
/**
* Generate actionable recommendations
*/
generateRecommendations(checks) {
const recommendations = [];
// JSON Structure issues
if (!checks.jsonStructure.passed) {
recommendations.push({
priority: 'HIGH',
category: 'JSON Structure',
action: 'Fix JSON serialization issues before API call',
details: checks.jsonStructure.issues
});
}
// Header issues (your past problem)
if (!checks.headerCompleteness.passed) {
recommendations.push({
priority: 'HIGH',
category: 'Header Completeness',
action: 'Ensure all required header fields are present',
details: checks.headerCompleteness.issues
});
}
// Content encoding issues
if (!checks.contentEncoding.passed) {
recommendations.push({
priority: 'MEDIUM',
category: 'Content Encoding',
action: 'Clean content encoding before API submission',
details: checks.contentEncoding.issues
});
}
// API response analysis
if (checks.apiResponse.details.httpStatus === 500) {
recommendations.push({
priority: 'HIGH',
category: 'Server Error',
action: 'Server processing error - check content validity rather than size',
details: ['Focus on malformed content, not payload size']
});
}
// Authentication issues
if (!checks.authentication.passed) {
recommendations.push({
priority: 'HIGH',
category: 'Authentication',
action: 'Resolve authentication issues before retry',
details: checks.authentication.issues
});
}
return recommendations;
}
}
export function createAPISaveDebugger() {
return new APISaveDebugger();
}