Fix: NIP-95 upload 500 error

This commit is contained in:
Nicolas Cantu 2026-01-05 21:56:09 +01:00
parent a90b77cec3
commit d8311078bc
2 changed files with 127 additions and 92 deletions

View File

@ -40,38 +40,17 @@ export const DEFAULT_RELAYS: RelayConfig[] = [
export const DEFAULT_NIP95_APIS: Nip95Config[] = [ export const DEFAULT_NIP95_APIS: Nip95Config[] = [
{ {
id: 'voidcat', id: 'nostrimg',
url: 'https://void.cat/upload', url: 'https://nostrimg.com/api/upload',
enabled: true, enabled: true,
priority: 1, priority: 1,
createdAt: Date.now(), createdAt: Date.now(),
}, },
{
id: 'nostrbuild',
url: 'https://nostr.build/api/v2/upload',
enabled: true,
priority: 2,
createdAt: Date.now(),
},
{
id: 'picstr',
url: 'https://picstr.build/api/v1/upload',
enabled: true,
priority: 3,
createdAt: Date.now(),
},
{ {
id: 'nostrcheck', id: 'nostrcheck',
url: 'https://nostrcheck.me/api/v1/media', url: 'https://nostrcheck.me/api/v1/media',
enabled: true, enabled: true,
priority: 4, priority: 2,
createdAt: Date.now(),
},
{
id: 'nostrimg',
url: 'https://nostrimg.com/api/upload',
enabled: true,
priority: 5,
createdAt: Date.now(), createdAt: Date.now(),
}, },
] ]

View File

@ -55,33 +55,70 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ error: 'No file provided' }) 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 // Forward to target endpoint using https/http native modules
const targetUrl = new URL(targetEndpoint) // Support redirects (301, 302, 307, 308)
const isHttps = targetUrl.protocol === 'https:' const currentUrl = new URL(targetEndpoint)
const clientModule = isHttps ? https : http const MAX_REDIRECTS = 5
let response: { statusCode: number; statusMessage: string; body: string } let response: { statusCode: number; statusMessage: string; body: string }
try { try {
response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => { response = await new Promise<{ statusCode: number; statusMessage: string; body: string }>((resolve, reject) => {
const headers = formData.getHeaders() 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 = { const requestOptions = {
hostname: targetUrl.hostname, hostname: url.hostname,
port: targetUrl.port || (isHttps ? 443 : 80), port: url.port || (isHttps ? 443 : 80),
path: targetUrl.pathname + targetUrl.search, path: url.pathname + url.search,
method: 'POST', method: 'POST',
headers: headers, headers: headers,
timeout: 30000, // 30 seconds timeout timeout: 30000, // 30 seconds timeout
} }
const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { 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 = '' let body = ''
proxyResponse.setEncoding('utf8') proxyResponse.setEncoding('utf8')
proxyResponse.on('data', (chunk) => { proxyResponse.on('data', (chunk) => {
@ -89,7 +126,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}) })
proxyResponse.on('end', () => { proxyResponse.on('end', () => {
resolve({ resolve({
statusCode: proxyResponse.statusCode || 500, statusCode: statusCode,
statusMessage: proxyResponse.statusMessage || 'Internal Server Error', statusMessage: proxyResponse.statusMessage || 'Internal Server Error',
body: body, body: body,
}) })
@ -111,22 +148,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') { if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') {
console.error('NIP-95 proxy DNS error:', { console.error('NIP-95 proxy DNS error:', {
targetEndpoint, targetEndpoint,
hostname: targetUrl.hostname, hostname: url.hostname,
errorCode, errorCode,
errorMessage: error.message, errorMessage: error.message,
suggestion: 'Check DNS resolution or network connectivity on the server', suggestion: 'Check DNS resolution or network connectivity on the server',
}) })
reject(new Error(`DNS resolution failed for ${targetUrl.hostname}: ${error.message}`)) reject(new Error(`DNS resolution failed for ${url.hostname}: ${error.message}`))
} else { } else {
reject(error) reject(error)
} }
}) })
formData.on('error', (error) => { requestFormData.on('error', (error) => {
reject(error) reject(error)
}) })
formData.pipe(proxyRequest) requestFormData.pipe(proxyRequest)
}
makeRequest(currentUrl, 0, fileField)
}) })
} catch (requestError) { } catch (requestError) {
// Clean up temporary file before returning error // Clean up temporary file before returning error
@ -140,7 +180,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
console.error('NIP-95 proxy request error:', { console.error('NIP-95 proxy request error:', {
targetEndpoint, targetEndpoint,
hostname: targetUrl.hostname, hostname: currentUrl.hostname,
error: errorMessage, error: errorMessage,
isDnsError, isDnsError,
fileSize: fileField.size, fileSize: fileField.size,
@ -151,7 +191,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Return a more specific error message for DNS issues // Return a more specific error message for DNS issues
if (isDnsError) { if (isDnsError) {
return res.status(500).json({ return res.status(500).json({
error: `DNS resolution failed for ${targetUrl.hostname}. The server cannot resolve the domain name. Please check DNS configuration and network connectivity.`, error: `DNS resolution failed for ${currentUrl.hostname}. The server cannot resolve the domain name. Please check DNS configuration and network connectivity.`,
}) })
} }
@ -171,12 +211,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const errorText = response.body.substring(0, 200) // Limit log size const errorText = response.body.substring(0, 200) // Limit log size
console.error('NIP-95 proxy response error:', { console.error('NIP-95 proxy response error:', {
targetEndpoint, targetEndpoint,
finalUrl: currentUrl.toString(),
status: response.statusCode, status: response.statusCode,
statusText: response.statusMessage, statusText: response.statusMessage,
errorText: errorText, 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({ return res.status(response.statusCode).json({
error: errorText || `Upload failed: ${response.statusCode} ${response.statusMessage}`, error: userFriendlyError,
}) })
} }