Episode 6 — Existing User Re-install: Register → Conflict → Fallback Login

Overview

This episode covers what happens when an existing user deletes and reinstalls the app. The local data (including adventureTube_id) is wiped, but the account still exists on the backend. The iOS code has a built-in conflict recovery mechanism for this scenario — but it was broken due to a wrong HTTP status code on the server side.


The Problem — Server Returns 401 Instead of 409

The iOS app has conflict recovery logic in GoogleLoginService.swift: when registerUser gets a 409 Conflict response, it falls back to loginWithGoogleIdToken. This is designed for the re-install case where the email already exists on the backend.

However, the server was returning the wrong HTTP status code:

// AuthErrorCode.java — CURRENT (WRONG)
USER_EMAIL_DUPLICATE("User already exists", HttpStatus.UNAUTHORIZED),  // sends 401

What happens with 401 (broken behavior)

registerUser → server returns HTTP 401 (wrong)
        ↓
handleHttpResponse case 401 → throws BackendError.unauthorized
        ↓
.sink receiveCompletion → .failure(BackendError.unauthorized)
        ↓
case .conflict = backendError → FALSE (it's .unauthorized, not .conflict)
        ↓
falls to else branch → completion(.failure(error))
        ↓
User sees an error screen instead of being logged in

The iOS code checks for .conflict but receives .unauthorized — so the fallback login never runs. The user is stuck after reinstall.

Why this is hard to debug

The error is silently misrouted — the iOS app catches the error and handles it, just by the wrong branch. The else branch prints “BackEnd Connection Error” and calls completion(.failure(error)), which looks like a legitimate network error. There is no indication that the real issue is a wrong HTTP status code from the server.


The Fix — Correct HTTP Status Code Contract

// AuthErrorCode.java — FIXED
USER_EMAIL_DUPLICATE("User already exists", HttpStatus.CONFLICT),  // sends 409

What happens with 409 (correct behavior)

registerUser → server returns HTTP 409 (correct)
        ↓
handleHttpResponse case 409 → throws BackendError.conflict
        ↓
.sink receiveCompletion → .failure(BackendError.conflict)
        ↓
case .conflict = backendError → TRUE ✓
        ↓
loginWithGoogleIdToken fallback runs
        ↓
User silently logged in — no error shown

The iOS Code — Two Branches in GoogleLoginService

// GoogleLoginService.swift — STEP 6
if adventureUser.adventureTube_id != nil {
    // Local ID exists → known existing user → login directly
    adventuretubeAPI.loginWithGoogleIdToken(adventureUser: adventureUser)
} else {
    // No local ID → either genuinely new user OR re-install (wiped local storage)
    // Attempt registration — the backend decides which case it is
    adventuretubeAPI.registerUser(adventureUser: adventureUser)
        .sink(receiveCompletion: { completionSink in
            case .failure(let error):
                if case .conflict = backendError {
                    // 409 — backend says email exists → this is a re-install
                    adventuretubeAPI.loginWithGoogleIdToken(adventureUser: adventureUser)
                } else {
                    // Other error — genuine failure
                    completion(.failure(error))  // ← 401 landed HERE (the bug)
                }
        })
}

Layer-by-Layer Flow (When Fixed with 409)

1. iOS — registerUser called

POST /auth/users
{ "googleIdToken": "...", "email": "user@gmail.com" }

2. Backend — email duplication check

auth-service calls member-service
POST /member/emailduplicationcheck
    → email exists → true
    → throws DuplicateException(USER_EMAIL_DUPLICATE)
    → GlobalExceptionHandler catches it
    → returns HTTP 409 + JSON

{ "success": false, "errorCode": "USER_EMAIL_DUPLICATE", "message": "User already exists" }

3. iOS — handleHttpResponse routes by status code

switch httpResponse.statusCode {
    case 409:
        throw BackendError.conflict(message: "User already exists", errorCode: "USER_EMAIL_DUPLICATE")
}

4. iOS — .sink catches conflict and falls back to login

case .failure(let error):
    if let backendError = error as? BackendError,
       case .conflict = backendError {
        adventuretubeAPI.loginWithGoogleIdToken(adventureUser: adventureUser)
    }

5. Backend — login with Google ID token

POST /auth/token
{ "googleIdToken": "..." }
    → member-service: /member/findmemberbyemail
    → member-service: /member/storetokens
    → returns accessToken + refreshToken

Zipkin Trace Evidence

The full flow was confirmed via Zipkin distributed tracing (after the 409 fix was applied):

Trace Endpoint Duration Outcome
1 POST /auth/users 1.788s CLIENT_ERROR (409)
2 POST /auth/token 1.588s SUCCESS
3 GET /web/geo/data/bounds 421ms SUCCESS

Trace 1 — Registration attempt (rejected with 409): gateway → auth-service → member-service: /member/emailduplicationcheck (284ms) → 409 returned

Trace 2 — Fallback login (triggered by 409 conflict): gateway → auth-service → member-service: /member/findmemberbyemail (26ms) → member-service: /member/storetokens (207ms) → SUCCESS

Trace 3 — App fully logged in, fetching map data: gateway → web-service → geospatial-service: /geo/data/bounds (127ms) → SUCCESS


Key Takeaway

The HTTP status code is the contract between the Java backend and the iOS client. The iOS error handling is driven entirely by status codes — handleHttpResponse maps each code to a specific BackendError case, and .sink branches on that case. If the server sends the wrong code (401 for a duplicate email instead of 409), the iOS app silently routes to the wrong error handler, and the designed recovery logic never executes. One wrong line in AuthErrorCode.java breaks the entire re-install experience.

Summary

Fresh install → Google sign-in → idToken fetched
        ↓
adventureTube_id = nil → registerUser called
        ↓
Backend: email exists → DuplicateException
        ↓
SERVER MUST RETURN 409 (not 401) ← the critical fix
        ↓
iOS: handleHttpResponse case 409 → BackendError.conflict
        ↓
iOS: case .conflict → loginWithGoogleIdToken fallback
        ↓
Backend: findmemberbyemail → storetokens → SUCCESS
        ↓
User signed in seamlessly — no error shown

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top