/ src / routes / api / yacy / search / +server.ts
+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  }