**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)
173 lines
5.6 KiB
TypeScript
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',
|
|
})
|
|
}
|
|
}
|