DSpace: diagnóstico y corrección de errores HTTP 500 al descargar bitstreams por permisos en el Assetstore

Por Staff INFODI
21 minutos
DSpace: diagnóstico y corrección de errores HTTP 500 al descargar bitstreams por permisos en el Assetstore

En una instalación de DSpace, un error HTTP 500 Internal Server Error al descargar un archivo no necesariamente significa que el registro esté dañado, que exista un problema con Nginx o que la política de acceso de DSpace esté mal configurada.

Un caso reciente nos permitió documentar un escenario especialmente útil para futuras tareas de migración y administración de repositorios: los metadatos y bitstreams estaban correctamente registrados en PostgreSQL, los archivos existían físicamente en el Assetstore, pero el proceso DSpace no tenía permisos suficientes para leerlos debido al propietario y permisos establecidos a nivel del filesystem.

Este artículo documenta el diagnóstico completo, desde Nginx hasta el archivo físico, y la forma utilizada para corregir el problema de manera controlada.

1. Síntoma inicial

El problema se presentaba principalmente al intentar descargar archivos PDF asociados a registros antiguos.

Desde la interfaz, la descarga terminaba en una página de error del backend:

Whitelabel Error Page

There was an unexpected error
(type=Internal Server Error, status=500).

An internal read or write operation failed

La URL involucrada correspondía al endpoint REST utilizado por DSpace para entregar el contenido de un bitstream:

/server/api/core/bitstreams/<UUID>/content

Una observación importante era que los registros ingresados recientemente funcionaban correctamente, mientras algunos registros antiguos devolvían HTTP 500.

Esto inmediatamente sugería comparar ambos grupos.


2. Antes de culpar a Nginx: determinar quién genera el HTTP 500

En una arquitectura habitual tenemos:

Cliente
   ↓
Cloudflare / Proxy
   ↓
Nginx
   ↓
DSpace REST / Tomcat
   ↓
PostgreSQL
   ↓
Assetstore

Por lo tanto, recibir un 500 desde Nginx no significa necesariamente que Nginx sea quien lo genera.

La forma más sencilla de aislarlo es consultar directamente el backend local:

curl -v \
'http://127.0.0.1:8080/server/api/core/bitstreams/<UUID>/content' \
-o /dev/null

Si la petición directa a 127.0.0.1:8080 también devuelve 500, podemos descartar:

  • Cloudflare;
  • DNS;
  • SSL;
  • Nginx;
  • comunicación Nginx → Tomcat.

El problema se encuentra desde DSpace hacia abajo.


3. Revisando el histórico de errores de Nginx

Los logs de acceso permiten determinar si estamos ante un caso aislado o un patrón.

En nuestro caso se analizaron los logs actuales y rotados:

{
    cat /var/log/nginx/dspace-ui-access.log \
        /var/log/nginx/dspace-ui-access.log.1

    zcat /var/log/nginx/dspace-ui-access.log.{2..6}.gz
} |
awk '$9 ~ /^50[0-4]$/ {print $9}' |
sort |
uniq -c

También resulta muy útil identificar los endpoints responsables:

{
    cat /var/log/nginx/dspace-ui-access.log \
        /var/log/nginx/dspace-ui-access.log.1

    zcat /var/log/nginx/dspace-ui-access.log.{2..6}.gz
} |
awk '$9 == 500 {print $7}' |
sort |
uniq -c |
sort -nr |
head -30

En el análisis aparecieron dos grupos relevantes:

/server/api/dso/find?uuid=...
/server/api/core/bitstreams/<UUID>/content

Los errores relacionados con dso/find constituían otro fenómeno asociado principalmente a la resolución de entidades restringidas y crawlers, por lo que fueron separados del problema de descarga.

Para los bitstreams se contabilizaron cientos de respuestas 500, justificando analizar directamente el almacenamiento.


4. Seguir el archivo desde el Handle hasta el Assetstore

Una buena técnica de diagnóstico en DSpace consiste en seguir toda la cadena:

Handle
  ↓
