Why 401 Errors Happen (And Why You Should Care)

When your Android app talks to a server, it often sends an authentication token (like a JWT) with each request. If that token expires or is invalid, the server responds with a 401 Unauthorized status. Left unhandled, your users see cryptic errors or get stuck in a broken state.

In this guide you’ll learn:

  • What triggers a 401 and why auto-refresh matters
  • How to structure your API client to retry once with a fresh token
  • How to broadcast logout events to your UI using a shared ViewModel
  • Best practices for clean, maintainable Kotlin code

Table of Contents

  1. Understanding Token Refresh
  2. Setting Up Your Volley Request Wrapper
  3. Detecting 401 and Retrying Automatically
  4. Broadcasting Logout to Your UI
  5. Putting It All Together
  6. Best Practices and Tips
  7. FAQ

Understanding Token Refresh

  1. Access token: Short-lived token used on every API call.
  2. Refresh token: Longer-lived credential used to get a new access token when the old one expires.

Workflow:

  • App sends API request with access token in Authorization: Bearer ….
  • Server returns 401 Unauthorized if token is expired.
  • App catches 401, sends a refresh-token request.
  • On success: retry original API call once with the new access token.
  • On failure: force user to log in again.

Setting Up Your Volley Request Wrapper

class ApiRequest(
    private val context: Context,
    private val activity: AppCompatActivity
) {
    private val requestQueue = Volley.newRequestQueue(context)
    private val communicator by lazy {
        ViewModelProvider(activity).get(Communicator::class.java)
    }
    
    fun addJsonRequest(
        method: Int,
        url: String,
        jsonBody: JSONObject?,
        headers: Map<String, String> = emptyMap(),
        maxRetries: Int = 1,
        onSuccess: (JSONObject) -> Unit,
        onError: ((VolleyError) -> Unit)? = null,
        onAuthFailed: (() -> Unit)? = null
    ) { /* … */ }
}
  1. requestQueue: your Volley instance.
  2. communicator: a shared ViewModel to push logout events.

Detecting 401 and Retrying Automatically

Inside addJsonRequest, nest a helper method makeRequest(token, currentRetry):

private fun makeRequest(token: String?, currentRetry: Int) {
  val errorListener = Response.ErrorListener { error ->
    val statusCode = error.networkResponse?.statusCode
    when {
      statusCode == 401 && currentRetry < maxRetries -> {
        refreshToken { success, newToken ->
          if (success && !newToken.isNullOrEmpty()) {
            makeRequest(newToken, currentRetry + 1)
          } else {
            notifyLogout()
          }
        }
      }
      statusCode == 401 -> {
        notifyLogout()
      }
      else -> {
        onError?.invoke(error)
      }
    }
  }

  val finalHeaders = headers.toMutableMap().apply {
    token?.takeIf { it.isNotBlank() }?.let {
      this["Authorization"] = "Bearer $it"
    }
  }

  val request = object : JsonObjectRequest(
      method, url, jsonBody,
      Response.Listener(onSuccess), errorListener
  ) {
    override fun getHeaders() = finalHeaders
  }
  requestQueue.add(request)
}
  • Single retry: controlled by maxRetries.
  • Token refresh: in your refreshToken callback, you update the token in SharedPreferences.
  • Clean up: both on refresh-failure and on second 401, call notifyLogout().

Broadcasting Logout to Your UI

Rather than directly manipulating fragments or activities from the networking layer, you’ll broadcast a “logout event” via a shared LiveData in your Communicator ViewModel.

// Communicator.kt
class Communicator : ViewModel() {
  // Use a simple sealed class for stronger typing:
  sealed class UiEvent {
    object Logout : UiEvent()
    data class Login(val token: String) : UiEvent()
  }

  val events = MutableLiveData<UiEvent>()
}

In your ApiRequest:

private fun notifyLogout() {
  communicator.events.postValue(Communicator.UiEvent.Logout)
  onAuthFailed?.invoke()
}

Then in any UI component (e.g. FragmentTab4), observe it:

communicator.events.observe(viewLifecycleOwner) { event ->
  when(event) {
    is Communicator.UiEvent.Logout -> handleLogoutUi()
    else -> {/* ignore */}
  }
}
private fun handleLogoutUi() {
  // 1) Clear tokens
  PreferencesHelper.clearTokens(requireContext())
  // 2) Clear web storage & cookies
  WebStorage.getInstance().deleteAllData()
  CookieManager.getInstance().removeAllCookies(null)
  CookieManager.getInstance().flush()
  // 3) Navigate to Login screen with animation
  parentFragmentManager.commit {
    setCustomAnimations(
      R.anim.slide_in_left, R.anim.slide_out_right
    )
    replace(R.id.container, LoginFragment())
    addToBackStack(null)
  }
}

Putting It All Together

  1. User action → triggers ApiRequest.addJsonRequest(...).
  2. Volley sends request with current access token header.
  3. Server → 401 Unauthorized.
  4. ApiRequest catches it, calls refreshToken().
  5. Refresh succeeds → replays original request with new token.
  6. Refresh fails (or second 401) → events.postValue(UiEvent.Logout).
  7. FragmentTab4 observes logout event → runs handleLogoutUi().

This keeps your networking, token‐management, and UI layers decoupled, testable, and easy to maintain.


Best Practices and Tips

  • Use a sealed class for your LiveData events → compile-time safety.
  • Limit retries (e.g. maxRetries=1) to avoid infinite loops.
  • Centralize token storage in a helper (e.g. PreferencesHelper).
  • Clear cookies and WebStorage on logout to avoid stale sessions.
  • Animate transitions to give users visual feedback.
  • Log meaningful messages at each step for easier debugging.

FAQ

Q: What if refreshToken itself gets a 401?
A: Treat it as “failed to refresh,” and fire the logout event immediately—don’t retry the refresh.

Q: Can I use Retrofit instead of Volley?
A: Absolutely—just implement an OkHttp interceptor to catch 401s and refresh your token synchronously, then retry the request. The same LiveData‐based logout broadcast applies.

Q: How do I test this flow?
A: Mock your server to return a 401 on the first call, a 200 on the refresh call, then assert that the second original request succeeds. Also test the “refresh fails” branch to confirm the logout UI shows.

This article is inspired by real-world challenges we tackle in our projects. If you're looking for expert solutions or need a team to bring your idea to life,

Let's talk!

    Please fill your details, and we will contact you back

      Please fill your details, and we will contact you back