Skip to content

Conversation

@Shantanu675
Copy link

@Shantanu675 Shantanu675 commented Jan 26, 2026

Description

Fixes #244 - Auth endpoints sometimes return empty response body

Changes Made:

  • Updated /signup endpoint to always return JSON on error
  • Updated /login endpoint to always return JSON on error
  • Added consistent error response format
  • Fixed frontend crash "Unexpected end of JSON input"

Root Cause:

Certain error paths (validation failure, user exists, password mismatch) were exiting without calling c.JSON(), resulting in empty responses.

Testing:

  1. Tested signup with duplicate email → Returns {error: "user already exists"}
  2. Tested login with wrong password → Returns {error: "invalid credentials"}
  3. Tested validation errors → Returns proper JSON error
  4. Frontend no longer crashes with "Unexpected end of JSON input"

Impact:

  • ✅ Frontend can handle auth errors properly
  • ✅ Better developer experience
  • ✅ Consistent API response format

Summary by CodeRabbit

  • New Features

    • Added email verification functionality for account signup
    • Added password recovery feature with verification codes
    • Enhanced Google login integration
  • Bug Fixes

    • Improved email handling with consistent normalization across authentication flows
    • Strengthened password verification security by reordering verification checks
  • Style

    • Adjusted Google Sign-In button layout

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

- Fix /signup endpoint to return JSON on validation errors
- Fix /login endpoint to return JSON on password mismatch
- Add consistent error response format
- Resolves AOSSIE-Org#244
- Fix /signup endpoint to return JSON on validation errors
- Fix /login endpoint to return JSON on password mismatch
- Add consistent error response format
- Resolves AOSSIE-Org#244
@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

This PR addresses authentication robustness by introducing email normalization in the backend, reordering password verification before status checks, implementing safe JSON parsing to prevent frontend crashes on invalid API responses, and adding new auth context methods (verifyEmail, forgotPassword, confirmForgotPassword, googleLogin). Minor UI and routing adjustments accompany the core changes.

Changes

Cohort / File(s) Summary
Backend Authentication Improvements
backend/.gitignore, backend/controllers/auth.go
Added tar.gz to ignore rules; enhanced SignUp, VerifyEmail, and Login handlers with email normalization (trim, lowercase), restructured Login error response formatting, reordered password verification to occur before email status checks, and improved database query readability.
Frontend Auth Context Refactoring
frontend/src/context/authContext.tsx
Introduced safeJson helper for safe JSON parsing and normalizeUser for consistent user data mapping; reworked token verification, login/signup flows to handle non-JSON responses gracefully; changed handleError signature from (error: string) => void to (error: unknown) => never; added public auth methods (verifyEmail, forgotPassword, confirmForgotPassword, googleLogin); improved user state and token caching consistency.
Frontend UI and Routing
frontend/src/App.tsx, frontend/src/Pages/Authentication/forms.tsx
Removed duplicate coach route and unused public /coach route; adjusted Google Sign-In button styling by removing explicit width property.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Frontend<br/>(Browser)
    participant Server as Backend<br/>(API)
    participant Storage as LocalStorage<br/>(Cache)
    
    rect rgba(100, 150, 200, 0.5)
    Note over Client,Storage: Traditional Flow (Unsafe)
    Client->>Server: POST /auth/login
    Server-->>Client: Response (may be empty or non-JSON)
    Client->>Client: ❌ response.json() crashes<br/>on invalid/empty body
    end
    
    rect rgba(100, 200, 100, 0.5)
    Note over Client,Storage: New Flow (Safe)
    Client->>Server: POST /auth/login
    Server-->>Client: Response
    Client->>Client: ✅ safeJson() safely parses<br/>or returns fallback
    alt Success Path
        Client->>Client: normalizeUser() maps to User type
        Client->>Storage: Store token & user
        Client->>Client: Update authContext state
    else Error Path
        Client->>Client: Extract error message safely
        Client->>Client: handleError(unknown) throws
        Client->>Storage: Clear cached data on failure
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Poem

