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,78 +55,118 @@ 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 {
const requestOptions = { if (redirectCount > MAX_REDIRECTS) {
hostname: targetUrl.hostname, reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`))
port: targetUrl.port || (isHttps ? 443 : 80), return
path: targetUrl.pathname + targetUrl.search, }
method: 'POST',
headers: headers, // Recreate FormData for each request (needed for redirects)
timeout: 30000, // 30 seconds timeout 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)
} }
const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { makeRequest(currentUrl, 0, fileField)
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)
})
})
// 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: targetUrl.hostname,
errorCode,
errorMessage: error.message,
suggestion: 'Check DNS resolution or network connectivity on the server',
})
reject(new Error(`DNS resolution failed for ${targetUrl.hostname}: ${error.message}`))
} else {
reject(error)
}
})
formData.on('error', (error) => {
reject(error)
})
formData.pipe(proxyRequest)
}) })
} 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,
}) })
} }