Item UUID
  ↓
Bundle
  ↓
Bitstream UUID
  ↓
internal_id
  ↓
store_number
  ↓
Archivo físico

En DSpace 7, el Handle puede obtenerse desde PostgreSQL:

SELECT handle, resource_id
FROM handle
WHERE handle = 'handle/70856';

Ejemplo:

handle     resource_id
---------  ------------------------------------
handle/70856  87d8b300-443c-4e44-8a87-f42aaa1d84d0

Luego podemos atravesar los bundles y bitstreams:

SELECT
    h.handle,
    h.resource_id AS item_uuid,
    i2b.bundle_id,
    b2b.bitstream_id,
    bs.internal_id,
    bs.store_number,
    bs.size_bytes,
    bs.deleted,
    bs.sequence_id
FROM handle h
JOIN item2bundle i2b
    ON i2b.item_id = h.resource_id
JOIN bundle2bitstream b2b
    ON b2b.bundle_id = i2b.bundle_id
JOIN bitstream bs
    ON bs.uuid = b2b.bitstream_id
WHERE h.handle = 'handle/70856'
ORDER BY i2b.bundle_id, b2b.bitstream_order;

Para el PDF problemático encontramos:

bitstream UUID:
9dfa54c7-781f-46c1-aa4c-c2960b2ac459

internal_id:
96680327590467491847516136017499321967

store_number:
0

size_bytes:
5950584

deleted:
false

Esto demostraba que DSpace conocía correctamente el bitstream y no estaba marcado como eliminado.


5. Identificar el Assetstore utilizado por DSpace

La configuración se encontraba en:

/app/dspace7/config/modules/assetstore.cfg

con:

assetstore.dir = /app/datos/assetstore
assetstore.index.primary = 0

Además:

assetstore.s3.enabled = false

Por lo tanto:

store_number = 0
        ↓
Assetstore primario
        ↓
/app/datos/assetstore

Ya podíamos buscar físicamente el internal_id:

find /app/datos/assetstore \
-type f \
-name '96680327590467491847516136017499321967' \
-ls

Y apareció:

/app/datos/assetstore/96/68/03/
96680327590467491847516136017499321967

Por lo tanto, el archivo no estaba perdido.


6. El hallazgo: root:root versus dspace:dspace

El resultado mostró:

-rw-r----- root root 5950584 ...

Al comparar con un registro reciente que funcionaba correctamente encontramos:

-rw-r----- dspace dspace 37496009 ...

Esto era una diferencia crítica.

Los permisos:

640

significan:

Propietario: lectura + escritura
Grupo:       lectura
Otros:       sin acceso

Por lo tanto:

-rw-r----- root root

permite leer el archivo a root y miembros del grupo root, pero no al usuario dspace.

La comprobación definitiva fue ejecutar la prueba como el mismo usuario utilizado por DSpace:

sudo -u dspace test -r \
/app/datos/assetstore/96/68/03/96680327590467491847516136017499321967 \
&& echo "PUEDE LEER" || echo "NO PUEDE LEER"

Resultado:

NO PUEDE LEER

Mientras el bitstream reciente devolvía:

PUEDE LEER

Habíamos encontrado una causa concreta del HTTP 500.


7. El problema también puede estar en los directorios

Cambiar únicamente el propietario del archivo no fue suficiente.

Después de convertirlo a:

-rw-r----- dspace dspace

la prueba continuaba indicando:

NO PUEDE LEER

¿Por qué?

La herramienta namei fue fundamental:

namei -l \
/app/datos/assetstore/96/68/03/96680327590467491847516136017499321967

El resultado permitió inspeccionar todos los componentes de la ruta:

drwxr-xr-x root   root   /
drwxr-xr-x root   root   app
drwxrwxrwx root   root   datos
drwxrwxrwx dspace dspace assetstore
drwxr-xr-x dspace dspace 96
drwxr-xr-x dspace dspace 68
drwxr-x--- root   root   03
-rw-r----- dspace dspace 966803...

El problema estaba ahora en:

