Malformed parts JSON returns HTTP 500 instead of 400 in multipart upload completion - apps/jobs/views.py:3208
- Dominant language
- Python
- Stars
- 2k
- Forks
- 983
- Avg merge
- 2h 54m
- Merged PRs (30d)
- 14
Description
Sending a multipart upload completion request with malformed `parts` JSON returns HTTP 500 instead of HTTP 400.
## Offending code
Both multipart-completion endpoints validate that `parts` is *present*, then parse it with a bare `json.loads()`:
https://github.com/Cloud-CV/EvalAI/blob/f8aff86a7e613c61fae3e4b0e5c0382fe3b52a25/apps/jobs/views.py#L3200-L3208
```python
if request.data.get("parts") is None:
response_data = {"error": "Uploaded file Parts metadata is missing"}
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
if request.data.get("upload_id") is None:
response_data = {"error": "Uploaded file UploadId is missing"}
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
file_parts = json.loads(request.data["parts"])
```
The same pattern is in `finish_annotation_file_upload()`:
https://github.com/Cloud-CV/EvalAI/blob/f8aff86a7e613c61fae3e4b0e5c0382fe3b52a25/apps/challenges/views.py#L4340
## Why it is wrong
That `json.loads()` call sits outside every `try` block in the view — in `finish_submission_file_upload()` the `try` only begins on the following line, around `get_submission_model()`.
`json.JSONDecodeError` subclasses `ValueError`, not `APIException`. DRF's `exception_handler` returns `None` for it:
```
>>> from rest_framework.views import exception_handler
>>> import json
>>> try: json.loads("not-json")
... except Exception as e: exc = e
>>> exception_handler(exc, {})
None
```
Returning `None` means DRF re-raises, the exception propagates to Django, and the request is answered with HTTP 500.
So a *missing* `parts` key is treated as a client error (400), while *malformed* `parts` — the same class of bad client input — is treated as a server fault (500).
This is inconsistent with how the codebase handles the identical situation elsewhere. `update_leaderboard_data()` in the same file guards its parse and returns 400:
https://github.com/Cloud-CV/EvalAI/blob/f8aff86a7e613c61fae3e4b0e5c0382fe3b52a25/apps/jobs/views.py#L2713-L2720
## What the user sees
- A client that sends a truncated or otherwise malformed `parts` payload gets an opaque `500 Internal Server Error` with no indication of what was wrong with the request.
- The traceback is reported to Sentry as a server fault, adding noise that hides real server errors.
- CLI/SDK clients cannot distinguish "fix your request" from "the server is broken", so they have no reason to stop retrying a request that will never succeed.
## Steps to reproduce
1. Authenticate as a participant with a submission in a challenge phase.
2. `POST` to `/api/jobs/phases//finish_submission_file_upload//` with:
```
parts=not-valid-json
upload_id=some-upload-id
```
3. The response is HTTP 500 with an unhandled `json.decoder.JSONDecodeError`.
Sending no `parts` key at all correctly returns HTTP 400, which shows the divergence.
## Expected behaviour
- Malformed `parts` JSON returns HTTP 400.
- The response body names `parts` as the offending field.
- No unhandled exception is raised, so the failure is not reported to Sentry as a server error.
- `finish_submission_file_upload()` and `finish_annotation_file_upload()` behave the same way.
---
I have a branch that guards both call sites using the same pattern as `update_leaderboard_data()` and adds a regression test; I'll open a PR referencing this issue.
Contributor guide
Research direction
Start in apps/jobs/views.py around finish_submission_file_upload() and update_leaderboard_data(), then inspect apps/challenges/views.py around finish_annotation_file_upload(). Add regression coverage for malformed parts input and verify both endpoints return HTTP 400 naming parts without an unhandled exception.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- api, backend, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100