🐰 No more crashes when things go wrong,
Safe JSON parsing makes auth strong,
Normalized emails, verified with care,
Graceful errors everywhere! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several changes appear out of scope: removal of /coach public route, removal of duplicate coach route, and Google Sign-In button width change are unrelated to the auth JSON response objective. Remove out-of-scope changes: coach route removal and Google Sign-In UI modification. Keep only auth-related JSON response improvements.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR: ensuring auth endpoints return valid JSON responses, which aligns with the primary changes in backend/controllers/auth.go.
Linked Issues check ✅ Passed The PR addresses the backend objective from issue #244 by ensuring auth endpoints return JSON responses. Frontend changes improve error handling, but frontend-specific improvements suggested in issue #244 are only partially addressed.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 4

🤖 Fix all issues with AI agents
In `@frontend/src/context/authContext.tsx`:
- Around line 204-208: The code calls safeJson(res) and then uses
data.accessToken without verifying data is non-null; update the authentication
flow in the function containing safeJson, setToken, and localStorage.setItem to
check that data is not null and that data.accessToken exists after awaiting
safeJson(res); if data is null or lacks accessToken, throw a descriptive Error
(e.g., "Invalid JSON response" or "Missing accessToken") instead of proceeding,
and only call setToken(data.accessToken) and localStorage.setItem('token',
data.accessToken) when the checks pass.
- Around line 274-278: In googleLogin, safeJson(res) can return null so using
data.accessToken will throw; add a null check after const data = await
safeJson(res) and before setToken/localStorage.setItem to throw or handle an
error when data is null (similar to verifyEmail), e.g., if (!data) throw new
Error('Invalid response from server') or include data?.message in the error;
ensure setToken and localStorage.setItem('token', data.accessToken) only run
when data is non-null and contains accessToken.
- Around line 139-151: The response body is being consumed twice: safeJson(res)
already reads the body into data, then the code calls res.text() again and
incorrectly shadows data; fix by removing the second body read and use the
parsed value returned by safeJson instead—inside the block that checks res.ok,
inspect the existing data variable (from safeJson) for message or error fields
and build the fallback message ('Invalid email or password') from that, and
remove the commented-out line that redeclares/shadows data so you don't re-read
or shadow the response payload.
- Around line 87-92: The handleError function signature was changed to return
never and re-throws errors, which breaks callers like SignUpForm.handleSubmit
that expect handleError to set error state via setError and allow normal flow;
revert handleError to return void and stop re-throwing so it only normalizes the
message and calls setError, and ensure any places that need to propagate an
actual Error (inside try/catch blocks) explicitly throw an Error object instead
of a string; update the function referenced as handleError in authContext.tsx
and adjust callers (e.g., SignUpForm.handleSubmit) to only rely on setError for
validation messages while preserving existing try/catch throw behavior where
appropriate.
🧹 Nitpick comments (3)
frontend/src/context/authContext.tsx (2)

52-73: Default rating value inconsistent with backend.

The backend initializes users with rating: 1200.0 (line 83 in auth.go), but normalizeUser defaults to 1500. Consider aligning these values.

Suggested fix
 const normalizeUser = (data: any, fallbackEmail?: string): User => ({
   id: data?.id || data?._id,
   email: data?.email || fallbackEmail || '',
   displayName: data?.displayName || 'User',
   bio: data?.bio || '',
-  rating: data?.rating || 1500,
+  rating: data?.rating ?? 1200,
   rd: data?.rd || 350,
   volatility: data?.volatility || 0.06,

119-122: Missing setUser dependency in useCallback.

verifyToken references setUser (via the closure) but it's not listed in the dependency array. While setUser from useSetAtom is stable, adding it explicitly is the idiomatic pattern and avoids lint warnings.

Suggested fix
-  }, []);
+  }, [setUser]);
frontend/src/Pages/Authentication/forms.tsx (1)

62-66: Inconsistent button width between LoginForm and SignUpForm.

The width: '100%' property was removed from the Google Sign-In button in LoginForm, but SignUpForm (lines 174-179) still includes it. This may cause visual inconsistency between the login and signup pages.

Was this intentional, or should both forms use the same styling?

Comment on lines +87 to 92
const handleError = (err: unknown): never => {
const message =
error instanceof Error ? error.message : 'An unexpected error occurred';
err instanceof Error ? err.message : 'Something went wrong';
setError(message);
throw error;
throw err;
};
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

🧩 Analysis chain

🏁 Script executed:

# First, let's find all usages of handleError
rg -n "handleError" --type=ts --type=tsx -A2 -B2

Repository: AOSSIE-Org/DebateAI

