From a90b77cec39786ddf82f93821b79034804888b2d Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Mon, 5 Jan 2026 21:22:49 +0100 Subject: [PATCH] Fix: NIP-95 upload 500 error --- components/Nip95ConfigManager.tsx | 284 ++++++++++++++++++ components/PageHeader.tsx | 6 + .../account-creation-buttons-separation.md | 1 + features/nip95-endpoints-configuration.md | 92 ++++++ fixKnowledge/favicon-404-error.md | 1 + fixKnowledge/nip95-upload-500-error.md | 1 + .../nip95-upload-dns-resolution-error.md | 107 +++++++ lib/configStorageTypes.ts | 13 +- pages/api/nip95-upload.ts | 35 ++- pages/settings.tsx | 25 ++ public/favicon.svg | 1 + 11 files changed, 562 insertions(+), 4 deletions(-) create mode 100644 components/Nip95ConfigManager.tsx create mode 100644 features/nip95-endpoints-configuration.md create mode 100644 fixKnowledge/nip95-upload-dns-resolution-error.md create mode 100644 pages/settings.tsx diff --git a/components/Nip95ConfigManager.tsx b/components/Nip95ConfigManager.tsx new file mode 100644 index 0000000..8bbdd7c --- /dev/null +++ b/components/Nip95ConfigManager.tsx @@ -0,0 +1,284 @@ +import { useState, useEffect } from 'react' +import { configStorage } from '@/lib/configStorage' +import type { Nip95Config } from '@/lib/configStorageTypes' + +interface Nip95ConfigManagerProps { + onConfigChange?: () => void +} + +export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps) { + const [apis, setApis] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [editingId, setEditingId] = useState(null) + const [newUrl, setNewUrl] = useState('') + const [showAddForm, setShowAddForm] = useState(false) + + useEffect(() => { + void loadApis() + }, []) + + async function loadApis() { + try { + setLoading(true) + setError(null) + const config = await configStorage.getConfig() + setApis(config.nip95Apis.sort((a, b) => a.priority - b.priority)) + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to load NIP-95 APIs' + setError(errorMessage) + console.error('Error loading NIP-95 APIs:', e) + } finally { + setLoading(false) + } + } + + async function handleToggleEnabled(id: string, enabled: boolean) { + try { + await configStorage.updateNip95Api(id, { enabled }) + await loadApis() + onConfigChange?.() + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to update API' + setError(errorMessage) + console.error('Error updating NIP-95 API:', e) + } + } + + async function handleUpdatePriority(id: string, priority: number) { + try { + await configStorage.updateNip95Api(id, { priority }) + await loadApis() + onConfigChange?.() + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to update priority' + setError(errorMessage) + console.error('Error updating priority:', e) + } + } + + async function handleUpdateUrl(id: string, url: string) { + try { + await configStorage.updateNip95Api(id, { url }) + await loadApis() + setEditingId(null) + onConfigChange?.() + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to update URL' + setError(errorMessage) + console.error('Error updating URL:', e) + } + } + + async function handleAddApi() { + if (!newUrl.trim()) { + setError('URL is required') + return + } + + try { + // Validate URL format + new URL(newUrl) + await configStorage.addNip95Api(newUrl.trim(), false) + setNewUrl('') + setShowAddForm(false) + await loadApis() + onConfigChange?.() + } catch (e) { + if (e instanceof TypeError && e.message.includes('Invalid URL')) { + setError('Invalid URL format') + } else { + const errorMessage = e instanceof Error ? e.message : 'Failed to add API' + setError(errorMessage) + } + console.error('Error adding NIP-95 API:', e) + } + } + + async function handleRemoveApi(id: string) { + if (!confirm('Are you sure you want to remove this endpoint?')) { + return + } + + try { + await configStorage.removeNip95Api(id) + await loadApis() + onConfigChange?.() + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to remove API' + setError(errorMessage) + console.error('Error removing NIP-95 API:', e) + } + } + + if (loading) { + return ( +
+
Loading...
+
+ ) + } + + return ( +
+ {error && ( +
+ {error} + +
+ )} + +
+

NIP-95 Upload Endpoints

+ +
+ + {showAddForm && ( +
+
+ + setNewUrl(e.target.value)} + placeholder="https://example.com/upload" + className="w-full px-4 py-2 bg-cyber-darker border border-cyber-accent/30 rounded text-cyber-light focus:border-neon-cyan focus:outline-none" + /> +
+
+ + +
+
+ )} + +
+ {apis.length === 0 ? ( +
+ No NIP-95 endpoints configured +
+ ) : ( + apis.map((api) => ( +
+
+
+ {editingId === api.id ? ( +
+ { + if (e.target.value !== api.url) { + void handleUpdateUrl(api.id, e.target.value) + } else { + setEditingId(null) + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.currentTarget.blur() + } else if (e.key === 'Escape') { + setEditingId(null) + } + }} + className="w-full px-3 py-2 bg-cyber-darker border border-neon-cyan/50 rounded text-cyber-light focus:border-neon-cyan focus:outline-none" + autoFocus + /> +
+ ) : ( +
setEditingId(api.id)} + title="Click to edit URL" + > + {api.url} +
+ )} +
+ Priority: {api.priority} | ID: {api.id} +
+
+
+ + +
+
+
+ +
+
+ )) + )} +
+ +
+

