User Guide

Request Etiquette

Use Etiquette when every request from a client wrapper should carry a validated User-Agent and optional contact metadata.

import io.ktor.client.request.HttpRequestBuilder
import one.wabbit.web.common.Etiquette
import one.wabbit.web.common.applyEtiquette

fun HttpRequestBuilder.applyClientHeaders() {
    applyEtiquette(
        Etiquette(
            userAgent = "example-client/1.0 (+https://example.com/contact)",
            referer = "https://example.com",
            extraHeaders = mapOf("X-Client" to "example"),
        ),
    )
}

extraHeaders is defensively copied and cannot contain User-Agent or Referer.

Timeouts

Timeouts stores request, connect, and socket timeout values as Kotlin Durations.

import io.ktor.client.request.HttpRequestBuilder
import one.wabbit.web.common.Timeouts
import one.wabbit.web.common.applyTimeouts
import kotlin.time.Duration.Companion.seconds

fun HttpRequestBuilder.applyApiTimeouts() {
    applyTimeouts(
        Timeouts(
            request = 30.seconds,
            connect = 10.seconds,
            socket = 30.seconds,
        ),
    )
}

The Ktor client must install HttpTimeout for these settings to take effect. For SSE, long-polling, or other quiet streams, derive a streaming profile:

import one.wabbit.web.common.Timeouts

val streamingTimeouts = Timeouts().forStreaming()

Retry Schedules

Schedule is an immutable description of retry delays. Compile it once per retry loop.

import one.wabbit.web.common.Schedule
import one.wabbit.web.common.compile
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

val schedule =
    Schedule.exponential(
        base = 200.milliseconds,
        maxRetries = 4,
        maxDelay = 5.seconds,
        jitterFactor = 0.2,
    )

val run = schedule.compile(random = Random(1))
val firstDelay = run.next()

Schedule.retries() is the standard bounded exponential preset used by the HTTP helpers.

Retry-After

Use parseRetryAfterHeader for server-provided retry hints.

import one.wabbit.web.common.parseRetryAfterHeader
import kotlin.time.Duration.Companion.seconds

check(parseRetryAfterHeader("2.5") == 2.5.seconds)

The parser accepts delta seconds and the HTTP-date forms used by Retry-After: IMF-fixdate, obsolete RFC 850, and ANSI C asctime().

HTTP Retry Helpers

Use exception-driven retries when your Ktor call throws for retryable statuses, usually with expectSuccess = true.

import one.wabbit.web.common.retryingIdempotentHttpCall

suspend fun <T> retryThrownFailures(block: suspend () -> T): T =
    retryingIdempotentHttpCall(block = block)

Use response-driven retries when the call returns HttpResponse objects directly.

import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import one.wabbit.web.common.retryingIdempotentHttpResponseBodyCall

suspend fun fetchText(client: HttpClient, url: String): String =
    retryingIdempotentHttpResponseBodyCall(
        request = { client.get(url) },
        transform = { response -> response.bodyAsText() },
    )

The response helpers discard retryable intermediate responses before delaying and retrying.

Body Sampling

Body sampling is for diagnostics after errors. It consumes the response body channel.

import io.ktor.client.plugins.ResponseException
import one.wabbit.web.common.responseBodySampleOrNull

suspend fun ResponseException.loggableBody(): String? =
    responseBodySampleOrNull(maxLen = 2048)

The sample is byte-limited and decoded as UTF-8. It is not charset-aware and should not be used as a normal response text reader.