drwxr-x--- root root 03

Un directorio necesita permiso x para poder ser atravesado.

Con 750 root:root, el usuario dspace no podía ingresar al directorio 03, aunque el archivo final fuera suyo.

Esta es una distinción importante:

Tener permiso de lectura sobre un archivo no sirve si el proceso no puede atravesar alguno de los directorios que componen su ruta.


8. Medir el problema antes de hacer cambios masivos

Antes de ejecutar cualquier chown -R, analizamos el Assetstore.

Para archivos:

find /app/datos/assetstore \
-type f \
-user root \
-group root \
-printf '%m\n' |
sort |
uniq -c |
sort -nr

Resultado:

16423 644
 2381 640

Esto era importante.

Los 16.423 archivos root:root 644 eran legibles por dspace, ya que 644 permite lectura a otros usuarios.

Los realmente problemáticos eran:

2381 archivos root:root 640

Hicimos lo mismo con directorios:

find /app/datos/assetstore \
-type d \
-user root \
-group root \
-printf '%m\n' |
sort |
uniq -c |
sort -nr

Resultado:

6116 755
 880 750

Nuevamente, los 755 podían ser atravesados por dspace.

Los problemáticos eran:

880 directorios root:root 750

Esto permitió evitar una modificación indiscriminada de todo el Assetstore.


9. Generar evidencia antes de modificar

Antes de corregir los archivos se generó un CSV:

OUT="/root/assetstore_root_root_640_20260817.csv"

echo '"ruta","propietario","grupo","permisos","bytes"' > "$OUT"

find /app/datos/assetstore \
-type f \
-user root \
-group root \
-perm 640 \
-printf '"%p","%u","%g","%m","%s"\n' >> "$OUT"

El archivo quedó almacenado en:

/root/assetstore_root_root_640_20260817.csv

También se generó un respaldo equivalente para los directorios:

/root/assetstore_dirs_root_root_750_20260817.csv

Esta práctica es muy recomendable antes de ejecutar modificaciones masivas: permite conocer exactamente qué objetos fueron afectados y mantener trazabilidad de la intervención.


10. Corrección controlada

En vez de:

chown -R dspace:dspace /app/datos/assetstore

se decidió modificar exclusivamente los objetos que impedían el acceso.

Para los 2.381 archivos:

find /app/datos/assetstore \
-type f \
-user root \
-group root \
-perm 640 \
-exec chown dspace:dspace {} +

Posteriormente:

find /app/datos/assetstore \
-type f \
-user root \
-group root \
-perm 640 |
wc -l

Resultado:

0

Para los 880 directorios:

find /app/datos/assetstore \
-type d \
-user root \
-group root \
-perm 750 \
-exec chown dspace:dspace {} +

Y verificamos:

find /app/datos/assetstore \
-type d \
-user root \
-group root \
-perm 750 |
wc -l

Resultado:

0

Finalmente repetimos:

sudo -u dspace test -r \
/app/datos/assetstore/96/68/03/96680327590467491847516136017499321967 \
&& echo "PUEDE LEER" || echo "NO PUEDE LEER"

Ahora obtuvimos:

PUEDE LEER

La descarga desde DSpace comenzó a funcionar inmediatamente.

No fue necesario reiniciar Nginx ni DSpace.


11. ¿Cambiar permisos del filesystem altera embargos o restricciones de DSpace?

No.

Este punto es especialmente importante.

Existen dos niveles completamente diferentes:

┌─────────────────────────────────┐
│ Linux / Filesystem              │
│                                 │
│ ¿DSpace puede leer físicamente  │
│ el archivo del Assetstore?      │
└───────────────┬─────────────────┘
                ↓
┌─────────────────────────────────┐
│ DSpace Authorization            │
│                                 │
│ Resource Policies               │
│ Anonymous / grupos / usuarios   │
│ Embargos                        │
│ Restricciones                   │
└───────────────┬─────────────────┘
                ↓
             Usuario

Cambiar:

root:root

por:

dspace:dspace

no hace público el archivo.