Length of output: 90


🏁 Script executed:

# Also examine the authContext.tsx file to see the full context
fd "authContext" --type=tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 233


🏁 Script executed:

# Let's also check the forms.tsx file mentioned in the review
fd "forms" --type=tsx

Repository: AOSSIE-Org/DebateAI

Length of output: 233


🏁 Script executed:

# Find the files mentioned in the review
fd -e tsx -e ts | grep -E "(authContext|forms)"

Repository: AOSSIE-Org/DebateAI

Length of output: 143


🏁 Script executed:

# Search for handleError usages with correct syntax
rg "handleError" -A2 -B2

Repository: AOSSIE-Org/DebateAI

Length of output: 3448


🏁 Script executed:

# Read authContext.tsx to see the full context around handleError
cat -n frontend/src/context/authContext.tsx | sed -n '80,100p'

Repository: AOSSIE-Org/DebateAI

Length of output: 772


🏁 Script executed:

# Read forms.tsx to see the handleError usages
cat -n frontend/src/Pages/Authentication/forms.tsx | head -200

Repository: AOSSIE-Org/DebateAI

Length of output: 7143


🏁 Script executed:

# Count total handleError usages to ensure we found them all
rg "handleError" --count-matches

Repository: AOSSIE-Org/DebateAI

Length of output: 147


🏁 Script executed:

# Check the second handleError usage in forms.tsx
sed -n '140,250p' frontend/src/Pages/Authentication/forms.tsx | grep -n "handleError" -A3 -B3

Repository: AOSSIE-Org/DebateAI

Length of output: 219


handleError signature change breaks validation error handling in SignUpForm.

The return type changed from void to never, and the function now re-throws errors. In SignUpForm.handleSubmit (line 144 in forms.tsx), the validation error 'Passwords do not match' is passed to handleError, which will now throw instead of setting the error state and allowing normal control flow. Since handleSubmit is async, this creates an unhandled promise rejection. Additionally, throwing a string violates error handling best practices — only Error objects should be thrown.

The calls within authContext.tsx itself (inside try-catch blocks) are safe, but the public API change breaks existing form validation patterns.

🤖 Prompt for AI Agents
In `@frontend/src/context/authContext.tsx` around lines 87 - 92, The handleError
function signature was changed to return never and re-throws errors, which
breaks callers like SignUpForm.handleSubmit that expect handleError to set error
state via setError and allow normal flow; revert handleError to return void and
stop re-throwing so it only normalizes the message and calls setError, and
ensure any places that need to propagate an actual Error (inside try/catch
blocks) explicitly throw an Error object instead of a string; update the
function referenced as handleError in authContext.tsx and adjust callers (e.g.,
SignUpForm.handleSubmit) to only rely on setError for validation messages while
preserving existing try/catch throw behavior where appropriate.

Comment on lines +139 to +151
const data = await safeJson(res);
// if (!res.ok) throw new Error(data?.message || 'Login failed');

