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

> Discover how gsygithubappcompose handles API errors and rate limits. Learn how TokenInterceptor captures rate-limit headers and maps error payloads for structured UI error handling.

- Repository: [Shuyu Guo/gsygithubappcompose](https://github.com/carguo/gsygithubappcompose)
- Tags: internals
- Published: 2026-02-26

---

**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`](https://github.com/carguo/gsygithubappcompose/blob/main/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`](https://github.com/carguo/gsygithubappcompose/blob/main/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`](https://github.com/carguo/gsygithubappcompose/blob/main/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 Interception** – `TokenInterceptor` 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:

```kotlin
// 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:

```kotlin
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:

```kotlin
@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:

```kotlin
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`](https://github.com/carguo/gsygithubappcompose/blob/main/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`.