Skip to content

fix: API endpoint hardening and JSON serialization fixes#1004

Open
ankushchk wants to merge 3 commits intoalphaonelabs:mainfrom
ankushchk:fix/brittle-api-endpoints
Open

fix: API endpoint hardening and JSON serialization fixes#1004
ankushchk wants to merge 3 commits intoalphaonelabs:mainfrom
ankushchk:fix/brittle-api-endpoints

Conversation

@ankushchk
Copy link

@ankushchk ankushchk commented Mar 4, 2026

Related issues

Addressed self-identified priority items from the security and stability audit.

Description

This PR hardens the JSON API endpoints in web/views.py to prevent application crashes caused by malformed requests or uninitialized data types.

Changes Made:

  • Robust JSON Parsing: Implemented try-except handling for json.loads to return a 400 Bad Request instead of a server crash (500) if a request contains invalid JSON.
  • Improved Field Validation: Added explicit checks for required keys in POST bodies (title, description, price, etc.) to prevent KeyError crashes.
  • Serialization Fixes: Resolved multiple type errors where database model instances (Subjects) were being passed directly to JsonResponse. String serialization (.name) is now used correctly.
  • Auth Standardization: Ensured all user-specific API endpoints are protected with @login_required decorators to prevent unauthorized access.

Technical Code Changes

1. Serialization Fix

  • Problem: Passing a Database Model directly to JsonResponse caused a TypeError.
  • Solution: Changed course.subject to course.subject.name to ensure JSON compatibility.

2. Input Hardening

  • Problem: Missing URL parameters or bad JSON body caused KeyError and JSONDecodeError (500 Errors).
  • Solution:
    try:
        data = json.loads(request.body)
    except json.JSONDecodeError:
        return JsonResponse({"error": "Invalid JSON"}, status=400)

3. Validation Loop

  • Solution: Added a validation check before processing data:
    required_fields = ["title", "description", "price", "subject"]
    for field in required_fields:
        if field not in data:
            return JsonResponse({"error": f"Missing field: {field}"}, status=400)

Checklist

  • Did you run the pre-commit?
  • Did you test the change? (Verified handling of missing fields, malformed JSON, and successful data serialization)
  • [/] Added screenshots to the PR description (Backend/API changes only)

Summary by CodeRabbit

  • Bug Fixes
    • Stronger input validation for course creation (required fields enforced, sensible default level applied).
    • Improved JSON parsing and error handling to surface clearer request errors.
    • Forum topic and reply creation now validate required fields before saving.
    • API responses now present subject as a concise name string instead of nested objects.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

👀 Peer Review Required

Hi @ankushchk! This pull request does not yet have a peer review.

Before this PR can be merged, please request a review from one of your peers:

  • Go to the PR page and click "Reviewers" on the right sidebar.
  • Select a team member or contributor to review your changes.
  • Once they approve, this reminder will be automatically removed.

Thank you for contributing! 🎉

@github-actions github-actions bot added the files-changed: 1 PR changes 1 file label Mar 4, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Warning

Rate limit exceeded

@ankushchk has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 6 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 441f3c5c-00f3-4f99-9c83-881717c1e19e

📥 Commits

Reviewing files that changed from the base of the PR and between 833e612 and 6463348.

📒 Files selected for processing (1)
  • web/views.py

Walkthrough

API responses now use subject.name instead of nested subject objects. Course creation and forum endpoints add robust JSON parsing, required-field validation, and proper Subject instance resolution (course level defaults to "beginner" if omitted).

Changes

Cohort / File(s) Summary
Web Views (Course & Forum APIs)
web/views.py
Reworked course endpoints to return subject.name for listings/details; strengthened api_course_create with safe JSON parsing, required fields (title, description, learning_objectives, price, max_students, subject), Subject ID validation/resolution, and default level="beginner". Hardened forum endpoints (api_forum_topic_create, api_forum_reply_create) with JSON parsing and required-field checks (topic, title, content, category as applicable).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: API endpoint hardening and JSON serialization fixes' directly aligns with the main changes: robust JSON parsing, field validation, serialization fixes, and auth standardization in API endpoints.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/views.py (1)

