Skip to content

fix(frontend): safely handle empty or invalid auth JSON responses#274

Open
SoorejS wants to merge 3 commits intoAOSSIE-Org:mainfrom
SoorejS:fix/frontend-auth-safe-json-244
Open

fix(frontend): safely handle empty or invalid auth JSON responses#274
SoorejS wants to merge 3 commits intoAOSSIE-Org:mainfrom
SoorejS:fix/frontend-auth-safe-json-244

Conversation

@SoorejS
Copy link

@SoorejS SoorejS commented Jan 28, 2026

Fixes #244

This PR fixes a frontend crash caused by auth endpoints occasionally
returning empty, non-JSON, or malformed responses, which previously
caused the application to crash during authentication flows.

Why this change was necessary:

The frontend auth layer assumed that all auth endpoints always return
valid JSON and accessed response bodies directly via response.json()
and unchecked property reads (e.g. data.message, data.accessToken).

In real-world error paths (validation failure, auth failure, early
returns), the backend may return:

  • an empty response body
  • a non-JSON response
  • a JSON response missing expected fields

This caused runtime crashes such as:
"Unexpected end of JSON input"

What changed (with code-level reasoning):

  • Introduced safe JSON parsing using a guarded helper (safeJson)
    → Prevents crashes when response.json() fails due to empty or
    invalid response bodies.

  • Removed unsafe double-reading of response bodies
    → Ensures each response body is consumed exactly once, avoiding
    stream consumption errors caused by calling json() and text()
    on the same response.

  • Replaced unsafe property access on unknown data
    → All error fields (message / error) are now accessed only after
    locally casting the response payload, preventing runtime and
    TypeScript errors.

  • Added explicit null and shape checks before accessing auth fields
    → Prevents undefined access when accessToken or user objects are
    missing from error responses.

  • Standardized auth method return values
    → All public auth methods now return a predictable
    { success: boolean } result instead of relying on thrown
    exceptions for control flow.

  • Preserved existing UI behavior and API contracts
    → No changes to backend APIs, UI logic, routing, or response formats.
    Only defensive handling was added.

Scope:

  • Frontend only
  • Limited to frontend/src/context/authContext.tsx
  • No backend changes
  • No UI or routing changes
  • No unrelated refactors

Result:

  • Authentication flows fail safely and predictably
  • Improved resilience against backend edge cases

Summary by CodeRabbit

  • Refactor

    • Improved authentication error handling and response validation across login, signup, password recovery, and email verification.
  • Bug Fixes

    • More reliable parsing of API responses to reduce unexpected errors during auth flows.
  • New Features

    • Client-side form validation, submission states, and disabled inputs during operations for clearer UX.
    • OTP and password-reset flows now show user-friendly error messages and proceed only on successful steps.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

The auth context and authentication forms were updated to safely parse API responses (via safeJson) and standardize API error handling (setApiError). Public auth methods now return { success: boolean } instead of throwing; forms added local submission/error states and client-side validation.

Changes

Cohort / File(s) Summary
Auth Context & API parsing
frontend/src/context/authContext.tsx
Added ApiErrorPayload and AuthPayload types, safeJson() helper, and setApiError() standardized error setter. Refactored auth methods (login, signup, verifyEmail, forgotPassword, googleLogin, confirmForgotPassword) to return Promise<{ success: boolean }> and populate error state instead of throwing. Token verification and profile fetch now use safe parsing; removed handleError from public API.
Auth Forms — client validation & UX
frontend/src/Pages/Authentication/forms.tsx
Introduced per-form localError and isSubmitting state, added client-side validation (email format, password length, password match), disabled inputs during submit, wrapped async flows with try/catch/finally, and updated OTP/reset flows to rely on success booleans. UI text and minor layout tweaks applied.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Client UI
  participant Auth as AuthProvider
  participant API as Backend API
  participant Store as LocalState/Storage

  UI->>Auth: call login(credentials)
  Auth->>API: fetch /login
  API-->>Auth: response (maybe empty/non-JSON)
  Auth->>Auth: safeJson(response) -> payload/null
  alt response.ok and payload valid
    Auth->>Store: set tokens, set user
    Auth-->>UI: { success: true }
  else error or invalid payload
    Auth->>Auth: setApiError(payload, fallback)
    Auth-->>UI: { success: false }
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nibble through responses, soft and slow,

safeJson guards me where wild errors grow.
Booleans now answer, no crash in sight,
OTPs and resets hop through the night.
A tidy burrow for auth—cozy and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes to frontend/src/Pages/Authentication/forms.tsx introduce local error handling, validation logic, and submission state management unrelated to the safe JSON parsing objective. Revert changes in forms.tsx or move form-level validation and error handling to a separate pull request focused on improving form UX rather than JSON response safety.
✅ 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(frontend): safely handle empty or invalid auth JSON responses' directly and clearly summarizes the primary change: introducing safe JSON parsing for auth endpoints.
Linked Issues check ✅ Passed The pull request fully addresses all coding requirements from issue #244: safe JSON parsing with safeJson() helper, fallback handling, null/shape checks, response.ok validation, and preventing frontend crashes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 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

