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 files: Record } 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((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, files: files as Record }) } }) }) 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', }) } }