Endpoint GET /api/jobs/{job_id} — consulta estado de operaciones async
Definición
Ruta: GET /api/jobs/{job_id}
Locación: core/api/jobs.py (jobs_router)
Autenticación: sí (requiere sesión válida)
RLS: sí, aislamiento por organization (doble anclaje: filtro ORM + policy BD)
Endpoint de polling que el cliente usa para consultar el estado de una operación larga encolada en Huey. Parte del patrón job + polling (ADR T2 / R5).
Parámetros
| Parámetro | Ubicación | Tipo | Descripción |
|---|---|---|---|
job_id | Path | UUID (str) | Identificador opaco de la operación (recibido del endpoint que la encoló) |
Respuesta (200 OK)
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"kind": "autoplan",
"status": "processing",
"progress": 45,
"result": null,
"error": ""
}
Campos:
job_id(str): eco del requestkind(str): tipo de operación (autoplan,ai_analysis,backup_full,restore_full)status(str):pending|processing|done|errorprogress(int): 0-100 (si la task no lo reporta, queda en 0 hasta que seadone)result(dict | null): endone, contiene datos (p.ej.{"blueprint_id": 12}); en otros estados,null. NUNCA incluye secretos.error(str): enerror, mensaje apto para usuario; en otros estados, vacío
Respuesta (404 Not Found)
{
"message": "Job not found"
}
Causas:
- El
job_idno existe - El
job_idexiste pero pertenece a una organización distinta (aislamiento tenant)
Nota: ambos casos devuelven 404, no 403, para no revelar si el job existe en otra org.
Lógica de aislamiento
# core/api/jobs.py
org = get_current_org(request) # GUC de la sesión
job = AsyncJob.objects.filter(job_id=job_id, organization=org).first()
# Doble anclaje:
# 1. Filtro ORM: solo jobs de esta org
# 2. RLS de BD: la tabla tiene política tenant_isolation (policy 0029)
# Si por error se bypaseara el ORM, la BD igualmente rechazaría el acceso.
if not job:
return 404, {"message": "Job not found"}
Flujo cliente típico
// 1. Encola operación
const enqueued = await fetch('/api/blueprints/autoplan/import', { ... });
const { job_id } = await enqueued.json(); // 202 Accepted
// 2. Polling
const poll = async () => {
const job = await fetch(`/api/jobs/${job_id}`);
const { status, progress, result, error } = await job.json();
if (status === 'done') {
// Éxito
window.location.href = `/blueprints/${result.blueprint_id}`;
} else if (status === 'error') {
// Error
alert(`Failed: ${error}`);
} else {
// pending o processing → vuelve a preguntar en 2.5 seg
setTimeout(poll, 2500);
}
};
setTimeout(poll, 2500); // Espera inicial
Tope de seguridad recomendado
El cliente debe implementar un timeout máximo para no preguntar infinitamente. Sugerencia:
const MAX_MS = 6 * 60 * 1000; // 6 minutos (Auto-Plan tardío)
if (Date.now() - startedAt > MAX_MS) {
alert("Timed out. Please try again.");
}
Véase también
- [[entity—core—model—asyncjob]]
- [[concept—infra—async-job-pattern]]
- [[entity—blueprints—service—run-autoplan-import]]
- [[feature—blueprints—autoplan-async]]