story-research-zapwall/pages/api/nip95-upload.ts
Nicolas Cantu 065ab30828 Fix: favicon 404 error and NIP-95 upload 500 error
**Motivations:**
- Corriger l'erreur 404 pour favicon.ico demandé par les navigateurs
- Corriger l'erreur 500 de l'API NIP-95 upload empêchant les uploads de fichiers

**Root causes:**
- Fichier favicon.ico manquant dans public/ causant des erreurs 404 répétées
- Incompatibilité entre form-data (npm) et fetch() natif de Node.js dans l'API NIP-95

**Correctifs:**
- Ajout de favicon.svg et mise à jour des références dans les pages
- Remplacement de fetch() par https/http natifs de Node.js dans nip95-upload.ts
- Amélioration de la gestion des erreurs et nettoyage des fichiers temporaires

**Evolutions:**
- Documentation des problèmes et solutions dans fixKnowledge/

**Pages affectées:**
- components/HomeView.tsx
- pages/docs.tsx
- pages/presentation.tsx
- pages/api/nip95-upload.ts
- features/account-creation-buttons-separation.md
- fixKnowledge/favicon-404-error.md (nouveau)
- fixKnowledge/nip95-upload-500-error.md (nouveau)
- public/favicon.svg (nouveau)
2026-01-05 01:34:55 +01:00

173 lines
5.6 KiB
TypeScript

import { NextApiRequest, NextApiResponse } from 'next'
import { IncomingForm, File as FormidableFile } from 'formidable'
import FormData from 'form-data'
import fs from 'fs'
import https from 'https'
import http from 'http'
import { URL } from 'url'
const MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
export const config = {
api: {
bodyParser: false, // Disable bodyParser to handle multipart
},
}
interface ParseResult {
fields: Record<string, string[]>
files: Record<string, FormidableFile[]>
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
// Get target endpoint from query or use default
const targetEndpoint = (req.query.endpoint as string) || 'https://void.cat/upload'
try {
// Parse multipart form data
// formidable needs the raw Node.js IncomingMessage, which NextApiRequest extends
const form = new IncomingForm({
maxFileSize: MAX_FILE_SIZE,
keepExtensions: true,
})
const parseResult = await new Promise<ParseResult>((resolve, reject) => {
// Cast req to any to work with formidable - NextApiRequest extends IncomingMessage
form.parse(req as any, (err, fields, files) => {
if (err) {
console.error('Formidable parse error:', err)
reject(err)
} else {
resolve({ fields: fields as Record<string, string[]>, files: files as Record<string, FormidableFile[]> })
}
})
})
const { files } = parseResult
// Get the file from the parsed form
const fileField = files.file?.[0]
if (!fileField) {
return res.status(400).json({ error: 'No file provided' })
}
// Create FormData for the target endpoint
const formData = new FormData()
const fileStream = fs.createReadStream(fileField.filepath)
formData.append('file', fileStream, {
filename: fileField.originalFilename || fileField.newFilename || 'upload',
contentType: fileField.mimetype || 'application/octet-stream',
})
// Forward to target endpoint using https/http native modules
const targetUrl = new URL(targetEndpoint)
const isHttps = targetUrl.protocol === 'https:'
const clientModule = isHttps ? https : http
let response: { statusCode: number; statusMessage: string; body: string }
try {
response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => {
const headers = formData.getHeaders()
const requestOptions = {
hostname: targetUrl.hostname,
port: targetUrl.port || (isHttps ? 443 : 80),
path: targetUrl.pathname + targetUrl.search,
method: 'POST',
headers: headers,
}
const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => {
let body = ''
proxyResponse.setEncoding('utf8')
proxyResponse.on('data', (chunk) => {
body += chunk
})
proxyResponse.on('end', () => {
resolve({
statusCode: proxyResponse.statusCode || 500,
statusMessage: proxyResponse.statusMessage || 'Internal Server Error',
body: body,
})
})
proxyResponse.on('error', (error) => {
reject(error)
})
})
proxyRequest.on('error', (error) => {
reject(error)
})
formData.on('error', (error) => {
reject(error)
})
formData.pipe(proxyRequest)
})
} catch (requestError) {
// Clean up temporary file before returning error
try {
fs.unlinkSync(fileField.filepath)
} catch (unlinkError) {
console.error('Error deleting temp file:', unlinkError)
}
const errorMessage = requestError instanceof Error ? requestError.message : 'Unknown request error'
console.error('NIP-95 proxy request error:', {
targetEndpoint,
error: errorMessage,
fileSize: fileField.size,
fileName: fileField.originalFilename,
})
return res.status(500).json({
error: `Failed to connect to upload endpoint: ${errorMessage}`,
})
}
// Clean up temporary file
try {
fs.unlinkSync(fileField.filepath)
} catch (unlinkError) {
console.error('Error deleting temp file:', unlinkError)
}
if (response.statusCode < 200 || response.statusCode >= 300) {
const errorText = response.body.substring(0, 200) // Limit log size
console.error('NIP-95 proxy response error:', {
targetEndpoint,
status: response.statusCode,
statusText: response.statusMessage,
errorText: errorText,
})
return res.status(response.statusCode).json({
error: errorText || `Upload failed: ${response.statusCode} ${response.statusMessage}`,
})
}
let result: unknown
try {
result = JSON.parse(response.body)
} catch (parseError) {
const errorMessage = parseError instanceof Error ? parseError.message : 'Invalid JSON response'
console.error('NIP-95 proxy JSON parse error:', {
targetEndpoint,
error: errorMessage,
bodyPreview: response.body.substring(0, 100),
})
return res.status(500).json({
error: `Invalid upload response: ${errorMessage}`,
})
}
return res.status(200).json(result)
} catch (error) {
console.error('NIP-95 proxy error:', error)
return res.status(500).json({
error: error instanceof Error ? error.message : 'Internal server error',
})
}
}