diff --git a/rq_dashboard/web.py b/rq_dashboard/web.py index dd41bdc..2f0fad7 100644 --- a/rq_dashboard/web.py +++ b/rq_dashboard/web.py @@ -40,7 +40,7 @@ Worker, requeue_job, ) -from rq.exceptions import NoSuchJobError +from rq.exceptions import DeserializationError, NoSuchJobError from rq.job import Job from rq.registry import ( DeferredJobRegistry, @@ -68,6 +68,27 @@ class Config: config: Config = Config() + +@blueprint.errorhandler(NoSuchJobError) +def handle_no_such_job(exc): + """Return 404 so missing/expired jobs do not result in 500.""" + return make_response( + json.dumps(dict(error="job_not_found")), + 404, + {"Content-Type": "application/json", "Cache-Control": "no-store"}, + ) + + +@blueprint.errorhandler(DeserializationError) +def handle_deserialization_error(exc): + """Return 409 so corrupt job data does not result in 500.""" + return make_response( + json.dumps(dict(error="job_deserialization_failed")), + 409, + {"Content-Type": "application/json", "Cache-Control": "no-store"}, + ) + + # @blueprint.before_app_first_request def setup_rq_connection(current_app): # we need to do It here instead of cli, since It may be embeded @@ -420,14 +441,8 @@ def job_view(instance_number, job_id): @check_delete_enable @jsonify def delete_job_view(job_id, registry=None): - try: - job = Job.fetch(job_id, connection=current_app.redis_conn) - job.delete() - except NoSuchJobError: - if registry: - registry.remove(job_id) - return dict(status="ERROR") - + job = Job.fetch(job_id, connection=current_app.redis_conn) + job.delete() return dict(status="OK") diff --git a/tests/test_basic.py b/tests/test_basic.py index 757a165..a4b4f46 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,8 +1,10 @@ import json import time import unittest +from unittest.mock import patch import redis +from rq.exceptions import DeserializationError, NoSuchJobError from rq import Queue, Worker from rq_dashboard.cli import make_flask_app @@ -80,6 +82,34 @@ def some_work(): response_del = self.client.post(job_del_url) self.assertEqual(response_del.status_code, HTTP_OK) + def test_delete_nonexistent_job_returns_404(self): + """Delete missing job returns 404 with job_not_found (not 500 or 200 ERROR).""" + response = self.client.post('/job/nonexistent-job-id/delete') + self.assertEqual(response.status_code, 404) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data, {'error': 'job_not_found'}) + + def test_view_nonexistent_job_returns_404(self): + """HTML job view for missing job returns 404.""" + response = self.client.get('/0/view/job/nonexistent-job-id') + self.assertEqual(response.status_code, 404) + + def test_job_info_nonexistent_returns_404(self): + """JSON job info for missing job returns 404 with job_not_found.""" + response = self.client.get('/0/data/job/nonexistent-job-id.json') + self.assertEqual(response.status_code, 404) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data, {'error': 'job_not_found'}) + + def test_job_info_deserialization_error_returns_409(self): + """Corrupt job data returns 409 with job_deserialization_failed.""" + with patch('rq_dashboard.web.Job') as mock_job: + mock_job.fetch.side_effect = DeserializationError() + response = self.client.get('/0/data/job/any-id.json') + self.assertEqual(response.status_code, 409) + data = json.loads(response.data.decode('utf8')) + self.assertEqual(data, {'error': 'job_deserialization_failed'}) + def test_registry_jobs_list(self): for registry_name in REGISTRY_NAMES: response = self.client.get(f'/0/data/jobs/default/{registry_name}/8/asc/1.json')