Simplemente permite que el proceso DSpace pueda leer el recurso físico que administra.

Después de leerlo, DSpace continúa aplicando normalmente sus políticas de autorización.

Por ejemplo:

Filesystem
-----------
-rw-r----- dspace dspace documento.pdf

              ↓

DSpace
------
Anonymous: DENEGADO
Administrador: PERMITIDO
Embargo: hasta 01-01-2027

El archivo sigue estando protegido.

Por el contrario, si Linux impide que DSpace lea el archivo:

-rw-r----- root root documento.pdf

DSpace ni siquiera puede llegar correctamente al punto de entregar el contenido, produciendo errores internos como:

HTTP 500
An internal read or write operation failed

12. ¿Desde cuándo existía el problema?

Linux permite obtener información interesante mediante stat.

Para uno de los archivos afectados:

stat \
/app/datos/assetstore/96/68/03/96680327590467491847516136017499321967

obtuvimos:

Access: 2026-08-16 15:56:31
Modify: 2026-08-10 18:34:04
Change: 2026-08-17 16:41:18
Birth:  2026-08-10 18:34:04

Birth indica que ese inode fue creado en el filesystem actual el:

10-08-2026 18:34

Mientras Change refleja nuestra posterior modificación del propietario.

Esto permite investigar cuándo se incorporaron los archivos afectados al Assetstore, aunque existe una advertencia importante:

Linux normalmente no mantiene un historial completo de propietarios y permisos anteriores.

Por lo tanto, stat permite establecer cuándo apareció físicamente el archivo en ese filesystem, pero no necesariamente demostrar por sí solo quién o qué proceso le asignó originalmente root:root.

Para investigaciones posteriores conviene correlacionar:

  • Birth y mtime;
  • historiales de shell;
  • comandos rsync, cp, scp o restauraciones;
  • scripts de migración;
  • backups;
  • logs de despliegue;
  • fechas de intervención.

13. Lecciones que deja este incidente

Cuando DSpace devuelve:

500 Internal Server Error
An internal read or write operation failed

durante una descarga, conviene revisar sistemáticamente:

1. ¿Nginx genera el 500 o lo devuelve DSpace?

2. ¿El bitstream existe en PostgreSQL?

3. ¿Está deleted=false?

4. ¿Qué store_number tiene?

5. ¿Cuál es su internal_id?

6. ¿Existe físicamente en el Assetstore?

7. ¿El usuario dspace puede leerlo?

8. ¿Puede atravesar TODOS los directorios de la ruta?

9. ¿Propietario y grupo son coherentes?

10. ¿El problema afecta archivos antiguos, recientes o ambos?

Y dos comandos resultaron especialmente valiosos:

sudo -u dspace test -r /ruta/al/bitstream

para comprobar el acceso desde la perspectiva real del servicio, y:

namei -l /ruta/completa/al/bitstream

para detectar permisos incorrectos en cualquier nivel de la ruta.

Conclusión

Un Assetstore puede parecer perfectamente íntegro: los archivos existen, tienen el tamaño correcto y están registrados en PostgreSQL. Sin embargo, si el usuario que ejecuta DSpace no puede leerlos o atravesar sus directorios, la aplicación puede terminar respondiendo HTTP 500.

En este caso se identificaron 2.381 archivos root:root 640 y 880 directorios root:root 750. Se documentaron previamente en CSV y se corrigió exclusivamente su propietario/grupo a dspace:dspace, manteniendo los permisos existentes y sin alterar las políticas de autorización de DSpace.

La comprobación final fue sencilla pero definitiva:

Antes: NO PUEDE LEER
Después: PUEDE LEER

y el bitstream que originalmente devolvía HTTP 500 volvió a descargarse correctamente desde la plataforma.

Este tipo de diagnóstico es especialmente recomendable después de migraciones, restauraciones, copias de Assetstore o intervenciones ejecutadas como root, donde los archivos pueden existir correctamente pero haber quedado con propietarios incompatibles con el usuario que ejecuta DSpace.