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.
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.
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:
El problema se encuentra desde DSpace hacia abajo.
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.
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.
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.
root:root versus dspace:dspaceEl 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.
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.
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.
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.
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.
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
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;rsync, cp, scp o restauraciones;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.
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.