Cómo diagnosticamos y resolvimos un error HTTP 500 en el servicio OAI-PMH de DSpace

Por Staff INFODI
21 minutos
Cómo diagnosticamos y resolvimos un error HTTP 500 en el servicio OAI-PMH de DSpace

Un error HTTP 500 en un endpoint OAI-PMH puede parecer, a primera vista, un problema de red, proxy, Cloudflare, certificados SSL o incluso de la interfaz Angular. Sin embargo, en este caso la causa estaba en un lugar mucho más específico: los permisos del directorio de caché utilizado por el servicio OAI de DSpace.

Contexto del problema

El repositorio institucional comenzó a presentar problemas de cosecha desde ANID. Al consultar el endpoint OAI-PMH, todas las solicitudes devolvían un error HTTP 500:

https://repositorio.infodi.cl/server/oai/request?verb=ListIdentifiers&metadataPrefix=oai_dc

La respuesta entregada por el backend era:

{
  "timestamp": "2026-07-28T20:57:32.968+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "An internal read or write operation failed",
  "path": "/server/oai/request"
}

El mensaje era genérico y no indicaba directamente qué archivo, componente o servicio estaba fallando.

¿Era un problema de Angular?

No. Esta fue una de las primeras conclusiones importantes.

En DSpace 7, Angular corresponde a la interfaz pública del repositorio. El servicio OAI-PMH, en cambio, es procesado por el backend Java de DSpace, desplegado en Tomcat.

El endpoint afectado era:

/server/oai/request

Por lo tanto, el error no estaba relacionado con componentes, rutas, plantillas o servicios Angular.

Primera prueba: descartar Cloudflare, Nginx y SSL

La consulta pública pasaba por Nginx y por la capa de seguridad del dominio. Para descartar esos componentes, realizamos una consulta directamente contra Tomcat:

curl -v \
"http://127.0.0.1:8080/server/oai/request?verb=Identify"

El resultado fue nuevamente:

HTTP/1.1 500

{ "status": 500, "error": "Internal Server Error", "message": "An internal read or write operation failed" }

Esta prueba permitió descartar como causa principal:

  • Cloudflare.
  • Nginx.
  • HTTP/2.
  • El certificado SSL.
  • La interfaz Angular.

El problema estaba dentro de DSpace o de alguno de sus componentes internos.

Todos los verbos OAI estaban fallando

Probamos los principales verbos del protocolo OAI-PMH:

curl -sk \
"https://repositorio.infodi.cl/server/oai/request?verb=Identify"

curl -sk \ "https://repositorio.infodi.cl/server/oai/request?verb=ListMetadataFormats"

curl -sk \ "https://repositorio.infodi.cl/server/oai/request?verb=ListSets"

curl -sk \ "https://repositorio.infodi.cl/server/oai/request?verb=ListIdentifiers&metadataPrefix=oai_dc"

Todos devolvían HTTP 500.

Este comportamiento era relevante porque permitía descartar, en principio, que el problema estuviera limitado a:

  • Una colección específica.
  • Un ítem defectuoso.
  • Un metadato mal formado.
  • Un registro puntual dentro del índice OAI.

Si incluso Identify fallaba, el servicio OAI completo estaba teniendo problemas al inicializar o generar su respuesta.

El error secundario de Tomcat

Al revisar los logs de Tomcat apareció repetidamente la siguiente excepción:

java.lang.IllegalStateException:
getOutputStream() has already been called for this response

Este mensaje podía inducir a pensar que el problema estaba en el manejo del flujo de salida HTTP, pero en realidad era un error secundario.

El flujo probable era el siguiente:

  1. El servicio OAI comenzaba a construir la respuesta.
  2. Se producía una excepción interna de lectura o escritura.
  3. Spring intentaba generar una página de error.
  4. La respuesta ya había utilizado getOutputStream().
  5. Spring intentaba utilizar getWriter().
  6. Tomcat generaba el error secundario.