if (!res.ok) {
let message = 'Invalid email or password';

try {
const data = JSON.parse(await res.text());
message = data.message || data.error || message;
} catch {}

throw new Error(message);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Response body consumed twice causes failure.

On line 139, safeJson(res) reads and consumes the response body. On line 146, res.text() attempts to read it again, but the body stream has already been consumed. This will either throw an error or return an empty string.

Additionally, line 140 has a commented-out line that shadows the data variable declared on line 139.

Proposed fix
-      const data = await safeJson(res);
-      // if (!res.ok) throw new Error(data?.message || 'Login failed');
-
       if (!res.ok) {
-        let message = 'Invalid email or password';
-      
-        try {
-          const data = JSON.parse(await res.text());
-          message = data.message || data.error || message;
-        } catch {}
-      
-        throw new Error(message);
+        const data = await safeJson(res);
+        const message = data?.message || data?.error || 'Invalid email or password';
+        throw new Error(message);
       }
-      
+
+      const data = await safeJson(res);
+      if (!data) throw new Error('Invalid response from server');
+
       setToken(data.accessToken);
📝 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
const data = await safeJson(res);
// if (!res.ok) throw new Error(data?.message || 'Login failed');
if (!res.ok) {
let message = 'Invalid email or password';
try {
const data = JSON.parse(await res.text());
message = data.message || data.error || message;
} catch {}
throw new Error(message);
}
if (!res.ok) {
const data = await safeJson(res);
const message = data?.message || data?.error || 'Invalid email or password';
throw new Error(message);
}
const data = await safeJson(res);
if (!data) throw new Error('Invalid response from server');
setToken(data.accessToken);
🤖 Prompt for AI Agents
In `@frontend/src/context/authContext.tsx` around lines 139 - 151, The response
body is being consumed twice: safeJson(res) already reads the body into data,
then the code calls res.text() again and incorrectly shadows data; fix by
removing the second body read and use the parsed value returned by safeJson
instead—inside the block that checks res.ok, inspect the existing data variable
(from safeJson) for message or error fields and build the fallback message
('Invalid email or password') from that, and remove the commented-out line that
redeclares/shadows data so you don't re-read or shadow the response payload.

Comment on lines +204 to +208
const data = await safeJson(res);
if (!res.ok) throw new Error(data?.error || 'Verification failed');

const data = await response.json();

// User is now verified and logged in
if (data.accessToken) {
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);

// Set user details
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 || 1200,
rd: data.user?.rd || 350,
volatility: data.user?.volatility || 0.06,
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: '',
nickname: data.user?.nickname || 'User',
isVerified: true,
verificationCode: undefined,
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));
navigate('/');
}
} catch (error) {
handleError(error);
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
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

Missing null check after safeJson.

safeJson can return null if the response isn't JSON. If data is null, accessing data.accessToken on line 207 will throw a TypeError.

Proposed fix
       const data = await safeJson(res);
       if (!res.ok) throw new Error(data?.error || 'Verification failed');
+      if (!data) throw new Error('Invalid response from server');

       setToken(data.accessToken);
📝 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
const data = await safeJson(res);
if (!res.ok) throw new Error(data?.error || 'Verification failed');
const data = await response.json();
// User is now verified and logged in
if (data.accessToken) {
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
// Set user details
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 || 1200,
rd: data.user?.rd || 350,
volatility: data.user?.volatility || 0.06,
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: '',
nickname: data.user?.nickname || 'User',
isVerified: true,
verificationCode: undefined,
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));
navigate('/');
}
} catch (error) {
handleError(error);
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
const data = await safeJson(res);
if (!res.ok) throw new Error(data?.error || 'Verification failed');
if (!data) throw new Error('Invalid response from server');
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
🤖 Prompt for AI Agents
In `@frontend/src/context/authContext.tsx` around lines 204 - 208, The code calls
safeJson(res) and then uses data.accessToken without verifying data is non-null;
update the authentication flow in the function containing safeJson, setToken,
and localStorage.setItem to check that data is not null and that
data.accessToken exists after awaiting safeJson(res); if data is null or lacks
accessToken, throw a descriptive Error (e.g., "Invalid JSON response" or
"Missing accessToken") instead of proceeding, and only call
setToken(data.accessToken) and localStorage.setItem('token', data.accessToken)
when the checks pass.

Comment on lines +274 to 278
const data = await safeJson(res);
if (!res.ok) throw new Error(data?.message || 'Google login failed');

setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
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

Missing null check after safeJson in googleLogin.

Same issue as verifyEmail — if safeJson returns null, accessing data.accessToken will throw.

Proposed fix
       const data = await safeJson(res);
       if (!res.ok) throw new Error(data?.message || 'Google login failed');
+      if (!data) throw new Error('Invalid response from server');

       setToken(data.accessToken);
📝 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
const data = await safeJson(res);
if (!res.ok) throw new Error(data?.message || 'Google login failed');
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
const data = await safeJson(res);
if (!res.ok) throw new Error(data?.message || 'Google login failed');
if (!data) throw new Error('Invalid response from server');
setToken(data.accessToken);
localStorage.setItem('token', data.accessToken);
🤖 Prompt for AI Agents
In `@frontend/src/context/authContext.tsx` around lines 274 - 278, In googleLogin,
safeJson(res) can return null so using data.accessToken will throw; add a null
check after const data = await safeJson(res) and before
setToken/localStorage.setItem to throw or handle an error when data is null
(similar to verifyEmail), e.g., if (!data) throw new Error('Invalid response
from server') or include data?.message in the error; ensure setToken and
localStorage.setItem('token', data.accessToken) only run when data is non-null
and contains accessToken.

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