<- Volver a las notas

La API estaba rota, así que rastreé el mapa que funcionaba

geo-hazard ingiere tres feeds públicos sobre España: avisos meteorológicos de AEMET, terremotos del IGN e incendios de Copernicus EFFIS. Los dos primeros estuvieron en vivo en un día. EFFIS tardó tres, y no porque el código fuera difícil: porque durante dos de esos días estuve haciéndole la pregunta correcta a un servidor roto, en la dirección equivocada.

Un 200 que miente, una capa que revienta

La documentación de EFFIS apunta a servicios OGC estándar. La realidad, capturada a las malas: el servidor se cuelga con HTTP/2, WFS 2.0.0 devuelve 502, 1.1.0 se cuelga, y solo WFS 1.0.0 contesta. Y cuando por fin le pedí la capa que quería - effis.nrt.ba.poly, áreas quemadas en near-real-time - contestó al instante, con HTTP 200, conteniendo msPostGISLayerGetItems(): Query error. Las capas de focos simplemente hacían timeout. Mismo resultado al día siguiente.

Así que pausé el job de incendios en producción - un UPDATE de una fila poniendo su next_run_at a infinity - y la API sirvió cero filas de incendios. El vacío honesto gana a los datos inventados.

Rastrea el producto, no la documentación

Entonces alguien (yo, enseñando el proyecto) abrió el visor oficial de EFFIS: incendios por todas partes, áreas quemadas de tres fuentes satelitales, actualizándose tan contento. Los datos existían y se estaban sirviendo. Solo que yo no preguntaba donde pregunta el visor.

Un navegador headless capturando el tráfico de red lo respondió en minutos. El visor pinta los incendios como tiles raster (WMTS) - lo que significa que ver incendios en un mapa no demuestra nada sobre ninguna API vectorial. Su API de apoyo resultó ser un FastAPI diminuto con cuatro endpoints, ninguno de datos. Pero los nombres de las capas de tiles - nrt.ba.today, s3.hs.today - eran la pista. Mismo servidor, mismo MapServer, distinto mapfile: el visor se alimenta de /gwis, la documentación apunta a /effis. En /gwis la familia completa existe y contesta: nrt.ba.poly.{today,week,month,season} con polígonos frescos, all.hs.* con focos de esta misma mañana.

Las rarezas son el contrato

Lo que significa “funcionar” en el mundo OGC de hace veinte años mereció su propio ADR:

  • El parámetro TIME se ignora en silencio. La ventana temporal va codificada en el nombre de la capa: .today, .week y .season son capas distintas.
  • La salida GeoJSON serializa las coordenadas como [lat, lon] - invertidas - mientras que el parámetro bbox de la petición quiere el orden normal lon,lat. El swap de ejes vive en exactamente un sitio de mi parser, junto a un comentario que explica por qué.
  • Los fallos del backend vuelven como HTTP 200 con un ServiceExceptionReport en XML. Clasifica por el cuerpo, no por el status: ese es transitorio (reintenta en el siguiente poll), un 404 es un cambio de contrato (que mire un humano).
  • Existen capas de archivo con atributos ricos (viirs.hs.query, con potencia radiativa y confianza) pero abarcan toda la historia desde 2019, y cualquier filtro por fecha hace timeout. Inservibles para sync; anotado y adiós.

Capturé payloads reales como fixtures commiteados, escribí el parser contra ellos, y el driver llegó a producción esa misma tarde.

El primer payload real es un test que no has ejecutado

La primera sincronización en vivo trajo 7.970 focos y 92 polígonos de área quemada, y se estrelló de inmediato: the number of query arguments cannot exceed 32767. Mi upsert mandaba el lote entero en una sola sentencia - unas 8.000 filas por 9 columnas son 72.000 parámetros, y asyncpg corta la sentencia en 32.767. Las otras dos fuentes nunca se acercaron (230 avisos, 14 sismos), así que 174 tests y un driver fake jamás habían ejercitado el límite. El fix es aburrido: trocear el lote dentro de la misma transacción. La contención funcionó como estaba diseñada - job fallido, backoff, cero corrupción - y el test de regresión inserta ahora 4.001 filas porque ese es el número honesto más pequeño que habría fallado.

Tres lecciones. Cuando el endpoint documentado está roto pero el producto que tiene encima funciona, rastrea el producto. En los servicios OGC veteranos el contrato real es folklore: captura payloads en vivo como fixtures y escribe los parsers contra eso, no contra la spec. Y los drivers fake te llevan a producción, pero el primer payload real sigue siendo un test, lo hayas escrito o no.