@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

Caution

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

⚠️ Outside diff range comments (1)
frontend/src/context/authContext.tsx (1)

144-163: Clear stale error state at the start of signup.

Unlike login, signup doesn’t reset error, so a previous failure can persist after a successful attempt. Consider mirroring this in the other auth flows too.

🔧 Proposed fix
   const signup = async (email: string, password: string) => {
     setLoading(true);
+    setError(null);
     try {
🤖 Fix all issues with AI agents
In `@frontend/src/context/authContext.tsx`:
- Around line 110-138: The login function currently casts safeJson's result to
AuthPayload and checks payload.accessToken without guarding against null,
causing a runtime TypeError when safeJson returns null; update the runtime
checks in login to treat the parsed data safely (e.g., use data as AuthPayload |
null and check payload?.accessToken or data?.accessToken before throwing) and
ensure you verify payload (or data) is non-null before proceeding to setToken,
localStorage, setUser, and navigate; apply the identical null-guard fix to
googleLogin as well to prevent the same failure when safeJson returns null.

Comment on lines 110 to +138
const login = async (email: string, password: string) => {
setLoading(true);
setError(null);

try {
const response = await fetch(`${baseURL}/login`, {
const res = await fetch(`${baseURL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});

const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Login failed');

setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
// Set user details in userAtom based on the new User type
const normalizedUser: User = {
id: data.user?.id || data.user?._id || undefined,
email: data.user?.email || email,
displayName: data.user?.displayName || 'User',
bio: data.user?.bio || '',
rating: data.user?.rating || 1500,
rd: data.user?.rd || 350, // Default Glicko-2 RD value
volatility: data.user?.volatility || 0.06, // Default Glicko-2 volatility
lastRatingUpdate:
data.user?.lastRatingUpdate || new Date().toISOString(),
avatarUrl:
data.user?.avatarUrl || 'https://avatar.iran.liara.run/public/10',
twitter: data.user?.twitter || undefined,
instagram: data.user?.instagram || undefined,
linkedin: data.user?.linkedin || undefined,
password: '', // Password should not be stored in client-side state
nickname: data.user?.nickname || 'User',
isVerified: data.user?.isVerified || false,
verificationCode: data.user?.verificationCode || undefined,
resetPasswordCode: data.user?.resetPasswordCode || undefined,
createdAt: data.user?.createdAt || new Date().toISOString(),
updatedAt: data.user?.updatedAt || new Date().toISOString(),
};
setUser(normalizedUser);
localStorage.setItem(USER_CACHE_KEY, JSON.stringify(normalizedUser));
const data = await safeJson(res);
if (!res.ok) setApiError(data, 'Login failed');

const payload = data as AuthPayload;
if (!payload.accessToken || !payload.user?.email) {
throw new Error('Invalid auth payload');
}

setToken(payload.accessToken);
localStorage.setItem('token', payload.accessToken);
setUser(payload.user);
localStorage.setItem(USER_CACHE_KEY, JSON.stringify(payload.user));
navigate('/');
} catch (error) {
handleError(error);

return { success: true };
} catch (err) {
setError(err instanceof Error ? err.message : 'Login failed');
return { success: false };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its size first
wc -l frontend/src/context/authContext.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 103


🏁 Script executed:

# Search for safeJson function definition
rg "safeJson" frontend/src/ -B 2 -A 5

Repository: AOSSIE-Org/DebateAI

Length of output: 4891


🏁 Script executed:

# Search for AuthPayload type definition
rg "AuthPayload" frontend/src/ -B 2 -A 5

Repository: AOSSIE-Org/DebateAI

Length of output: 1607


🏁 Script executed:

# Read lines 110-140 to see the login function
sed -n '110,140p' frontend/src/context/authContext.tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 1040


🏁 Script executed:

# Check if the same pattern exists in googleLogin
rg "const payload = data as AuthPayload" frontend/src/context/authContext.tsx -B 5 -A 8

Repository: AOSSIE-Org/DebateAI

Length of output: 998


Use optional chaining to guard against null payload from safeJson.

When safeJson fails to parse JSON, it returns null. The TypeScript cast to AuthPayload doesn't protect against this at runtime. If data is null, accessing payload.accessToken throws TypeError: Cannot read properties of null instead of the intended error message. The second condition already uses optional chaining, but the first does not.

🔧 Proposed fix
       const payload = data as AuthPayload;
-      if (!payload.accessToken || !payload.user?.email) {
+      if (!payload?.accessToken || !payload?.user?.email) {
         throw new Error('Invalid auth payload');
       }

Apply the same fix to the googleLogin function, which has the identical vulnerability.

🤖 Prompt for AI Agents
In `@frontend/src/context/authContext.tsx` around lines 110 - 138, The login
function currently casts safeJson's result to AuthPayload and checks
payload.accessToken without guarding against null, causing a runtime TypeError
when safeJson returns null; update the runtime checks in login to treat the
parsed data safely (e.g., use data as AuthPayload | null and check
payload?.accessToken or data?.accessToken before throwing) and ensure you verify
payload (or data) is non-null before proceeding to setToken, localStorage,
setUser, and navigate; apply the identical null-guard fix to googleLogin as well
to prevent the same failure when safeJson returns null.

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

🤖 Fix all issues with AI agents
In `@frontend/src/Pages/Authentication/forms.tsx`:
- Around line 395-409: The current flow calls confirmForgotPassword(email, code,
newPassword) and then unconditionally calls login(email, newPassword) and
handlePasswordReset(), but the new auth contract returns { success: boolean } so
you must check those results; update the flow to await confirmForgotPassword and
verify its returned object's success is true before calling login, and similarly
await login(...) and only call handlePasswordReset() when login returns success;
if either returns { success: false } setLocalError with an appropriate message
and return to avoid false-positive success.

Comment on lines 395 to 409
setLocalError(null);

if (newPassword !== confirmNewPassword) {
authContext.handleError('Passwords do not match');
setLocalError('Passwords do not match');
return;
}

await confirmForgotPassword(email, code, newPassword);
await login(email, newPassword);
handlePasswordReset();
try {
await confirmForgotPassword(email, code, newPassword);
await login(email, newPassword);
handlePasswordReset();
} catch (err) {
console.error('Password reset error:', err);
setLocalError('Failed to reset password. Please try again.');
}
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

Handle { success } results to avoid false-positive reset success.
With the new auth contract returning { success: boolean }, this flow will continue even if reset/login fails but doesn’t throw. It should gate login and handlePasswordReset() on success flags.

✅ Suggested fix
-    try {
-      await confirmForgotPassword(email, code, newPassword);
-      await login(email, newPassword);
-      handlePasswordReset();
-    } catch (err) {
+    try {
+      const { success: resetSuccess } = await confirmForgotPassword(email, code, newPassword);
+      if (!resetSuccess) {
+        setLocalError('Failed to reset password. Please try again.');
+        return;
+      }
+      const { success: loginSuccess } = await login(email, newPassword);
+      if (!loginSuccess) {
+        setLocalError('Password reset succeeded, but login failed. Please sign in again.');
+        return;
+      }
+      handlePasswordReset();
+    } catch (err) {
       console.error('Password reset error:', err);
       setLocalError('Failed to reset password. Please try again.');
     }
📝 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
setLocalError(null);
if (newPassword !== confirmNewPassword) {
authContext.handleError('Passwords do not match');
setLocalError('Passwords do not match');
return;
}
await confirmForgotPassword(email, code, newPassword);
await login(email, newPassword);
handlePasswordReset();
try {
await confirmForgotPassword(email, code, newPassword);
await login(email, newPassword);
handlePasswordReset();
} catch (err) {
console.error('Password reset error:', err);
setLocalError('Failed to reset password. Please try again.');
}
setLocalError(null);
if (newPassword !== confirmNewPassword) {
setLocalError('Passwords do not match');
return;
}
try {
const { success: resetSuccess } = await confirmForgotPassword(email, code, newPassword);
if (!resetSuccess) {
setLocalError('Failed to reset password. Please try again.');
return;
}
const { success: loginSuccess } = await login(email, newPassword);
if (!loginSuccess) {
setLocalError('Password reset succeeded, but login failed. Please sign in again.');
return;
}
handlePasswordReset();
} catch (err) {
console.error('Password reset error:', err);
setLocalError('Failed to reset password. Please try again.');
}
🤖 Prompt for AI Agents
In `@frontend/src/Pages/Authentication/forms.tsx` around lines 395 - 409, The
current flow calls confirmForgotPassword(email, code, newPassword) and then
unconditionally calls login(email, newPassword) and handlePasswordReset(), but
the new auth contract returns { success: boolean } so you must check those
results; update the flow to await confirmForgotPassword and verify its returned
object's success is true before calling login, and similarly await login(...)
and only call handlePasswordReset() when login returns success; if either
returns { success: false } setLocalError with an appropriate message and return
to avoid false-positive success.

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.

Frontend crashes when backend returns non-JSON or empty responses

1 participant

Comments