Skip to content

fix(flashcards): handle invalid rating types and values in reviewFlashcard (#1199) - #1805

Open
suhaniiz wants to merge 1 commit into
Canopus-Labs:hex-authfrom
suhaniiz:fix/issue-1199-unhandled-typeerror
Open

fix(flashcards): handle invalid rating types and values in reviewFlashcard (#1199)#1805
suhaniiz wants to merge 1 commit into
Canopus-Labs:hex-authfrom
suhaniiz:fix/issue-1199-unhandled-typeerror

Conversation

@suhaniiz

@suhaniiz suhaniiz commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

📝 Pull Request Description

Related Issue

Closes #1199

Summary

Fixed an unhandled TypeError in reviewFlashcard where passing non-primitive data types (e.g. objects {"rating": {}}, arrays {"rating": []}) or unsupported rating strings caused runtime exceptions and a 500 Internal Server Error.

Added strict type checking and value whitelisting at the controller level so that invalid or malformed inputs return a clean 400 Bad Request with an informative error message before calculateSM2 is called.


Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature
  • ♻️ Refactoring
  • 📝 Documentation update
  • 🎨 UI/UX improvement
  • 🔥 Other(please describe) ______

How Has This Been Tested?

Tested endpoint PUT /api/flashcards/:id/review via Postman/cURL with the following cases:

  • Passed object/array payloads like {"rating": {"invalid": "object"}} and {"rating": [1, 2]} -> Returns 400 Bad Request (previously triggered 500 error).
  • Passed unsupported string like {"rating": "super_easy"} -> Returns 400 Bad Request.
  • Passed empty body {} and {"rating": null} -> Returns 400 Bad Request.
  • Passed valid strings/numbers like {"rating": "good"}, {"rating": "HARD"}, and {"rating": 3} -> Returns 200 OK and updates SM-2 parameters correctly.

Screenshots (if applicable)

N/A (Backend API fix)


Checklist

  • My code follows the project's guidelines
  • I have tested my changes
  • I have updated documentation where necessary
  • I have linked the related issue
  • My changes do not introduce new warnings or errors

Summary

  • Added reviewFlashcard validation for rating types and supported values.
  • Return 400 Bad Request for missing, invalid, or unsupported ratings.
  • Normalize supported string and numeric ratings before calling calculateSM2.
  • Preserve successful review handling for valid ratings.
  • Normalize Adzuna country values in jobController.js.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Flashcard reviews now normalize and validate numeric or labeled ratings before SM-2 processing. Adzuna country configuration and request values are trimmed and lowercased before URL construction.

Changes

Flashcard rating validation

Layer / File(s) Summary
Rating validation and SM-2 input
backend/controllers/flashcardController.js
calculateSM2 accepts numeric ratings and normalized string labels. Review requests distinguish missing, invalid-type, and unsupported ratings before passing the normalized value to SM-2.

Adzuna country normalization

Layer / File(s) Summary
Adzuna country normalization
backend/controllers/jobController.js
The default country and request country are trimmed and lowercased before the Adzuna endpoint URL is built.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: tmdeveloper007, gitguru-sudo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning jobController.js changes normalize Adzuna country values but are unrelated to issue #1199 and flashcard rating validation. Remove the unrelated Adzuna country normalization changes or link them to a separate issue.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes validate missing, invalid, and unsupported ratings before calling calculateSM2, satisfying issue #1199.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: handling invalid rating types and values in reviewFlashcard.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/controllers/flashcardController.js`:
- Around line 5-9: Update calculateSM2 so both numeric 5 and string "5" enter
the existing easy/score-5 branch instead of falling through to score 3; preserve
ALLOWED_RATINGS and add tests covering both accepted forms and their expected
interval/efactor results.
- Around line 192-196: In the review request handler containing the rating
validation and Flashcard.findOne call, validate the incoming id as a valid
ObjectId before querying. Return the existing client-error response for
malformed ids and only execute Flashcard.findOne with validated ids, preserving
the current review flow for valid requests.

In `@backend/controllers/jobController.js`:
- Line 7: Validate ADZUNA_COUNTRY after normalization against
ALLOWED_ADZUNA_COUNTRIES and fall back to the established safe country when
unsupported. Ensure the validated constant is used by fetchFromAdzuna’s default
parameter and any endpoint construction, including the direct caller path that
bypasses getJobs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17f7b5b0-eb80-40f1-b853-978946b4f6ff

📥 Commits

Reviewing files that changed from the base of the PR and between 7d3f898 and 8f030cc.

📒 Files selected for processing (2)
  • backend/controllers/flashcardController.js
  • backend/controllers/jobController.js

Comment on lines +5 to +9
const ALLOWED_RATINGS = new Set([
"again", "hard", "medium", "good", "easy",
"1", "2", "3", "4", "5",
1, 2, 3, 4, 5
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Map rating 5 to the score-5 path.

ALLOWED_RATINGS accepts both "5" and 5, but calculateSM2 has no branch for either value. Both inputs fall through to score = 3, which persists incorrect interval and efactor values for a valid rating. Add both forms to the easy branch and test them.

Proposed fix
-  else if (normalizedRating === "easy" || normalizedRating === "4" || normalizedRating === 4) score = 5;
+  else if (
+    normalizedRating === "easy" ||
+    normalizedRating === "4" ||
+    normalizedRating === 4 ||
+    normalizedRating === "5" ||
+    normalizedRating === 5
+  ) score = 5;

Also applies to: 18-22

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/flashcardController.js` around lines 5 - 9, Update
calculateSM2 so both numeric 5 and string "5" enter the existing easy/score-5
branch instead of falling through to score 3; preserve ALLOWED_RATINGS and add
tests covering both accepted forms and their expected interval/efactor results.

Comment on lines +192 to +196
// 4. Check if rating is in allowed list
if (!ALLOWED_RATINGS.has(normalizedRating)) {
return res.status(400).json({
success: false,
message: "Rating is required. Supported values: 'again', 'hard', 'good', 'easy'.",
message: `Invalid rating '${rating}'. Supported values: 'again', 'hard', 'medium', 'good', 'easy' or numbers 1-5.`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 10 'validateReviewFlashcard|ObjectId\.isValid|isValidObjectId|Flashcard\.findOne|reviewFlashcard' .

Repository: Canopus-Labs/PrepPilot

Length of output: 20637


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- controller review handler ---'
sed -n '162,238p' backend/controllers/flashcardController.js

printf '%s\n' '--- flashcard validator and routes ---'
sed -n '1,45p' backend/Input_validators/ValidateFlashcard.js
sed -n '1,35p' backend/routes/flashcardRoutes.js

printf '%s\n' '--- error handling and app registration ---'
rg -n -C 8 'errorHandler|err, req, res, next|app\.use|flashcardRoutes|/api/flashcards' backend --glob '*.js'

printf '%s\n' '--- dependency versions ---'
rg -n -C 2 '"mongoose"|"express"' backend/package.json package.json 2>/dev/null || true

Repository: Canopus-Labs/PrepPilot

Length of output: 19382


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

controller = Path("backend/controllers/flashcardController.js").read_text()
validator = Path("backend/Input_validators/ValidateFlashcard.js").read_text()

review = controller[controller.index("const reviewFlashcard"):controller.index("const deleteFlashcard")]
query_pos = review.index("Flashcard.findOne({ _id: id, userId })")
prefix = review[:query_pos]

has_id_guard = bool(re.search(
    r"(?:isValidObjectId|ObjectId\.isValid)\s*\(\s*id\s*\)", prefix
))
catch_returns_500 = bool(re.search(
    r"catch\s*\([^)]*\)\s*\{[\s\S]*?res\.status\(500\)",
    review
))
validator_checks_id = bool(re.search(
    r"\bid\b[\s\S]{0,300}(?:isValidObjectId|ObjectId\.isValid)",
    validator
))

print({
    "review_query_present": True,
    "object_id_guard_before_review_query": has_id_guard,
    "review_catch_returns_500": catch_returns_500,
    "review_validator_checks_id": validator_checks_id,
})
assert not has_id_guard
assert catch_returns_500
assert not validator_checks_id
PY

Repository: Canopus-Labs/PrepPilot

Length of output: 308


🌐 Web query:

Mongoose 8 findOne invalid ObjectId cast error CastError query

💡 Result:

In Mongoose 8, a CastError occurring during a findOne or similar query indicates that Mongoose failed to convert a provided value into the data type defined in your schema for that specific path [1][2]. When querying by _id, Mongoose automatically expects an ObjectId; if you provide a string that is not a valid 24-character hex string, Mongoose will throw a CastError before the query is ever sent to MongoDB [3][1]. To resolve or handle this error, you can use the following strategies: 1. Validate the Input: Before executing the query, check if the provided string is a valid ObjectId using mongoose.Types.ObjectId.isValid(id) [4][5][6]. If it is invalid, you can handle it immediately (e.g., return null or a 400 Bad Request error) without running the database operation [6][7]. if (!mongoose.Types.ObjectId.isValid(id)) { return null; // Or handle as an invalid request } const doc = await MyModel.findOne({ _id: id }); 2. Catch the Error: Use standard try/catch blocks with async/await to catch the CastError during execution [7]. Since the error is a specific Mongoose class, you can verify it using instance checks [8][1][2]. try { const doc = await MyModel.findOne({ _id: id }); } catch (err) { if (err instanceof mongoose.Error.CastError) { // Handle the cast error specifically (e.g., return null) } } 3. Schema Configuration (If _id is not an ObjectId): If your _id is intended to be a different type (such as a string or number), ensure your schema explicitly defines it as such [9]. Mongoose will only attempt to cast to ObjectId if the schema path is configured as an ObjectId (which is the default for _id) [9][3]. const schema = new mongoose.Schema({ _id: String // Explicitly set to String to avoid ObjectId casting }); If you are using sanitizeFilter (mongoose.set('sanitizeFilter', true)), be aware that this can sometimes cause unexpected CastErrors when using query operators ($lt, $gt) if the inputs are not properly trusted [10]. In such cases, wrapping the query object or values with mongoose.trusted may be required [10].

Citations:


Preserve ObjectId validation for review requests.

A malformed id reaches Flashcard.findOne({ _id: id, userId }) without validation. Mongoose raises a cast error, and the catch block returns 500. Add an ObjectId check before the query.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/flashcardController.js` around lines 192 - 196, In the
review request handler containing the rating validation and Flashcard.findOne
call, validate the incoming id as a valid ObjectId before querying. Return the
existing client-error response for malformed ids and only execute
Flashcard.findOne with validated ids, preserving the current review flow for
valid requests.

const ADZUNA_APP_ID = process.env.ADZUNA_APP_ID;
const ADZUNA_API_KEY = process.env.ADZUNA_API_KEY;
const ADZUNA_COUNTRY = process.env.ADZUNA_COUNTRY || "in";
const ADZUNA_COUNTRY = (process.env.ADZUNA_COUNTRY || "in").toLowerCase().trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate ADZUNA_COUNTRY before constructing requests.

Line 7 only trims and lowercases the environment value. An unsupported value such as USA becomes usa and remains invalid. The direct fetchFromAdzuna(normalizedRole) caller uses this value through the default parameter, so Lines 38-39 can construct an unsupported Adzuna endpoint without the ALLOWED_ADZUNA_COUNTRIES check in getJobs. Validate the configured value against the allowed list and use a safe validated fallback.

Also applies to: 38-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/controllers/jobController.js` at line 7, Validate ADZUNA_COUNTRY
after normalization against ALLOWED_ADZUNA_COUNTRIES and fall back to the
established safe country when unsupported. Ensure the validated constant is used
by fetchFromAdzuna’s default parameter and any endpoint construction, including
the direct caller path that bypasses getJobs.

@github-actions github-actions Bot added the merge ready PR is mergeable and has no conflicts label Aug 11, 2026
@KaranUnique

Copy link
Copy Markdown
Contributor

@suhaniiz Address coderabbit suggestions

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

Labels

merge ready PR is mergeable and has no conflicts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: [SECURITY] Unhandled TypeError in reviewFlashcard when rating is invalid or missing

2 participants