Skip to content

Fix/google form response contract#617

Open
Ashvin-KS wants to merge 2 commits intoAOSSIE-Org:mainfrom
Ashvin-KS:fix/google-form-response-contract
Open

Fix/google form response contract#617
Ashvin-KS wants to merge 2 commits intoAOSSIE-Org:mainfrom
Ashvin-KS:fix/google-form-response-contract

Conversation

@Ashvin-KS
Copy link
Contributor

@Ashvin-KS Ashvin-KS commented Mar 21, 2026

Addressed Issues:

Fixes #616

Screenshots/Recordings:

N/A (no visual UI redesign).
This PR is a reliability and API contract fix for Google Form generation flow across backend + clients.

Additional Notes:

  • This PR is intentionally scoped to one bugfix: normalize /generate_gform response contract and make consumers backward-compatible.
  • Backend now returns:
    • form_link
    • edit_link
  • Client-side logic supports both:
    • New object response (form_link)
    • Legacy string response fallback
  • Added backend payload guard: returns 400 when qa_pairs is not a list.
  • Removed server-side browser open side effect from backend.

AI Usage Disclosure:

  • This PR does not contain AI-generated code at all.
  • This PR contains AI-generated code. I have read the AI Usage Policy and this PR complies with this policy. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools: GPT-5.3-Codex (GitHub Copilot) for drafting/refinement; all changes were reviewed and validated before submission.

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

Summary by CodeRabbit

Release Notes

  • New Features

    • Google Forms generation now returns both a shareable form link and an edit link.
  • Bug Fixes

    • Improved error handling when Google Form URLs are unavailable, preventing crashes with graceful fallback behavior.
    • Enhanced data loading reliability with fallback recovery on parsing failures.
    • Added input validation for form generation requests.

Copilot AI review requested due to automatic review settings March 21, 2026 03:28
@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

The changes standardize the /generate_gform endpoint's response contract to consistently return form_link and edit_link as structured JSON, remove browser-opening side effects, add input validation for qa_pairs, and harden client-side URL parsing with backward-compatible handling across web and extension frontends.

Changes

Cohort / File(s) Summary
Backend API Contract
backend/server.py
Updated /generate_gform endpoint to default request.get_json() to {}, validate qa_pairs as a list (return HTTP 400 if not), and return standardized JSON with both form_link and edit_link instead of opening a browser and returning only responderUri.
Web Client URL Handling
eduaid_web/src/pages/Output.jsx
Added defensive try/catch for localStorage JSON parsing with fallback to empty object, refactored combinedQaPairs construction with Array.isArray validation, updated useEffect dependency list to re-run on questionType changes, and hardened Google Form URL handling by validating result.form_link presence before opening.
Extension Client URL Normalization
extension/src/pages/question/Question.jsx, extension/src/pages/question/SidePanel.jsx
Implemented backward-compatible API response parsing that accepts either result.form_link (object) or result (string), added validation guards to detect and log missing/invalid URLs, and return early to prevent opening undefined URLs.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Web as Web/Extension Client
    participant Backend as Backend Server
    participant Browser

    User->>Web: Click "Generate Google Form"
    Web->>Web: Validate qa_pairs input
    Web->>Backend: POST /generate_gform (qa_pairs list)
    alt Invalid Input
        Backend-->>Web: HTTP 400 (error JSON)
        Web-->>User: Display error
    else Valid Input
        Backend->>Backend: Process QA pairs
        Backend->>Backend: Generate Form (responderUri)
        Backend-->>Web: JSON {form_link, edit_link}
        Web->>Web: Parse response (backward-compatible)
        alt URL Present & Valid
            Web->>Browser: window.open(form_link)
            Browser-->>User: Open Google Form
        else URL Missing/Invalid
            Web-->>User: Log error & display message
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Hops with glee through API gates,
Response schemas now coordinate!
No more browser side-effects creep,
Forms come back with data deep,
Validation guards our sanity,
URL parsing, sane and grand!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/google form response contract' directly summarizes the main change: normalizing the backend response structure and adding validation to fix the API contract inconsistency described in issue #616.
Linked Issues check ✅ Passed All objectives from issue #616 are addressed: backend now returns normalized JSON with form_link and edit_link, input validation for qa_pairs is added, server-side browser tab effect is removed, and clients handle both legacy and new response formats.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the /generate_gform response contract. The modifications to defensive parsing and URL handling in client files are necessary to support the new API contract and ensure reliability.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Normalizes the /generate_gform API response contract in the Flask backend and updates web/extension consumers to be backward-compatible with both the new object shape and the legacy string response, improving reliability of the Google Form generation flow.

Changes:

  • Backend: return { form_link, edit_link }, remove server-side browser-opening side effect, and validate qa_pairs is a list (400 otherwise).
  • Clients (web + extension): accept either {form_link: ...} or legacy string response and avoid opening when URL is missing.
  • Web: harden qaPairs localStorage parsing and array-shape handling when assembling qaPairs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
