/**
 * @fileoverview Helpers for converting HTTP `Response` objects into properly
 * classified `McpError` instances. Replaces the hand-rolled status → code ladder
 * that consumer servers tend to write (and get wrong, especially for 401/403/408).
 * @module src/utils/network/httpError
 */
import { JsonRpcErrorCode, McpError } from '../../types-global/errors.js';
/**
 * Maps an HTTP status code to a `JsonRpcErrorCode`. Covers the full client/server
 * 4xx/5xx range, with specific mappings for the codes most upstream APIs use to
 * signal authoritative outcomes (auth failures, conflicts, validation, rate limits).
 *
 * Returns `undefined` when the status is in the 1xx/2xx/3xx range — those are not
 * errors and the caller should not be invoking error mapping on them.
 *
 * | Status | Code |
 * |:-------|:-----|
 * | 400 | `InvalidParams` |
 * | 401 | `Unauthorized` |
 * | 402 | `Forbidden` (payment-required, treated as access denial) |
 * | 403 | `Forbidden` |
 * | 404 | `NotFound` |
 * | 405, 406, 410, 415 | `InvalidRequest` |
 * | 408 | `Timeout` |
 * | 409 | `Conflict` |
 * | 412, 416, 417 | `InvalidRequest` |
 * | 422 | `ValidationError` |
 * | 423, 424 | `Conflict` |
 * | 425 | `Timeout` |
 * | 428 | `InvalidRequest` |
 * | 429 | `RateLimited` |
 * | 431, 451 | `InvalidRequest` |
 * | 4xx (other) | `InvalidRequest` |
 * | 500, 501 | `InternalError` |
 * | 502, 503 | `ServiceUnavailable` |
 * | 504 | `Timeout` |
 * | 5xx (other) | `ServiceUnavailable` |
 */
export function httpStatusToErrorCode(status) {
    if (status < 400)
        return;
    switch (status) {
        case 400:
            return JsonRpcErrorCode.InvalidParams;
        case 401:
            return JsonRpcErrorCode.Unauthorized;
        case 402:
        case 403:
            return JsonRpcErrorCode.Forbidden;
        case 404:
            return JsonRpcErrorCode.NotFound;
        case 408:
            return JsonRpcErrorCode.Timeout;
        case 409:
            return JsonRpcErrorCode.Conflict;
        case 422:
            return JsonRpcErrorCode.ValidationError;
        case 423:
        case 424:
            return JsonRpcErrorCode.Conflict;
        case 425:
            return JsonRpcErrorCode.Timeout;
        case 429:
            return JsonRpcErrorCode.RateLimited;
        case 500:
        case 501:
            return JsonRpcErrorCode.InternalError;
        case 502:
        case 503:
            return JsonRpcErrorCode.ServiceUnavailable;
        case 504:
            return JsonRpcErrorCode.Timeout;
        default:
            return status >= 500 ? JsonRpcErrorCode.ServiceUnavailable : JsonRpcErrorCode.InvalidRequest;
    }
}
const DEFAULT_BODY_LIMIT = 500;
/**
 * Builds an `McpError` from an HTTP `Response`, with status-aware classification
 * and optional body capture.
 *
 * Reads the response body (consuming it) when `captureBody` is true, so callers
 * must `response.clone()` first if they intend to read the body elsewhere.
 *
 * Always returns an `McpError` even for 1xx/2xx/3xx — the caller is expected to
 * have verified `!response.ok` first, but the helper falls back to a sensible
 * code (`InternalError`) instead of silently producing nothing.
 *
 * @example
 * ```ts
 * const response = await fetch(url);
 * if (!response.ok) {
 *   throw await httpErrorFromResponse(response, {
 *     service: 'NCBI',
 *     data: { endpoint: 'esearch' },
 *   });
 * }
 * ```
 *
 * @example Wrapping a network failure
 * ```ts
 * try {
 *   const response = await fetch(url);
 *   if (!response.ok) {
 *     throw await httpErrorFromResponse(response, { service: 'NCBI' });
 *   }
 *   return await response.text();
 * } catch (error) {
 *   if (error instanceof McpError) throw error;
 *   throw new McpError(JsonRpcErrorCode.ServiceUnavailable, 'NCBI request failed', { url }, { cause: error });
 * }
 * ```
 */
export async function httpErrorFromResponse(response, options = {}) {
    const { captureBody = true, bodyLimit = DEFAULT_BODY_LIMIT, service, data: extraData, cause, codeOverride, } = options;
    const code = codeOverride?.(response.status) ??
        httpStatusToErrorCode(response.status) ??
        JsonRpcErrorCode.InternalError;
    const subject = service ?? safeHost(response.url) ?? 'Upstream';
    const statusText = response.statusText ? ` ${response.statusText}` : '';
    let body;
    if (captureBody) {
        try {
            const raw = await response.text();
            body = raw.length > bodyLimit ? `${raw.slice(0, bodyLimit)}…` : raw;
        }
        catch {
            /* body unreadable (already consumed, network error mid-stream) */
        }
    }
    const retryAfter = response.headers.get('retry-after') ?? undefined;
    const data = {
        url: response.url || undefined,
        status: response.status,
        statusText: response.statusText || undefined,
        ...(body !== undefined && { body }),
        ...(retryAfter !== undefined && { retryAfter }),
        ...extraData,
    };
    return new McpError(code, `${subject} returned HTTP ${response.status}${statusText}.`, data, cause !== undefined ? { cause } : undefined);
}
/** Returns the hostname from a URL string, or `undefined` if it can't be parsed. */
function safeHost(url) {
    if (!url)
        return;
    try {
        return new URL(url).host;
    }
    catch {
        return;
    }
}
//# sourceMappingURL=httpError.js.map