Por tanto, esta excepción no era la causa raíz, sino el resultado del intento de representar el error original.

Revisión de espacio en disco e inodos

Debido al mensaje:

An internal read or write operation failed

una de las hipótesis iniciales fue falta de espacio, inodos agotados o un sistema de archivos montado en modo de solo lectura.

Se revisó el almacenamiento:

df -h /
df -h /app
df -h /tmp
df -i /

Los resultados indicaron:

/       50 % utilizado
/app    82 % utilizado, con 356 GB disponibles
/tmp    50 % utilizado
inodos   9 % utilizados

Por lo tanto, no existía falta de espacio ni agotamiento de inodos.

Durante la revisión también detectamos un volumen importante de logs:

/app/tomcat9/logs  28 GB
/app/dspace7/log   67 GB

Aunque esto no causaba directamente el error, sí evidenciaba la necesidad de implementar o revisar políticas de rotación, compresión y retención de logs.

Verificación del core OAI de Solr

DSpace utiliza un core de Solr específico para OAI. Por ello, la siguiente hipótesis fue que el core:

  • No existiera.
  • No hubiera iniciado.
  • Estuviera corrupto.
  • No contuviera registros.

Consultamos el estado de los cores:

curl -sS \
"http://127.0.0.1:8983/solr/admin/cores?action=STATUS&wt=json"

El core oai se encontraba operativo:

"oai": {
  "name": "oai",
  "numDocs": 47847,
  "deletedDocs": 4708,
  "current": true,
  "size": "328.99 MB"
}

Además:

"initFailures": {}

Esto permitió descartar:

  • Solr detenido.
  • Core OAI inexistente.
  • Error de inicialización del core.
  • Índice OAI vacío.

Revisión de la configuración OAI

La configuración de DSpace indicaba:

oai.enabled = true
oai.path = oai
oai.storage = solr
oai.url = ${dspace.server.url}/${oai.path}
oai.solr.url = ${solr.server}/${solr.multicorePrefix}oai
oai.cache.enabled = true
oai.cache.dir = ${dspace.dir}/var/oai

El último punto fue especialmente importante:

oai.cache.enabled = true
oai.cache.dir = ${dspace.dir}/var/oai

DSpace utilizaba una caché en:

/app/dspace7/var/oai

A partir de ese momento, la investigación se concentró en los permisos de ese directorio.

La causa raíz

Al revisar el contenido de la caché OAI encontramos:

ls -lah /app/dspace7/var/oai

Resultado:

drwxr-xr-x 3 dspace dspace 4.0K Apr 13 18:17 .
-rw-rw-r-- 1 dspace dspace   16 Jul 28 01:00 date.file
drwxr-x--- 2 root   root   204K Jun 30 16:47 requests

El directorio:

/app/dspace7/var/oai/requests

pertenecía a:

root:root

y tenía permisos:

750

Sin embargo, Tomcat estaba ejecutándose como:

dspace:dspace

Esto se confirmó con:

ps -eo user,group,pid,cmd \
| grep '[o]rg.apache.catalina.startup.Bootstrap'

Por lo tanto, el proceso Java de DSpace no podía:

  • Entrar al directorio requests.
  • Leer los archivos de caché.
  • Crear nuevas respuestas.
  • Actualizar solicitudes OAI existentes.

Esa era la causa directa del mensaje:

An internal read or write operation failed

Por qué el directorio tenía propietario root

El directorio contenía cientos de megabytes de respuestas OAI almacenadas:

total 437M

Los archivos también pertenecían a root:root:

-rw-r----- 1 root root 564K ...
-rw-r----- 1 root root 602K ...
-rw-r----- 1 root root 570K ...

La explicación más probable es que en algún momento se ejecutó un comando administrativo de DSpace como usuario root, por ejemplo:

/app/dspace7/bin/dspace oai ...