extension/src/pages/question/SidePanel.jsx Adds backward-compatible parsing of /generate_gform response before opening the form link.
extension/src/pages/question/Question.jsx Adds backward-compatible parsing of /generate_gform response before opening the form link.
eduaid_web/src/pages/Output.jsx Hardens localStorage parsing + normalizes QA arrays; adds backward-compatible parsing for form link opening.
backend/server.py Normalizes /generate_gform JSON response, adds qa_pairs type guard, and removes webbrowser side effect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +118 to +125
const formUrl =
(result && result.form_link) ||
(typeof result === "string" ? result : null);

if (!formUrl) {
console.error("Google Form URL missing in API response", result);
return;
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The response-shape normalization logic is duplicated here (and in other clients). To avoid the extension/web drifting on future contract tweaks (e.g., adding edit_link or changing legacy handling), consider extracting a small helper (e.g., getGFormLinkFromResponse(result)) and reusing it across the three call sites.

Copilot uses AI. Check for mistakes.
return;
}

window.open(formUrl, "_blank");
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

window.open(formUrl, "_blank") can allow reverse-tabnabbing via window.opener. Prefer opening with noopener,noreferrer (or explicitly nulling opener) so the newly opened page can't navigate the extension page.

Suggested change
window.open(formUrl, "_blank");
const newWindow = window.open(formUrl, "_blank", "noopener,noreferrer");
if (newWindow) {
newWindow.opener = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +122
const formUrl =
(result && result.form_link) ||
(typeof result === "string" ? result : null);

if (!formUrl) {
console.error("Google Form URL missing in API response", result);
return;
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

The legacy/new response handling is repeated across multiple components. Consider factoring this into a shared helper so any future API contract adjustments (e.g., preferring edit_link in some flows) only need to be made once.

Copilot uses AI. Check for mistakes.
return;
}

window.open(formUrl, "_blank");
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

window.open(..., "_blank") should be opened with noopener/noreferrer (or newWindow.opener = null) to prevent reverse-tabnabbing from a potentially attacker-controlled URL.

Suggested change
window.open(formUrl, "_blank");
const newWindow = window.open(formUrl, "_blank", "noopener,noreferrer");
if (newWindow) {
newWindow.opener = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +182 to +190
const formUrl =
(result && result.form_link) ||
(typeof result === "string" ? result : null);

if (!formUrl) {
console.error("Google Form URL missing in API response", result);
return;
}

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

This response parsing pattern is now duplicated in multiple clients. Consider extracting it into a small utility (web-side) so the /generate_gform response contract is handled consistently and changes are easier to roll out.

Copilot uses AI. Check for mistakes.
return;
}

window.open(formUrl, "_blank");
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

Opening a URL in a new tab with target=_blank should include noopener/noreferrer (or opener = null) to avoid reverse-tabnabbing. This is especially relevant since the URL comes from an API response.

Suggested change
window.open(formUrl, "_blank");
const newWindow = window.open(formUrl, "_blank", "noopener,noreferrer");
if (newWindow) {
newWindow.opener = null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +271
data = request.get_json() or {}
qa_pairs = data.get("qa_pairs", [])
question_type = data.get("question_type", "")

if not isinstance(qa_pairs, list):
return jsonify({"error": "qa_pairs must be a list"}), 400

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

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

There’s now new behavior to validate payload shape (qa_pairs must be a list) and return a 400. Since the repo has endpoint-level tests in backend/test_server.py, please add coverage for this branch (at least the 400 on non-list qa_pairs, which avoids any external Google API calls).

Copilot uses AI. Check for mistakes.
Copy link

@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

🧹 Nitpick comments (2)
eduaid_web/src/pages/Output.jsx (1)

129-149: Add deduping when merging MCQ arrays from both sources.

This block appends output_mcq.questions and output directly; overlapping items can render twice and create unstable ordering. Add a deterministic dedupe key before setQaPairs.

Example dedupe pass
-      setQaPairs(combinedQaPairs);
+      const seen = new Set();
+      const uniqueQaPairs = combinedQaPairs.filter((item) => {
+        const key = `${item.question_type}::${item.question}::${item.answer ?? ""}`;
+        if (seen.has(key)) return false;
+        seen.add(key);
+        return true;
+      });
+      setQaPairs(uniqueQaPairs);

Based on learnings: In Output.jsx, when multiple question types are generated, output_mcq.questions and output should be coordinated without duplicates or order inconsistencies.

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

In `@eduaid_web/src/pages/Output.jsx` around lines 129 - 149, The merging of MCQ
entries appends outputMcqQuestions and output into combinedQaPairs without
deduplication, causing duplicate questions and unstable ordering; update the
merge logic that pushes into combinedQaPairs (the block referencing
outputMcqQuestions, qaPairsFromStorage["output_mcq"] / questionType ===
"get_mcq", and output) to perform a deterministic dedupe before calling
setQaPairs: compute a stable dedupe key (e.g., normalized question text plus
serialized options or question_statement) for each pushed object, track seen
keys in a Set to skip duplicates, preserve the desired sort/order
deterministically (e.g., push from storage first then generated, or sort by the
dedupe key) and finally call setQaPairs with the filtered combined array.
backend/server.py (1)

429-431: Harden link construction against missing Google API fields.

Line 430 uses unsafe bracket access to result['formId'] which will throw a KeyError if the upstream API response lacks this field. The Google Forms API documentation confirms that formId and responderUri are not guaranteed in all successful responses, particularly responderUri for unpublished forms. Switch to safe .get() extraction and return a controlled error response for incomplete API data.

Proposed hardening
-    form_link = result.get("responderUri")
-    edit_link = f"https://docs.google.com/forms/d/{result['formId']}/edit"
-    return jsonify({"form_link": form_link, "edit_link": edit_link})
+    form_link = result.get("responderUri")
+    form_id = result.get("formId")
+    if not form_id or not form_link:
+        return jsonify({"error": "Failed to create Google Form links"}), 502
+    edit_link = f"https://docs.google.com/forms/d/{form_id}/edit"
+    return jsonify({"form_link": form_link, "edit_link": edit_link})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/server.py` around lines 429 - 431, The code currently assumes
result['formId'] and responderUri exist and will KeyError; change direct bracket
access to safe .get() calls (use form_id = result.get("formId") and
responder_uri = result.get("responderUri")), validate that form_id (and
responder_uri if required) is present, and if missing return a controlled JSON
error via jsonify (e.g., jsonify({"error":"missing formId or responderUri"})
with appropriate status code) instead of constructing edit_link unconditionally;
update where edit_link is built (edit_link =
f"https://docs.google.com/forms/d/{form_id}/edit") and only return {"form_link":
responder_uri, "edit_link": edit_link} when validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/server.py`:
- Around line 265-267: The handler currently assumes request.get_json() returns
an object and directly calls data.get(...), which fails for JSON arrays/strings;
validate that the parsed JSON (the variable data) is a dict before using .get.
Update the route handler around the request.get_json() call to check
isinstance(data, dict) (or equivalent) and return a clear 400/BadRequest when it
is not an object, then safely access qa_pairs and question_type only after that
check.

---

Nitpick comments:
In `@backend/server.py`:
- Around line 429-431: The code currently assumes result['formId'] and
responderUri exist and will KeyError; change direct bracket access to safe
.get() calls (use form_id = result.get("formId") and responder_uri =
result.get("responderUri")), validate that form_id (and responder_uri if
required) is present, and if missing return a controlled JSON error via jsonify
(e.g., jsonify({"error":"missing formId or responderUri"}) with appropriate
status code) instead of constructing edit_link unconditionally; update where
edit_link is built (edit_link =
f"https://docs.google.com/forms/d/{form_id}/edit") and only return {"form_link":
responder_uri, "edit_link": edit_link} when validated.

In `@eduaid_web/src/pages/Output.jsx`:
- Around line 129-149: The merging of MCQ entries appends outputMcqQuestions and
output into combinedQaPairs without deduplication, causing duplicate questions
and unstable ordering; update the merge logic that pushes into combinedQaPairs
(the block referencing outputMcqQuestions, qaPairsFromStorage["output_mcq"] /
questionType === "get_mcq", and output) to perform a deterministic dedupe before
calling setQaPairs: compute a stable dedupe key (e.g., normalized question text
plus serialized options or question_statement) for each pushed object, track
seen keys in a Set to skip duplicates, preserve the desired sort/order
deterministically (e.g., push from storage first then generated, or sort by the
dedupe key) and finally call setQaPairs with the filtered combined array.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc7b7da6-3b2b-434e-b17c-40c3e5336e02

📥 Commits

Reviewing files that changed from the base of the PR and between 2038116 and 1dbfb4b.

📒 Files selected for processing (4)
  • backend/server.py
  • eduaid_web/src/pages/Output.jsx
  • extension/src/pages/question/Question.jsx
  • extension/src/pages/question/SidePanel.jsx

Comment on lines +265 to 267
data = request.get_json() or {}
qa_pairs = data.get("qa_pairs", [])
question_type = data.get("question_type", "")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate JSON root type before calling .get().

At Line 265–266, a JSON array/string payload will make data.get(...) fail with 500 instead of returning a clear 400. Add an object-shape check first.

Proposed fix
-    data = request.get_json() or {}
+    data = request.get_json(silent=True) or {}
+    if not isinstance(data, dict):
+        return jsonify({"error": "Request body must be a JSON object"}), 400
     qa_pairs = data.get("qa_pairs", [])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data = request.get_json() or {}
qa_pairs = data.get("qa_pairs", [])
question_type = data.get("question_type", "")
data = request.get_json(silent=True) or {}
if not isinstance(data, dict):
return jsonify({"error": "Request body must be a JSON object"}), 400
qa_pairs = data.get("qa_pairs", [])
question_type = data.get("question_type", "")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/server.py` around lines 265 - 267, The handler currently assumes
request.get_json() returns an object and directly calls data.get(...), which
fails for JSON arrays/strings; validate that the parsed JSON (the variable data)
is a dict before using .get. Update the route handler around the
request.get_json() call to check isinstance(data, dict) (or equivalent) and
return a clear 400/BadRequest when it is not an object, then safely access
qa_pairs and question_type only after that check.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Fix inconsistent /generate_gform response contract causing undefined form URL in web/extension clients

2 participants