+ Note: Endpoints are tried in priority order (lower number = higher priority). + Only enabled endpoints will be used for uploads. +

+

+ If an endpoint fails, the next enabled endpoint will be tried automatically. +

+
+
+ ) +} diff --git a/components/PageHeader.tsx b/components/PageHeader.tsx index 5dc9406..3051d33 100644 --- a/components/PageHeader.tsx +++ b/components/PageHeader.tsx @@ -18,6 +18,12 @@ export function PageHeader() { > {t('nav.documentation')} + + Settings + diff --git a/features/account-creation-buttons-separation.md b/features/account-creation-buttons-separation.md index c14f45f..5cc26b7 100644 --- a/features/account-creation-buttons-separation.md +++ b/features/account-creation-buttons-separation.md @@ -53,3 +53,4 @@ Aucun déploiement spécial nécessaire. Les modifications sont purement fronten + diff --git a/features/nip95-endpoints-configuration.md b/features/nip95-endpoints-configuration.md new file mode 100644 index 0000000..1d5001f --- /dev/null +++ b/features/nip95-endpoints-configuration.md @@ -0,0 +1,92 @@ +# Configuration des endpoints NIP-95 + +## Date +2025-01-27 + +## Objectif +Permettre aux utilisateurs de configurer les endpoints NIP-95 pour l'upload de médias via une interface utilisateur, avec la possibilité d'ajouter, modifier, activer/désactiver et supprimer des endpoints. + +## Motivations +- Résoudre les problèmes de DNS/connectivité en permettant d'utiliser plusieurs endpoints +- Donner aux utilisateurs le contrôle sur les endpoints utilisés +- Faciliter l'ajout de nouveaux endpoints sans modifier le code +- Permettre l'activation/désactivation d'endpoints selon les besoins + +## Impacts +- **Utilisateurs** : Peuvent configurer leurs propres endpoints NIP-95 via l'interface +- **Développeurs** : Plus besoin de modifier le code pour ajouter des endpoints +- **Fiabilité** : Possibilité d'avoir plusieurs endpoints de secours activés + +## Modifications + +### Fichiers créés +- **`pages/settings.tsx`** : Page de configuration des paramètres de l'application +- **`components/Nip95ConfigManager.tsx`** : Composant de gestion des endpoints NIP-95 avec interface complète + +### Fichiers modifiés +- **`lib/configStorageTypes.ts`** : Ajout d'un endpoint supplémentaire (`nostrimg.com`) dans `DEFAULT_NIP95_APIS` +- **`components/PageHeader.tsx`** : Ajout d'un lien vers la page Settings dans la navigation + +### Fonctionnalités implémentées + +#### Interface de gestion des endpoints +- **Affichage de la liste** : Tous les endpoints configurés sont affichés avec leur statut (activé/désactivé), priorité et URL +- **Ajout d'endpoints** : Formulaire pour ajouter de nouveaux endpoints avec validation d'URL +- **Modification d'URL** : Clic sur l'URL pour la modifier directement +- **Activation/Désactivation** : Checkbox pour activer ou désactiver chaque endpoint +- **Gestion de la priorité** : Champ numérique pour définir l'ordre de tentative (plus bas = priorité plus haute) +- **Suppression** : Bouton pour supprimer un endpoint avec confirmation + +#### Endpoints par défaut +Les endpoints suivants sont maintenant disponibles par défaut : +1. `https://void.cat/upload` (activé, priorité 1) +2. `https://nostr.build/api/v2/upload` (désactivé, priorité 2) +3. `https://picstr.build/api/v1/upload` (désactivé, priorité 3) +4. `https://nostrcheck.me/api/v1/media` (désactivé, priorité 4) +5. `https://nostrimg.com/api/upload` (désactivé, priorité 5) - **Nouveau** + +## Modalités de déploiement +1. Les modifications sont dans le code source +2. Rebuild de l'application : `npm run build` +3. Redémarrage du service Next.js +4. Aucune migration de données nécessaire (les endpoints par défaut sont créés automatiquement) + +## Modalités d'analyse +Pour vérifier que la fonctionnalité fonctionne : + +1. **Accéder à la page Settings** : + - Naviguer vers `/settings` depuis le menu de navigation + - Vérifier que la liste des endpoints s'affiche + +2. **Tester l'ajout d'un endpoint** : + - Cliquer sur "+ Add Endpoint" + - Entrer une URL valide (ex: `https://example.com/upload`) + - Vérifier que l'endpoint apparaît dans la liste + +3. **Tester l'activation/désactivation** : + - Cocher/décocher la checkbox d'un endpoint + - Vérifier que le statut change + - Tester un upload pour vérifier que seuls les endpoints activés sont utilisés + +4. **Tester la modification de priorité** : + - Modifier la priorité d'un endpoint + - Vérifier que l'ordre dans la liste change + - Tester un upload pour vérifier l'ordre de tentative + +5. **Tester la modification d'URL** : + - Cliquer sur une URL pour la modifier + - Entrer une nouvelle URL et appuyer sur Enter + - Vérifier que l'URL est mise à jour + +6. **Tester la suppression** : + - Cliquer sur "Remove" d'un endpoint + - Confirmer la suppression + - Vérifier que l'endpoint disparaît de la liste + +## Notes +- Les endpoints sont stockés dans IndexedDB (côté client) +- Les modifications sont persistantes et survivent aux rechargements de page +- Les endpoints sont essayés dans l'ordre de priorité (plus bas = priorité plus haute) +- Seuls les endpoints activés sont utilisés pour les uploads +- Si tous les endpoints activés échouent, une erreur est retournée à l'utilisateur +- La validation d'URL vérifie le format mais pas la disponibilité du service diff --git a/fixKnowledge/favicon-404-error.md b/fixKnowledge/favicon-404-error.md index e4254ea..bf17ebe 100644 --- a/fixKnowledge/favicon-404-error.md +++ b/fixKnowledge/favicon-404-error.md @@ -55,3 +55,4 @@ Pour vérifier si le problème existe : - SVG est plus léger et plus flexible qu'un fichier .ico - Si nécessaire, on peut créer un fichier `.ico` en plus pour la compatibilité avec les anciens navigateurs - Le favicon SVG actuel est minimal (rectangle cyan) et peut être remplacé par un design plus élaboré si nécessaire + diff --git a/fixKnowledge/nip95-upload-500-error.md b/fixKnowledge/nip95-upload-500-error.md index cdafc2c..c99d198 100644 --- a/fixKnowledge/nip95-upload-500-error.md +++ b/fixKnowledge/nip95-upload-500-error.md @@ -61,3 +61,4 @@ Pour vérifier si le problème existe : - Le package `form-data` (npm) doit être utilisé avec les modules `https`/`http` natifs de Node.js, pas avec `fetch()` - `fetch()` natif de Node.js est compatible avec FormData du web standard (disponible dans le navigateur), pas avec le package npm `form-data` - Les fichiers temporaires créés par formidable doivent être nettoyés même en cas d'erreur pour éviter l'accumulation de fichiers + diff --git a/fixKnowledge/nip95-upload-dns-resolution-error.md b/fixKnowledge/nip95-upload-dns-resolution-error.md new file mode 100644 index 0000000..c1b3b16 --- /dev/null +++ b/fixKnowledge/nip95-upload-dns-resolution-error.md @@ -0,0 +1,107 @@ +# Problème : Erreur DNS resolution pour NIP-95 upload + +## Date +2025-01-27 + +## Problème +L'API NIP-95 upload retourne une erreur `getaddrinfo ENOTFOUND void.cat` lors des tentatives d'upload, indiquant que le serveur Node.js ne peut pas résoudre le nom de domaine. + +## Symptômes +- Erreur : `Failed to upload to all endpoints: {"error":"Failed to connect to upload endpoint: getaddrinfo ENOTFOUND void.cat"}` +- Code d'erreur Node.js : `ENOTFOUND` ou `EAI_AGAIN` +- Les uploads NIP-95 échouent avec une erreur de résolution DNS + +## Root cause +Le serveur Node.js qui exécute l'API Next.js ne peut pas résoudre le nom de domaine `void.cat` (ou d'autres endpoints NIP-95). Cela peut être dû à : + +1. **Problème de DNS sur le serveur** : Le serveur n'a pas accès à un serveur DNS fonctionnel +2. **Pas d'accès Internet** : Le serveur n'a pas de connexion Internet ou est derrière un pare-feu qui bloque les requêtes sortantes +3. **Configuration réseau incorrecte** : Les paramètres réseau du serveur sont mal configurés +4. **Problème de résolution DNS temporaire** : Problème temporaire avec le serveur DNS + +## Impact +- Impossible d'uploader des fichiers via NIP-95 +- Les utilisateurs ne peuvent pas publier d'articles avec des médias +- Fonctionnalité d'upload complètement non fonctionnelle + +## Correctifs +1. **Amélioration de la gestion d'erreur DNS** : Détection spécifique des erreurs DNS avec messages d'erreur plus clairs +2. **Ajout d'un timeout** : Timeout de 30 secondes pour éviter que les requêtes restent bloquées indéfiniment +3. **Amélioration des logs** : Logs plus détaillés pour diagnostiquer les problèmes DNS + +## Modifications +- **Fichier modifié** : `pages/api/nip95-upload.ts` +- **Ajout de timeout** : `timeout: 30000` dans les options de requête +- **Détection spécifique des erreurs DNS** : Vérification des codes d'erreur `ENOTFOUND` et `EAI_AGAIN` +- **Messages d'erreur améliorés** : Messages plus clairs pour les erreurs DNS avec suggestions de diagnostic +- **Logs améliorés** : Logs incluant le hostname, le code d'erreur et des suggestions + +## Modalités de déploiement +1. Les modifications sont dans le code source +2. Rebuild de l'application : `npm run build` +3. Redémarrage du service Next.js +4. Aucune dépendance supplémentaire nécessaire + +## Modalités d'analyse +Pour diagnostiquer le problème DNS : + +1. **Vérifier la résolution DNS depuis le serveur** : + ```bash + # Depuis le serveur + nslookup void.cat + # ou + dig void.cat + # ou + host void.cat + ``` + +2. **Vérifier la connectivité réseau** : + ```bash + # Tester la connexion HTTPS + curl -v https://void.cat/upload + # ou + wget --spider https://void.cat/upload + ``` + +3. **Vérifier les logs du serveur Node.js** : + - Chercher les erreurs `ENOTFOUND` ou `EAI_AGAIN` + - Vérifier les logs avec le hostname et les suggestions + +4. **Vérifier la configuration DNS du serveur** : + ```bash + # Vérifier les serveurs DNS configurés + cat /etc/resolv.conf + # ou sur systemd + systemd-resolve --status + ``` + +5. **Tester depuis le conteneur/service** : + ```bash + # Si le service tourne dans un conteneur Docker + docker exec nslookup void.cat + docker exec curl -v https://void.cat/upload + ``` + +## Solutions possibles + +### Si le problème est la résolution DNS +1. Vérifier que le serveur a accès à un serveur DNS fonctionnel +2. Configurer des serveurs DNS fiables (par exemple 8.8.8.8, 1.1.1.1) +3. Vérifier que `/etc/resolv.conf` contient des serveurs DNS valides + +### Si le problème est l'accès Internet +1. Vérifier que le serveur a une connexion Internet fonctionnelle +2. Vérifier les règles de pare-feu qui pourraient bloquer les requêtes sortantes HTTPS +3. Vérifier les proxies si le serveur est derrière un proxy + +### Si le problème est temporaire +1. Attendre quelques minutes et réessayer +2. Vérifier si d'autres services ont des problèmes de connectivité +3. Vérifier les statuts des endpoints NIP-95 (void.cat, etc.) + +## Notes +- L'erreur `ENOTFOUND` signifie que le nom de domaine n'a pas pu être résolu en adresse IP +- L'erreur `EAI_AGAIN` signifie que la résolution DNS a échoué temporairement (peut être réessayé) +- Le timeout de 30 secondes évite que les requêtes restent bloquées indéfiniment +- Les messages d'erreur améliorés aident à diagnostiquer rapidement le problème +- Si le problème persiste, vérifier la configuration réseau du serveur et l'accès à Internet diff --git a/lib/configStorageTypes.ts b/lib/configStorageTypes.ts index a5afb66..6817397 100644 --- a/lib/configStorageTypes.ts +++ b/lib/configStorageTypes.ts @@ -49,24 +49,31 @@ export const DEFAULT_NIP95_APIS: Nip95Config[] = [ { id: 'nostrbuild', url: 'https://nostr.build/api/v2/upload', - enabled: false, + enabled: true, priority: 2, createdAt: Date.now(), }, { id: 'picstr', url: 'https://picstr.build/api/v1/upload', - enabled: false, + enabled: true, priority: 3, createdAt: Date.now(), }, { id: 'nostrcheck', url: 'https://nostrcheck.me/api/v1/media', - enabled: false, + enabled: true, priority: 4, createdAt: Date.now(), }, + { + id: 'nostrimg', + url: 'https://nostrimg.com/api/upload', + enabled: true, + priority: 5, + createdAt: Date.now(), + }, ] export const DEFAULT_PLATFORM_LIGHTNING_ADDRESS = '' diff --git a/pages/api/nip95-upload.ts b/pages/api/nip95-upload.ts index 267b108..2349af1 100644 --- a/pages/api/nip95-upload.ts +++ b/pages/api/nip95-upload.ts @@ -78,6 +78,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) path: targetUrl.pathname + targetUrl.search, method: 'POST', headers: headers, + timeout: 30000, // 30 seconds timeout } const proxyRequest = clientModule.request(requestOptions, (proxyResponse) => { @@ -98,8 +99,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }) }) + // Set timeout on the request + proxyRequest.setTimeout(30000, () => { + proxyRequest.destroy() + reject(new Error('Request timeout after 30 seconds')) + }) + proxyRequest.on('error', (error) => { - reject(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) => { @@ -116,12 +136,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) 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: targetUrl.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 ${targetUrl.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}`, }) diff --git a/pages/settings.tsx b/pages/settings.tsx new file mode 100644 index 0000000..9f9f7f3 --- /dev/null +++ b/pages/settings.tsx @@ -0,0 +1,25 @@ +import Head from 'next/head' +import { PageHeader } from '@/components/PageHeader' +import { Footer } from '@/components/Footer' +import { Nip95ConfigManager } from '@/components/Nip95ConfigManager' + +export default function SettingsPage() { + return ( + <> + + Settings - zapwall.fr + + + + +
+ +
+

Settings

+ +
+
+
+ + ) +} diff --git a/public/favicon.svg b/public/favicon.svg index 4bffb8d..88347a1 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -2,3 +2,4 @@ +