Al ejecutar la tarea como root, los archivos y directorios creados quedaron bajo ese propietario.

Posteriormente, Tomcat, que corre como usuario dspace, perdió acceso a la caché.

Corrección aplicada

Se corrigió el propietario del árbol OAI:

chown -R dspace:dspace /app/dspace7/var/oai

También se dejaron permisos restrictivos pero funcionales:

find /app/dspace7/var/oai -type d -exec chmod 750 {} \;
find /app/dspace7/var/oai -type f -exec chmod 640 {} \;
chmod 755 /app/dspace7/var/oai

Luego se comprobó que el usuario dspace

sudo -u dspace sh -c '
touch /app/dspace7/var/oai/requests/prueba_permiso &&
rm -f /app/dspace7/var/oai/requests/prueba_permiso
'

La prueba terminó correctamente.

Validación final

No fue necesario reiniciar Tomcat.

Se volvió a consultar el endpoint directamente:

curl -i \
"http://127.0.0.1:8080/server/oai/request?verb=Identify"

La respuesta fue:

HTTP/1.1 200
Content-Type: text/xml;charset=UTF-8

El XML OAI-PMH volvió a generarse correctamente:

<Identify>
  <repositoryName>Repositorio infodi</repositoryName>
  <baseURL>
    https://repositorio.infodi.cl/server/oai/request
  </baseURL>
  <protocolVersion>2.0</protocolVersion>
  <adminEmail>repositorio@infodi.cl</adminEmail>
  <earliestDatestamp>2013-03-05T17:45:02Z</earliestDatestamp>
  <deletedRecord>transient</deletedRecord>
  <granularity>YYYY-MM-DDThh:mm:ssZ</granularity>
</Identify>

La interfaz visual del proveedor OAI también volvió a funcionar. Al acceder sin indicar un verbo:

https://repositorio.infodi.cl/server/oai/request

DSpace mostró correctamente:

Error: Illegal verb

Este mensaje es esperado, ya que una solicitud OAI-PMH debe incluir un verbo válido como Identify, ListSets, ListRecords o ListIdentifiers.

Diferencia entre el error 500 y un posible 403

Luego de resolver el error interno, un cosechador externo todavía podía mostrar:

Invalid URL (403 Forbidden)

Es importante distinguir ambos problemas.

Error HTTP 500

El error 500 estaba dentro del backend de DSpace y era causado por permisos incorrectos en la caché OAI.

Error HTTP 403

Un 403 indica que la solicitud fue rechazada por una capa de acceso o seguridad. Las causas posibles incluyen:

  • Cloudflare WAF.
  • Reglas de firewall.
  • Protección contra bots.
  • Bloqueo por User-Agent.
  • Restricciones por dirección IP.
  • Rate limiting.
  • Reglas configuradas en Nginx.

Un problema de certificado SSL normalmente no produce directamente un HTTP 403. Generalmente genera mensajes como:

SSL certificate problem
infodile to get local issuer certificate
certificate verify failed
TLS handshake failed

Sin embargo, en una arquitectura con Cloudflare también debe considerarse que el servidor de origen puede utilizar un certificado Cloudflare Origin CA.

Ese certificado está diseñado para la conexión:

Cloudflare → servidor de origen

No es un certificado de confianza pública para navegadores o cosechadores que se conecten directamente al origen.

Por lo tanto, si un cliente evita Cloudflare y accede directamente al servidor, puede presentar problemas de validación SSL. No obstante, si recibe específicamente un 403, debe revisarse la capa de seguridad y el registro de acceso.

Cómo diagnosticar un 403 desde un cosechador externo

Para analizar un 403 conviene solicitar al cosechador:

  • La URL exacta utilizada.
  • La IP pública de origen.
  • El User-Agent.
  • La fecha y hora exacta de la prueba.
  • El mensaje completo del error.

Luego se puede buscar la solicitud en los logs:

grep "IP_DEL_COSECHADOR" /var/log/nginx/access.log

Si el servidor está detrás de Cloudflare, se debe registrar la IP real mediante encabezados como:

CF-Connecting-IP

También conviene revisar:

/var/log/nginx/error.log
/app/tomcat9/logs/localhost_access_log.YYYY-MM-DD.txt

Buenas prácticas para evitar que vuelva a ocurrir

1. Ejecutar comandos DSpace con el usuario correcto

Los comandos administrativos deben ejecutarse como el usuario propietario de la instalación:

sudo -u dspace /app/dspace7/bin/dspace ...

Evitar:

root@servidor:~# /app/dspace7/bin/dspace ...

2. Revisar periódicamente propietarios incorrectos

find /app/dspace7/var \
-user root \
-printf '%M %u:%g %p\n'

También:

find /app/dspace7/log \
-user root \
-printf '%M %u:%g %p\n'

3. Validar la escritura de la caché OAI

sudo -u dspace test -w /app/dspace7/var/oai/requests \
&& echo "Directorio escribible" \
|| echo "Sin permiso de escritura"

4. Configurar rotación de logs

En este caso se detectaron decenas de gigabytes de logs históricos. Es recomendable implementar logrotate para:

  • Rotar diariamente.
  • Comprimir archivos antiguos.
  • Retener una cantidad limitada de días.
  • Evitar eliminar logs activos.

5. Validar periódicamente el servicio OAI

Se puede implementar una verificación automática:

curl -fsS \
"https://repositorio.infodi.cl/server/oai/request?verb=Identify" \
-o /dev/null

Si el comando devuelve un código distinto de cero, se puede generar una alerta.

6. Verificar Solr OAI

curl -fsS \
"http://127.0.0.1:8983/solr/oai/admin/ping?wt=json"

Comandos principales utilizados en el diagnóstico

# Probar OAI públicamente
curl -sk \
"https://repositorio.infodi.cl/server/oai/request?verb=Identify"

Probar directamente contra Tomcat

curl -i \ "http://127.0.0.1:8080/server/oai/request?verb=Identify"

Revisar espacio

df -h / df -h /app df -i /

Verificar proceso de Tomcat

ps -eo user,group,pid,cmd \ | grep '[o]rg.apache.catalina.startup.Bootstrap'

Verificar cores de Solr

curl -sS \ "http://127.0.0.1:8983/solr/admin/cores?action=STATUS&amp;wt=json"

Revisar configuración OAI

grep -RniE \ "oai.|xoai|solr.*oai|baseUrl|baseURL" \ /app/dspace7/config

Revisar permisos de la caché

ls -lah /app/dspace7/var/oai ls -lah /app/dspace7/var/oai/requests | head

Corregir propietarios

chown -R dspace:dspace /app/dspace7/var/oai

Probar escritura

sudo -u dspace sh -c ' touch /app/dspace7/var/oai/requests/prueba_permiso && rm -f /app/dspace7/var/oai/requests/prueba_permiso '

Conclusión

El error HTTP 500 del servicio OAI-PMH no estaba relacionado con Angular, Cloudflare, Nginx, SSL, falta de disco ni con un core OAI de Solr dañado.

La causa fue un problema de propiedad y permisos en:

/app/dspace7/var/oai/requests

El directorio había sido creado o modificado como root:root, mientras que DSpace se ejecutaba como dspace:dspace.

Al corregir el propietario, el endpoint volvió inmediatamente a responder con HTTP 200.

Este incidente demuestra la importancia de no ejecutar tareas administrativas de DSpace como root y de revisar siempre el usuario efectivo de Tomcat antes de modificar índices, cachés, logs o directorios de trabajo.

También muestra que los errores visibles en Tomcat no siempre son la causa original. En este caso, getOutputStream() has already been called era solo una excepción secundaria que ocultaba el verdadero problema: DSpace no podía acceder a su propia caché OAI.