2328-2338: ⚠️ Potential issue | 🟠 Major

Validate and normalize price, max_students, and level before persisting.

Currently these values are accepted as-is. Invalid numeric types can still throw server-side errors, and invalid level values can be stored if not explicitly checked.

Suggested input normalization before Course.objects.create
+    try:
+        price = Decimal(str(data["price"]))
+    except Exception:
+        return JsonResponse({"error": "Invalid price"}, status=400)
+
+    try:
+        max_students = int(data["max_students"])
+        if max_students < 1:
+            raise ValueError
+    except (TypeError, ValueError):
+        return JsonResponse({"error": "Invalid max_students"}, status=400)
+
+    allowed_levels = {choice[0] for choice in Course._meta.get_field("level").choices}
+    level = data.get("level", "beginner")
+    if level not in allowed_levels:
+        return JsonResponse({"error": "Invalid level"}, status=400)
+
     course = Course.objects.create(
         teacher=request.user,
         title=data["title"],
         description=data["description"],
         learning_objectives=data["learning_objectives"],
         prerequisites=data.get("prerequisites", ""),
-        price=data["price"],
-        max_students=data["max_students"],
+        price=price,
+        max_students=max_students,
         subject=subject,
-        level=data.get("level", "beginner"),
+        level=level,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/views.py` around lines 2328 - 2338, Before calling Course.objects.create,
validate and normalize data["price"], data["max_students"], and
data.get("level"): coerce price to a decimal/float (or Decimal) and max_students
to an int with proper try/except to return a validation error on failure,
enforce non-negative bounds, and ensure level is one of the allowed values
(e.g., "beginner","intermediate","advanced") falling back to "beginner" if
missing or invalid; replace the raw values passed into Course.objects.create
with the normalized variables and return a clear HTTP 400/validation response
when conversion or validation fails so invalid types are never persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/views.py`:
- Around line 2462-2466: The current required-field loop (using required_fields
and data) only checks key presence and allows empty strings or null relation IDs
to slip through; update the validation to ensure string fields ("title",
"content") are non-empty after trimming and that relation fields ("category" or
"topic") are present and not null/empty (and optionally castable to an int)
before proceeding, returning JsonResponse({"error": "..."} , status=400) on
failure; apply the same strengthened checks to the other forum create endpoint
that uses the same pattern (the block at the other location using
required_fields/data).
- Around line 2317-2321: The current required-fields loop only checks for key
presence (required_fields and data) and allows null/blank values; update the
validation to return 400 if a field is missing OR data[field] is None OR
(isinstance(data[field], str) and data[field].strip() == '') and for sequence
types (e.g., lists like learning_objectives) treat empty sequences as missing by
checking if not data[field] or len(data[field]) == 0; modify the loop that
iterates required_fields to perform these checks and return
JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
when any of those conditions are met so downstream object creation won't receive
null/blank inputs.

---

Outside diff comments:
In `@web/views.py`:
- Around line 2328-2338: Before calling Course.objects.create, validate and
normalize data["price"], data["max_students"], and data.get("level"): coerce
price to a decimal/float (or Decimal) and max_students to an int with proper
try/except to return a validation error on failure, enforce non-negative bounds,
and ensure level is one of the allowed values (e.g.,
"beginner","intermediate","advanced") falling back to "beginner" if missing or
invalid; replace the raw values passed into Course.objects.create with the
normalized variables and return a clear HTTP 400/validation response when
conversion or validation fails so invalid types are never persisted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50c2e8ee-f3ec-4f8e-8fb6-e1f72836872e

📥 Commits

Reviewing files that changed from the base of the PR and between c94caf8 and 5981703.

📒 Files selected for processing (1)
  • web/views.py

web/views.py Outdated
Comment on lines +2317 to +2321
# Required fields check
required_fields = ["title", "description", "learning_objectives", "price", "max_students", "subject"]
for field in required_fields:
if field not in data:
return JsonResponse({"error": f"Missing required field: {field}"}, status=400)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Required-field check still accepts null/blank values.

This guard only checks key presence. Payloads with blank strings or null can still pass and fail later during object creation.

Suggested tightening for required field validation
     required_fields = ["title", "description", "learning_objectives", "price", "max_students", "subject"]
     for field in required_fields:
         if field not in data:
             return JsonResponse({"error": f"Missing required field: {field}"}, status=400)
+        if data[field] is None:
+            return JsonResponse({"error": f"Field cannot be null: {field}"}, status=400)
+
+    for field in ["title", "description", "learning_objectives"]:
+        if not str(data[field]).strip():
+            return JsonResponse({"error": f"Field cannot be blank: {field}"}, status=400)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/views.py` around lines 2317 - 2321, The current required-fields loop only
checks for key presence (required_fields and data) and allows null/blank values;
update the validation to return 400 if a field is missing OR data[field] is None
OR (isinstance(data[field], str) and data[field].strip() == '') and for sequence
types (e.g., lists like learning_objectives) treat empty sequences as missing by
checking if not data[field] or len(data[field]) == 0; modify the loop that
iterates required_fields to perform these checks and return
JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
when any of those conditions are met so downstream object creation won't receive
null/blank inputs.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR has 1 unresolved review conversation. Please resolve them before this PR can be merged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
web/views.py (2)

2462-2468: ⚠️ Potential issue | 🔴 Critical

Non-numeric relation IDs can still bypass hardening and crash these endpoints.

category/topic are only checked for presence/blank. Non-numeric values can still reach ORM lookups and cause server errors instead of returning a 400.

Suggested fix (validate relation IDs before ORM lookup)
     # Required fields check (must be present and not empty)
     required_fields = ["title", "content", "category"]
     for field in required_fields:
         val = data.get(field)
         if val is None or (isinstance(val, str) and not val.strip()):
             return JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
+    try:
+        category_id = int(data["category"])
+    except (TypeError, ValueError):
+        return JsonResponse({"error": "Invalid category ID"}, status=400)
 
-    category = get_object_or_404(ForumCategory, id=data["category"])
+    category = get_object_or_404(ForumCategory, id=category_id)
     # Required fields check (must be present and not empty)
     required_fields = ["topic", "content"]
     for field in required_fields:
         val = data.get(field)
         if val is None or (isinstance(val, str) and not val.strip()):
             return JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
+    try:
+        topic_id = int(data["topic"])
+    except (TypeError, ValueError):
+        return JsonResponse({"error": "Invalid topic ID"}, status=400)
 
-    topic = get_object_or_404(ForumTopic, id=data["topic"])
+    topic = get_object_or_404(ForumTopic, id=topic_id)
#!/bin/bash
set -euo pipefail

# Verify the current code path still uses raw payload values for relation IDs.
rg -n -C4 'required_fields = \["title", "content", "category"\]|get_object_or_404\(ForumCategory, id=data\["category"\]\)|required_fields = \["topic", "content"\]|get_object_or_404\(ForumTopic, id=data\["topic"\]\)' web/views.py

# Verify there is no local coercion/guard for these IDs in the touched blocks.
rg -n -C2 'int\(data\["category"\]\)|int\(data\["topic"\]\)|Invalid category ID|Invalid topic ID' web/views.py

Also applies to: 2496-2502

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/views.py` around lines 2462 - 2468, The current required_fields check
only ensures presence/blank but allows non-numeric relation IDs to pass; before
calling the ORM lookups (e.g. get_object_or_404(ForumCategory,
id=data["category"]) and get_object_or_404(ForumTopic, id=data["topic"]")),
validate and coerce the incoming relation IDs from data.get("category") /
data.get("topic") to integers and return JsonResponse({"error": "Invalid
category ID"} , status=400) or similar for topic on failure (non-numeric or
out-of-range), so the required_fields loop stays but you explicitly
parse/validate the relation IDs and short-circuit with 400 when int conversion
fails.

2317-2323: ⚠️ Potential issue | 🟡 Minor

Required-field check still allows empty non-string payloads.

Line [2321] rejects None and blank strings, but required fields like learning_objectives can still pass as []/{}.

Suggested tightening for required-field emptiness checks
     required_fields = ["title", "description", "learning_objectives", "price", "max_students", "subject"]
     for field in required_fields:
         val = data.get(field)
-        if val is None or (isinstance(val, str) and not val.strip()):
+        if val is None:
+            return JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
+        if isinstance(val, str) and not val.strip():
+            return JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
+        if isinstance(val, (list, tuple, dict, set)) and len(val) == 0:
             return JsonResponse({"error": f"Missing or empty required field: {field}"}, status=400)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/views.py` around lines 2317 - 2323, The required-field validation
currently only rejects None and blank strings; update the loop that iterates
over required_fields (the block using required_fields and val = data.get(field))
to also treat empty containers as invalid by checking for empty
lists/tuples/sets/dicts and zero-length iterables: after obtaining val, if val
is None OR (isinstance(val, str) and not val.strip()) OR (isinstance(val, (list,
tuple, set, dict)) and len(val) == 0) (or more generally: truthiness check for
iterable containers), return the same JsonResponse error for that field; keep
the same error message and status code so required fields like
learning_objectives cannot be [] or {}.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/views.py`:
- Around line 2337-2338: Normalize and validate the `level` value from `data`
before passing it to create: trim whitespace, treat null/empty/blank strings as
missing, default to "beginner", and ensure it is one of the allowed values
(e.g., "beginner", "intermediate", "advanced")—if invalid, set to the default or
raise a controlled validation error; replace the direct use of data.get("level",
"beginner") with a local `level` variable computed from `data` (trimmed,
lowercased, validated) and pass that `level` into the create call where
`subject=subject, level=...` is used.

---

Duplicate comments:
In `@web/views.py`:
- Around line 2462-2468: The current required_fields check only ensures
presence/blank but allows non-numeric relation IDs to pass; before calling the
ORM lookups (e.g. get_object_or_404(ForumCategory, id=data["category"]) and
get_object_or_404(ForumTopic, id=data["topic"]")), validate and coerce the
incoming relation IDs from data.get("category") / data.get("topic") to integers
and return JsonResponse({"error": "Invalid category ID"} , status=400) or
similar for topic on failure (non-numeric or out-of-range), so the
required_fields loop stays but you explicitly parse/validate the relation IDs
and short-circuit with 400 when int conversion fails.
- Around line 2317-2323: The required-field validation currently only rejects
None and blank strings; update the loop that iterates over required_fields (the
block using required_fields and val = data.get(field)) to also treat empty
containers as invalid by checking for empty lists/tuples/sets/dicts and
zero-length iterables: after obtaining val, if val is None OR (isinstance(val,
str) and not val.strip()) OR (isinstance(val, (list, tuple, set, dict)) and
len(val) == 0) (or more generally: truthiness check for iterable containers),
return the same JsonResponse error for that field; keep the same error message
and status code so required fields like learning_objectives cannot be [] or {}.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a2b01e5-aba8-4da8-b173-e49abee0a6dc

📥 Commits

Reviewing files that changed from the base of the PR and between 5981703 and 833e612.

📒 Files selected for processing (1)
  • web/views.py

@github-actions github-actions bot dismissed their stale review March 4, 2026 11:24

All review conversations have been resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

files-changed: 1 PR changes 1 file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant