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' }) } // Forward to target endpoint using https/http native modules // Support redirects (301, 302, 307, 308) const currentUrl = new URL(targetEndpoint) const MAX_REDIRECTS = 5 let response: { statusCode: number; statusMessage: string; body: string } try { response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => { function makeRequest(url: URL, redirectCount: number, fileField: FormidableFile): void { if (redirectCount > MAX_REDIRECTS) { reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`)) return } // Recreate FormData for each request (needed for redirects) const requestFormData = new FormData() const fileStream = fs.createReadStream(fileField.filepath) requestFormData.append('file', fileStream, { filename: fileField.originalFilename || fileField.newFilename || 'upload', contentType: fileField.mimetype || 'application/octet-stream', }) const isHttps = url.protocol === 'https:' const clientModule = isHttps ? https : http const headers = requestFormData.getHeaders() const requestOptions = { hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname + url.search, method: 'POST', headers: headers, timeout: 30000, // 30 seconds timeout } const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { // Handle redirects (301, 302, 307, 308) const statusCode = proxyResponse.statusCode || 500 if ((statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) && proxyResponse.headers.location) { const location = proxyResponse.headers.location let redirectUrl: URL try { // Handle relative and absolute URLs redirectUrl = new URL(location, url.toString()) console.log('NIP-95 proxy redirect:', { from: url.toString(), to: redirectUrl.toString(), statusCode, redirectCount: redirectCount + 1, }) // Drain the response before redirecting proxyResponse.resume() // Make new request to redirect location makeRequest(redirectUrl, redirectCount + 1, fileField) return } catch (urlError) { console.error('NIP-95 proxy invalid redirect URL:', { location, error: urlError instanceof Error ? urlError.message : 'Unknown error', }) reject(new Error(`Invalid redirect URL: ${location}`)) return } } let body = '' proxyResponse.setEncoding('utf8') proxyResponse.on('data', (chunk) => { body += chunk }) proxyResponse.on('end', () => { resolve({ statusCode: statusCode, statusMessage: proxyResponse.statusMessage || 'Internal Server Error', body: body, }) }) proxyResponse.on('error', (error) => { reject(error) }) }) // Set timeout on the request proxyRequest.setTimeout(30000, () => { proxyRequest.destroy() reject(new Error('Request timeout after 30 seconds')) }) proxyRequest.on('error', (error) => { // Check for DNS errors specifically const errorCode = (error as NodeJS.ErrnoException).code if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') { console.error('NIP-95 proxy DNS error:', { targetEndpoint, hostname: url.hostname, errorCode, errorMessage: error.message, suggestion: 'Check DNS resolution or network connectivity on the server', }) reject(new Error(`DNS resolution failed for ${url.hostname}: ${error.message}`)) } else { reject(error) } }) requestFormData.on('error', (error) => { reject(error) }) requestFormData.pipe(proxyRequest) } makeRequest(currentUrl, 0, fileField) }) } 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' const isDnsError = errorMessage.includes('DNS resolution failed') || errorMessage.includes('ENOTFOUND') || errorMessage.includes('EAI_AGAIN') console.error('NIP-95 proxy request error:', { targetEndpoint, hostname: currentUrl.hostname, error: errorMessage, isDnsError, fileSize: fileField.size, fileName: fileField.originalFilename, suggestion: isDnsError ? 'The server cannot resolve the domain name. Check DNS configuration and network connectivity.' : undefined, }) // Return a more specific error message for DNS issues if (isDnsError) { return res.status(500).json({ error: `DNS resolution failed for ${currentUrl.hostname}. The server cannot resolve the domain name. Please check DNS configuration and network connectivity.`, }) } 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, finalUrl: currentUrl.toString(), status: response.statusCode, statusText: response.statusMessage, errorText: errorText, }) // Provide more specific error messages for common HTTP status codes let userFriendlyError = errorText || `Upload failed: ${response.statusCode} ${response.statusMessage}` if (response.statusCode === 401) { userFriendlyError = 'Authentication required. This endpoint requires authorization headers.' } else if (response.statusCode === 403) { userFriendlyError = 'Access forbidden. This endpoint may require authentication or have restrictions.' } else if (response.statusCode === 405) { userFriendlyError = 'Method not allowed. This endpoint may not support POST requests or the URL may be incorrect.' } else if (response.statusCode === 413) { userFriendlyError = 'File too large. The file exceeds the maximum size allowed by this endpoint.' } else if (response.statusCode >= 500) { userFriendlyError = `Server error (${response.statusCode}). The endpoint server encountered an error.` } return res.status(response.statusCode).json({ error: userFriendlyError, }) } 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', }) } }