How the Network Layer Handles API Errors and Rate Limits in gsygithubappcompose

The gsygithubappcompose network layer intercepts HTTP responses via TokenInterceptor to capture GitHub rate-limit headers and maps error payloads to the GitHubError model, exposing structured error data to the UI for graceful handling.

The gsygithubappcompose Android application communicates with the GitHub REST and GraphQL APIs through a Retrofit/OkHttp-based network stack. Understanding how this network layer handles API errors and rate limits is essential for building resilient client applications. The architecture cleanly separates header inspection, error deserialization, and UI presentation to deliver user-friendly error messages and retry mechanisms.

Core Network Components for Error Handling

The error handling architecture relies on several key components working together to process HTTP responses and GitHub-specific error payloads.

TokenInterceptor

The TokenInterceptor class, located at core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/interceptor/TokenInterceptor.kt, serves as the primary interception point for all outgoing requests and incoming responses. It adds the Authorization: token <access_token> header to every request and extracts GitHub-specific rate-limit headers from responses.

GitHubError Model

Error payloads from the GitHub API deserialize into the GitHubError data class defined in core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/model/Error.kt. This model captures human-readable messages, documentation URLs, and granular field-level validation errors.

NetworkModule Configuration

The NetworkModule at core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/di/NetworkModule.kt wires the stack together, providing a configured OkHttpClient instance that includes both logging capabilities and the TokenInterceptor.

Error Handling Flow and Rate Limit Detection

When an API request fails or hits a rate limit, the network layer processes the response through a structured pipeline:

  1. Request InterceptionTokenInterceptor injects the OAuth token into the request headers before transmission.
  2. Header Extraction – Upon receiving a response, the interceptor inspects three critical headers:
    • X-RateLimit-Remaining: The number of requests left in the current window
    • X-RateLimit-Reset: UTC epoch seconds when the quota resets
    • X-RateLimit-Limit: The total allowed requests per hour
  3. Payload Deserialization – For non-2xx responses, the error body JSON converts into a GitHubError instance using Gson or Moshi.
  4. Exception Mapping – When X-RateLimit-Remaining equals "0" and the HTTP status is 403, the layer raises a RateLimitException containing the header data.

The GitHubError data structure mirrors GitHub's error schema exactly:

// core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/model/Error.kt
data class GitHubError(
    val message: String?,                     // Human readable description
    val documentationUrl: String?,            // Link to GitHub docs
    val errors: List<ErrorDetail>?,           // Optional list of field-level problems
    val code: String?                         // Short error code (e.g., "missing", "invalid")
) {
    data class ErrorDetail(
        val resource: String?,                // e.g., "Repository"
        val field: String?,                   // e.g., "name"
        val code: String?                     // e.g., "missing"
    )
}

Implementation Examples

Retrofit API Call with Error Parsing

Repository implementations check Response.isSuccessful and parse error bodies when requests fail:

suspend fun searchRepositories(query: String, page: Int): Result<List<Repository>> {
    return try {
        val response = apiService.searchRepositories(query, page)
        if (response.isSuccessful) {
            Result.success(response.body()?.items.orEmpty())
        } else {
            // Parse the GitHub error payload
            val errorJson = response.errorBody()?.string()
            val githubError = gson.fromJson(errorJson, GitHubError::class.java)

            // Detect rate-limit condition
            val rateLimited = response.code() == 403 && 
                              response.headers()["X-RateLimit-Remaining"] == "0"

            if (rateLimited) {
                Result.failure(RateLimitException(githubError, response.headers()))
            } else {
                Result.failure(ApiException(githubError))
            }
        }
    } catch (e: IOException) {
        // Network-level problems (no internet, timeout, etc.)
        Result.failure(NetworkException(e))
    }
}

Jetpack Compose UI Reaction

The presentation layer consumes error states through a sealed UiState class, rendering specific UI components for rate limits versus general errors:

@Composable
fun RepositoryListScreen(viewModel: RepoViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    when (uiState) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> RepositoryList((uiState as UiState.Success).data)
        is UiState.Error -> {
            val error = (uiState as UiState.Error).cause
            if (error is RateLimitException) {
                RateLimitMessage(error.headers)
            } else {
                ErrorMessage(error.message ?: "Unknown error")
            }
        }
    }
}

The RateLimitMessage composable reads the header map to display remaining quota and reset countdown timers.

TokenInterceptor Implementation

The interceptor maintains a lastRateLimitInfo property that the ViewModel queries to display rate-limit status:

class TokenInterceptor @Inject constructor(
    private val authRepo: AuthRepository
) : Interceptor {

    var lastRateLimitInfo: RateLimitInfo? = null
        private set

    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val token = authRepo.getAccessToken()
        val request = original.newBuilder()
            .addHeader("Authorization", "token $token")
            .build()

        val response = chain.proceed(request)

        // Capture rate-limit headers for later use
        if (response.header("X-RateLimit-Remaining") == "0") {
            lastRateLimitInfo = RateLimitInfo(
                limit = response.header("X-RateLimit-Limit")?.toIntOrNull(),
                remaining = 0,
                reset = response.header("X-RateLimit-Reset")?.toLongOrNull()
            )
        }

        return response
    }
}

/** Simple holder used by the UI layer */
data class RateLimitInfo(
    val limit: Int?,
    val remaining: Int,
    val reset: Long?   // epoch seconds
)

Summary

  • Error Modeling: All GitHub API error payloads deserialize into the GitHubError class, providing type-safe access to messages, documentation URLs, and field-level validation errors.
  • Rate Limit Awareness: The TokenInterceptor extracts X-RateLimit-* headers from every response, storing quota information in RateLimitInfo when limits are exceeded.
  • Structured Exceptions: The network layer distinguishes between standard ApiException instances and RateLimitException objects, enabling the UI to display specific "try again later" messaging with countdown timers.
  • UI Integration: Components like GSYGeneralLoadState and GSYPullRefresh located in core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/ consume these errors to present retry buttons and rate-limit notices.

Frequently Asked Questions

How does TokenInterceptor detect GitHub rate limits?

The interceptor examines the X-RateLimit-Remaining header on every response. When this value equals "0" and the HTTP status code is 403, it constructs a RateLimitInfo object containing the limit, remaining count (zero), and reset timestamp from the X-RateLimit-Reset header, storing it for the UI layer to access.

What data structure represents API errors in gsygithubappcompose?

The GitHubError data class in core/network/src/main/java/com/shuyu/gsygithubappcompose/core/network/model/Error.kt represents API errors. It contains fields for message, documentationUrl, code, and a list of ErrorDetail objects that specify which resource and field caused validation failures.

How does the UI layer access rate limit information?

ViewModels access the lastRateLimitInfo property exposed by TokenInterceptor, which holds the most recent RateLimitInfo data class instance. This object contains the limit, remaining, and reset epoch timestamp, allowing Composable functions to calculate countdown timers and display quota status.

Where is the GraphQL RateLimit type defined?

The GraphQL schema defining the RateLimit type resides at core/network/src/main/graphql/github/schema.graphqls. This schema allows the application to query rate limit data directly through GraphQL operations, complementing the REST header-based detection used by TokenInterceptor.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →