+server.ts
1 import { json } from '@sveltejs/kit'; 2 import { yacyConfig, timeouts } from '$lib/config'; 3 import type { RequestHandler } from './$types'; 4 5 /** 6 * Proxy de API para YaCy 7 * Conecta directamente con la API JSON de YaCy y maneja errores de forma robusta 8 * 9 * Permite: 10 * 1. Conectar con YaCy sin problemas de CORS 11 * 2. Centralizar manejo de errores y timeouts 12 * 3. Proporcionar respuestas consistentes incluso con errores 13 * 4. Realizar verificaciones de disponibilidad 14 */ 15 export const GET: RequestHandler = async ({ url, fetch }) => { 16 try { 17 // Extraer parámetros de la consulta 18 const params = new URLSearchParams(url.search); 19 const query = params.get('query') || ''; 20 21 // Log detallado para depuración 22 console.log('[YaCy Proxy] Parámetros recibidos:', Object.fromEntries(params.entries())); 23 console.log('[YaCy Proxy] URL original:', url.toString()); 24 25 // Si no hay consulta, el cliente está verificando la disponibilidad de YaCy 26 if (!query) { 27 try { 28 return await checkYacyStatus(fetch); 29 } catch (e: any) { 30 console.error('[YaCy Proxy] Error al verificar YaCy:', e); 31 return json({ 32 status: 'error', 33 message: 'YaCy no está disponible', 34 error: e.message 35 }, { status: 503 }); // Service Unavailable 36 } 37 } 38 39 // Intentar conectar con YaCy real 40 try { 41 // Construir parámetros para YaCy 42 const yacyParams = buildYacyParams(params); 43 44 // URL completa para YaCy 45 const yacyUrl = `${yacyConfig.baseUrl}/yacysearch.json?${yacyParams.toString()}`; 46 47 console.log('[YaCy Proxy] Enviando solicitud a:', yacyUrl); 48 console.log('[YaCy Proxy] Parámetros convertidos para YaCy:', Object.fromEntries(yacyParams.entries())); 49 50 // Intentar obtener la respuesta con manejo de timeout 51 const controller = new AbortController(); 52 const timeoutId = setTimeout(() => controller.abort(), yacyConfig.timeout); 53 54 const response = await fetch(yacyUrl, { 55 signal: controller.signal, 56 headers: { 57 'Accept': 'application/json' 58 } 59 }); 60 61 clearTimeout(timeoutId); 62 63 // Verificar si la respuesta es exitosa 64 if (!response.ok) { 65 console.error(`[YaCy Proxy] Error HTTP de YaCy: ${response.status} ${response.statusText}`); 66 67 // Mensaje detallado para depuración 68 console.log(`[YaCy Proxy] URL completa: ${yacyUrl}`); 69 console.log(`[YaCy Proxy] Headers:`, response.headers); 70 71 // Si es error 404, significa que no hubo resultados 72 if (response.status === 404) { 73 console.log('[YaCy Proxy] No se encontraron resultados (404)'); 74 return json({ 75 channels: [{ 76 title: 'YaCy Search', 77 description: 'No results found', 78 totalResults: '0', 79 startIndex: '0', 80 itemsPerPage: '0', 81 items: [] 82 }] 83 }); 84 } else { 85 // Otro tipo de error 86 console.log('[YaCy Proxy] Error diferente a 404'); 87 return json({ 88 error: `Error ${response.status}: ${response.statusText}` 89 }, { status: 200 }); // 200 para que el cliente maneje el error 90 } 91 } 92 93 // Procesar respuesta exitosa 94 let data; 95 try { 96 // Primero obtener el texto crudo para debugging 97 const responseText = await response.text(); 98 console.log('[YaCy Proxy] Respuesta cruda:', responseText.substring(0, 500) + '...'); 99 100 try { 101 // Intentar parsear como JSON 102 if (responseText.trim().length === 0) { 103 console.warn('[YaCy Proxy] Respuesta vacía de YaCy'); 104 return json({ 105 error: 'YaCy devolvió una respuesta vacía', 106 channels: [{ 107 title: 'Error', 108 description: 'Respuesta vacía', 109 totalResults: '0', 110 startIndex: '0', 111 itemsPerPage: '0', 112 items: [] 113 }] 114 }); 115 } else { 116 // Intentar parsear la respuesta real 117 data = JSON.parse(responseText); 118 } 119 120 // Log de respuesta estructurada 121 console.log('[YaCy Proxy] Estructura de la respuesta:', 122 JSON.stringify({ 123 channels: data.channels ? data.channels.map(c => ({ 124 title: c.title, 125 totalResults: c.totalResults, 126 itemsPerPage: c.itemsPerPage, 127 startIndex: c.startIndex, 128 items: c.items ? `${c.items.length} items` : 'sin items' 129 })) : 'sin canales' 130 }, null, 2) 131 ); 132 } catch (parseError) { 133 console.error('[YaCy Proxy] Error al parsear JSON:', parseError); 134 // Si no es JSON válido, crear una estructura compatible 135 return json({ 136 error: 'YaCy no devolvió JSON válido', 137 channels: [{ 138 title: 'Error', 139 description: 'Formato de respuesta inválido', 140 totalResults: '0', 141 startIndex: '0', 142 itemsPerPage: '0', 143 items: [] 144 }] 145 }); 146 } 147 } catch (responseError) { 148 console.error('[YaCy Proxy] Error al obtener respuesta:', responseError); 149 return json({ 150 error: 'Error al leer la respuesta de YaCy', 151 channels: [{ 152 title: 'Error', 153 description: 'No se pudo leer la respuesta', 154 totalResults: '0', 155 startIndex: '0', 156 itemsPerPage: '0', 157 items: [] 158 }] 159 }); 160 } 161 162 // Si llegamos aquí, tenemos datos JSON válidos 163 164 // Si no tiene la estructura esperada, crear una compatible 165 if (!data.channels || !Array.isArray(data.channels)) { 166 console.warn('[YaCy Proxy] Respuesta sin estructura de canales, creando compatible'); 167 data = { 168 channels: [{ 169 title: 'YaCy Search', 170 description: data.error || 'Respuesta no estándar', 171 totalResults: '0', 172 startIndex: '0', 173 itemsPerPage: '0', 174 items: [] 175 }] 176 }; 177 } 178 179 // Asegurarse de que hay una estructura items 180 if (!data.channels[0].items) { 181 console.warn('[YaCy Proxy] Canal sin items, añadiendo array vacío'); 182 data.channels[0].items = []; 183 } 184 185 // Verificar si hay resultados 186 const result = data?.channels?.[0]?.items; 187 console.log(`[YaCy Proxy] Resultados obtenidos: ${result?.length || 0}`); 188 189 // Si no hay resultados pero hay total de resultados, podría ser un problema de YaCy 190 const totalResults = parseInt(data?.channels?.[0]?.totalResults || '0', 10); 191 const startIndex = parseInt(data?.channels?.[0]?.startIndex || '0', 10); 192 193 if (result?.length === 0 && totalResults > 0 && startIndex > 1) { 194 console.log(`[YaCy Proxy] ⚠️ Problema de paginación detectado: totalResults=${totalResults}, startIndex=${startIndex}, pero items vacío`); 195 196 // Si tenemos más de 10 resultados pero YaCy devuelve vacío, añadimos un mensaje de error 197 data.pagination_error = true; 198 data.error_message = "YaCy reporta resultados pero devolvió una página vacía. Esto suele ocurrir con paginación extensa."; 199 200 console.log('[YaCy Proxy] Añadido mensaje de error de paginación para el cliente'); 201 } else if (result?.length > 0) { 202 console.log('[YaCy Proxy] Ejemplo de primer resultado:', JSON.stringify(result[0], null, 2).substring(0, 500)); 203 } 204 205 // Devolver datos al cliente (sea como sea la estructura) 206 return json(data); 207 } catch (searchError: any) { 208 // Error de búsqueda 209 console.error('[YaCy Proxy] Error en búsqueda:', searchError); 210 211 if (searchError.name === 'AbortError') { 212 return json({ 213 error: 'Timeout al conectar con YaCy' 214 }, { status: 200 }); // 200 para que el cliente maneje el error 215 } 216 217 return json({ 218 error: searchError.message || 'Error al realizar la búsqueda' 219 }, { status: 200 }); // 200 para que el cliente maneje el error 220 } 221 } catch (error: any) { 222 // Error general del servidor 223 console.error('[YaCy Proxy] Error general:', error); 224 return json({ 225 error: error.message || 'Error interno del servidor' 226 }, { status: 500 }); 227 } 228 }; 229 230 /** 231 * Verifica si el servidor YaCy está disponible 232 */ 233 async function checkYacyStatus(fetch: any) { 234 try { 235 // Verificar disponibilidad con un timeout reducido 236 const controller = new AbortController(); 237 const timeoutId = setTimeout(() => controller.abort(), timeouts.statusCheck); 238 239 const statusUrl = `${yacyConfig.baseUrl}/api/status.json`; 240 console.log(`[YaCy Proxy] Verificando estado en: ${statusUrl}`); 241 242 const response = await fetch(statusUrl, { 243 signal: controller.signal 244 }); 245 246 clearTimeout(timeoutId); 247 248 if (!response.ok) { 249 throw new Error(`YaCy respondió con código: ${response.status}`); 250 } 251 252 try { 253 const data = await response.json(); 254 255 // Verificar si la respuesta contiene la versión (muestra que está operativo) 256 if (data && data.version) { 257 return json({ 258 status: 'online', 259 version: data.version, 260 uptime: data.uptime, 261 busyThreads: data.busyThreads, 262 idleThreads: data.idleThreads 263 }); 264 } 265 266 return json({ 267 status: 'unknown', 268 message: 'YaCy respondió pero con datos no reconocidos' 269 }); 270 } catch (jsonError) { 271 return json({ 272 status: 'error', 273 message: 'YaCy respondió pero con formato inválido' 274 }); 275 } 276 } catch (error: any) { 277 if (error.name === 'AbortError') { 278 throw new Error('Timeout al verificar YaCy'); 279 } 280 throw error; 281 } 282 } 283 284 /** 285 * Construye los parámetros para la API de YaCy a partir de nuestros parámetros estándar 286 */ 287 function buildYacyParams(params: URLSearchParams) { 288 const yacyParams = new URLSearchParams(); 289 290 // Parámetro de búsqueda 291 const query = params.get('query') || ''; 292 yacyParams.set('query', query); 293 294 // Número máximo de resultados 295 const resultCount = params.get('resultadosPorPagina') || yacyConfig.defaultMaxResults.toString(); 296 yacyParams.set('maximumRecords', resultCount); 297 298 // Página de resultados (calculada a partir de startRecord) 299 const page = parseInt(params.get('page') || '0', 10); 300 // Asegurarse de que la página comienza desde 0 para el cálculo 301 const startRecord = (page * parseInt(resultCount, 10) + 1).toString(); 302 yacyParams.set('startRecord', startRecord); 303 304 // Añadir parámetros adicionales para asegurar compatibilidad con YaCy 305 yacyParams.set('verify', 'false'); 306 307 // Resource si se especifica 308 // Opción predeterminada si no está definido 309 let resourceValue = 'global'; 310 311 // Obtener valor de recurso 312 const recurso = params.get('recurso'); 313 314 // Mapeo específico para YaCy según documentación 315 if (recurso === 'local') { 316 resourceValue = 'local'; 317 } else if (recurso === 'peer' || recurso === 'global') { 318 resourceValue = 'global'; 319 } else if (recurso) { 320 // Si se proporciona algún otro valor, usarlo directamente 321 resourceValue = recurso; 322 } 323 324 console.log(`[YaCy Proxy] Valor de recurso: '${recurso}' mapeado a resource: '${resourceValue}'`); 325 yacyParams.set('resource', resourceValue); 326 327 yacyParams.set('nav', 'all'); 328 329 // Tipo de recursos según documentación YaCy 330 const type = params.get('type') || 'web'; 331 console.log(`[YaCy Proxy] Tipo de búsqueda: '${type}'`); 332 333 // Mapear el tipo a los contentdom exactos que soporta YaCy 334 if (type === 'images') { 335 yacyParams.set('contentdom', 'image'); 336 yacyParams.set('maximumRecords', yacyConfig.imageMaxResults.toString()); 337 } else if (type === 'video') { 338 yacyParams.set('contentdom', 'video'); 339 } else if (type === 'audio') { 340 yacyParams.set('contentdom', 'audio'); 341 } else if (type === 'document' || type === 'app') { 342 yacyParams.set('contentdom', 'app'); 343 } else { 344 // Para web y cualquier otro tipo no especificado, usar 'text' 345 yacyParams.set('contentdom', 'text'); 346 } 347 348 // Idioma 349 const language = params.get('language'); 350 if (language && language !== 'all') { 351 yacyParams.set('language', language); 352 } 353 354 // Resource ya ha sido configurado arriba 355 356 // Preferencias para urlMask - restringir a un dominio específico 357 const preferMask = params.get('preferMask'); 358 if (preferMask) { 359 yacyParams.set('urlmask', preferMask); 360 } 361 362 // No usar constraint por defecto, ya que causa problemas 363 // YaCy tiene problemas de paginación cuando se usa constraint 364 // incluso con "all" si hay muchos resultados 365 366 // Si el usuario explícitamente pide restricciones, respetarlo 367 // pero advertir en logs que puede causar problemas 368 const constraints = params.get('constraints'); 369 if (constraints && constraints.trim() !== '' && constraints !== 'false') { 370 console.log(`[YaCy Proxy] ADVERTENCIA: Usando constraint=${constraints} (puede causar problemas de paginación)`); 371 yacyParams.set('constraint', constraints); 372 } 373 374 // Parámetros para búsqueda explícita de medios 375 if (params.get('mediaSearch') === 'true') { 376 yacyParams.set('former', query); 377 yacyParams.set('verify', 'false'); 378 yacyParams.set('nav', 'hosts,authors,namespace,topics,filetype,protocol'); 379 yacyParams.set('prefermaskfilter', ''); 380 yacyParams.set('depth', '0'); 381 yacyParams.set('cat', 'href'); 382 yacyParams.set('display', params.get('format') || 'json'); 383 } 384 385 // Forzar que se use el formato JSON para la respuesta 386 yacyParams.set('display', 'json'); 387 388 return yacyParams; 389 }