/
nv-lang
/
nova-http
Обзор
Документация
Войти
/
nv-lang
/
nova-http
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
src/error.nv
193 строки
12 KB
Evgeniy Golovin
fix(nova-http, [M-statuscode-u16-cast-bounce]): StatusCode.code u16→int, снятие каст-мостов, ServerResponse.status: int→StatusCode
24 июл 2026, 01:01
24 июл 2026, 01:01
4c937b1
Код
Авторство
О чём код?
// SPDX-License-Identifier: MIT OR Apache-2.0 // nova-http: error.nv — единая структурная ошибка домена http (D358/D325). // Extracted from monorepo std/http (Plan 203 Ф.1). // // `HttpError{kind, url, source}` — единственный error-тип модуля (R5/D325): // каждая fallible-операция возвращает `Result[T, HttpError]`. `ErrorKind` — OPEN // (catch-all `Other(str)` → пользователь ОБЯЗАН крыть wildcard-arm; добавление // h2/h3-специфичных kind'ов не ломает существующие match'и). Source-chaining // через `ErrSource`. // // GATE (Ф.1 self-contained): payload-типизированные `ErrSource`-варианты живут за // внешними гейтами и добавляются ПРИ приземлении их зависимостей (enum OPEN → // non-breaking): // - Net(NetError) ✅ приземлён (Plan 176 Ф.4, Q3): `HttpError.transport` // теперь несёт типизированный `NetError` вместо string-detail. // - Tls(TlsError) ✅ приземлён (Plan 116 Ф.5.3 / M-178): `HttpError.from_tls` // несёт типизированный `TlsError` из handshake/write/read вместо string-detail // (std/http/transport/real.nv). Разгейчено ПОСЛЕ фикса mono-кэша конструкторов // enum-вариантов (`ErrSource.Net(NetError)` vs `TlsError.Net(NetError)` — // оба несут одноимённый вариант `Net` с одинаковым payload; codegen раньше // резолвил bare `Net(e)` по первому/короче-имени кандидату без учёта // call-сайт контекста, `debt_find_variant_ctx` в compiler-codegen/src/codegen/ // emit_c.rs — closes [M-178-errsource-tls]). // - Utf8(Utf8Error) ← 176 Ф.0.5 (canonical str.from_bytes+Utf8Error) [M-178-errsource-utf8] // - Io(IoError) ← 176 (fs/io writer в copy_to) [M-178-errsource-io] // - Compress(CompressError) ← 179 Ф.1 (decompress) [M-178-errsource-compress] // Остальные ждут своих зависимостей; source-chaining демонстрируется также через // `ErrSource.Url(ParseUrlError)`. module http import compress.{CompressError} import std.net.{NetError} import tls.{TlsError} /// Категория ошибки http-домена. **OPEN** (D358): `Other(str)` — catch-all, /// поэтому `match` ОБЯЗАН нести wildcard-arm; расширение (h2/h3) не ломает /// существующие match'и. /// /// Транспортная ошибка несётся как `kind: Connect/Closed/Timeout` + /// `source: Net(NetError)` (Ф.2) — НЕ дублируется отдельным `Io`-kind (Q2). /// /// `Status(code)` возникает ТОЛЬКО через явный `response.error_for_status()` /// (Ф.2); 4xx/5xx сами по себе — валидный `Response`, не ошибка (Q4). #stable(since = "0.1") export type ErrorKind enum | Connect // не удалось установить TCP/транспорт-соединение | Dns // резолв host провалился | Tls // TLS-handshake/verify (Ф.4 gate 116) | Timeout // deadline/timeout scope (173) | Protocol(str) // парсинг/framing/smuggling/charset-деталь | InvalidUrl // URL не распарсился (деталь — в source: Url(ParseUrlError)) | InvalidHeader // не-tchar имя / CR-LF-NUL значение | Status(StatusCode) // от error_for_status (opt-in, Ф.2) | TooManyRedirects(int) // redirect-loop за лимитом (Ф.2) | BodyTooLarge // тело/decompress превысило cap | Closed // соединение закрыто до завершения | Canceled // ВНЕШНЯЯ отмена (peer-abort/RST_STREAM); свой scope-cancel ПРОПАГИРУЕТ | Blocked(str) // SSRF-guard deny (private-range target) (Ф.2) | Other(str) // OPEN → wildcard обязателен /// Причина-источник (source-chain). **EXPORT + OPEN** (Ред.2): публичное `ro /// source` требует, чтобы варианты были матчибельны извне; `OtherSource(str)` — /// catch-all, wildcard обязателен. См. GATE-нотку выше про отложенные варианты. #stable(since = "0.1") export type ErrSource enum | UrlParse(ParseUrlError) // ошибка разбора URL (Ф.1). ⚠ вариант НЕ `Url`: // имя-вариант == имя-тип `Url` ломает codegen // (cast вместо wrap) → spec-amend D358 (§5). | Net(NetError) // транспортная ошибка (Connect/Dns/Closed) — Plan // 176 Ф.4/Q3: типизированный `std.net.NetError`. | Compress(CompressError) // decode Content-Encoding (gzip/deflate) провалился // (Ф.2, 179; D381 сделал co-presence compress+http // ErrorKind линкуемой). [M-178-errsource-compress] | Tls(TlsError) // TLS handshake / encrypted write / encrypted read // провалился (Plan 116 Ф.5.3) — типизированный // `std.tls.TlsError`. `TlsError` несёт СВОЙ // `Net(NetError)`-вариант (underlying-транспорт) — // тот же variant-имя+payload, что и `ErrSource.Net` // выше, из ДРУГОГО enum'а; codegen различает их по // call-сайт контексту (M-178). | OtherSource(str) // OPEN / placeholder для гейтнутых источников /// Единственная структурная ошибка домена http (R5/D325). /// /// # Examples /// ```nova /// let e = HttpError.protocol_error("missing status line") /// assert(match e.kind { Protocol(_) => true, _ => false }) /// ``` #stable(since = "0.1") export type HttpError { ro kind ErrorKind ro url Option[Url] ro source Option[ErrSource] } // ── Конструкторы (внутримодульные хелперы + публичные) ─────────────────────── /// Ошибка без url/source из голого `ErrorKind`. #stable(since = "0.1") export fn HttpError.new(kind ErrorKind) -> HttpError => { kind, url: None, source: None } /// `Protocol(msg)` — парсинг/framing/smuggling. #stable(since = "0.1") export fn HttpError.protocol_error(msg str) -> HttpError => { kind: Protocol(msg), url: None, source: None } /// `InvalidHeader` — не-tchar имя / CR-LF-NUL значение (anti-injection). #stable(since = "0.1") export fn HttpError.invalid_header(msg str) -> HttpError => { kind: InvalidHeader, url: None, source: Some(OtherSource(msg)) } /// `InvalidUrl` с source-chain на детальный `ParseUrlError`. #stable(since = "0.1") export fn HttpError.invalid_url(e ParseUrlError) -> HttpError => { kind: InvalidUrl, url: None, source: Some(UrlParse(e)) } /// Транспортная ошибка (`Connect`/`Dns`/`Closed`/`Timeout`) с текстовой деталью в /// `source` — used where no typed `NetError` is available (e.g. a hard-coded /// transport-layer condition, not an actual `Net`-effect failure). #stable(since = "0.1") export fn HttpError.transport(kind ErrorKind, detail str) -> HttpError => { kind, url: None, source: Some(OtherSource(detail)) } /// Транспортная ошибка с типизированным `source: Net(NetError)` (Plan 176 Ф.4, /// Q3) — the `Net`-effect failure that triggered `kind` (`Connect`/`Dns`) is /// preserved instead of collapsing to a fixed string. #stable(since = "0.1") export fn HttpError.from_net(kind ErrorKind, e NetError) -> HttpError => { kind, url: None, source: Some(Net(e)) } /// Транспортная ошибка с типизированным `source: Tls(TlsError)` (Plan 116 Ф.5.3, /// M-178) — handshake/encrypted-write/encrypted-read failure over `TlsStream` /// preserves the actual `TlsError` instead of collapsing to a fixed string. #stable(since = "0.2") export fn HttpError.from_tls(kind ErrorKind, e TlsError) -> HttpError => { kind, url: None, source: Some(Tls(e)) } /// `BodyTooLarge` — превышен cap тела/decompress (DoS-guard). #stable(since = "0.1") export fn HttpError.body_too_large() -> HttpError => { kind: BodyTooLarge, url: None, source: None } /// Ошибка декодирования Content-Encoding (gzip/deflate) — corrupt/framing/checksum; /// `kind: Protocol` + типизированный `source: Compress(CompressError)` (179). Bomb /// (превышение `max_decompressed`, D334) НЕ сюда — он мапится в `body_too_large()`. #stable(since = "0.1") export fn HttpError.from_compress(e CompressError) -> HttpError => { kind: Protocol("content-encoding decode failed"), url: None, source: Some(Compress(e)) } /// Ошибка типизированной десериализации тела (`json_decode_body[T]`, Plan 180). /// `kind: Protocol` + деталь serde `DeError` (JSONPath+причина) в `source`. /// (Типизированный `ErrSource.De(DeError)` — отдельный gate, чтобы не тянуть serde /// в lean core-error.) Ф.2. #stable(since = "0.1") export fn HttpError.decode_error(detail str) -> HttpError => { kind: Protocol("json decode failed"), url: None, source: Some(OtherSource(detail)) } /// Приклеить url к ошибке (context-enrichment на границе client-слоя, Ф.2). #stable(since = "0.1") export fn HttpError @with_url(u Url) -> HttpError => { @kind, url: Some(u), @source } /// Человекочитаемое описание (для логов; НЕ на проводе). #stable(since = "0.1") export fn HttpError @to_str() -> str { ro base = match @kind { Connect => "connect error" Dns => "dns resolution failed" Tls => "tls error" Timeout => "timed out" Protocol(m) => "protocol error: ${m}" InvalidUrl => "invalid url" InvalidHeader => "invalid header" Status(c) => "unexpected status ${c.code()} ${c.reason()}" TooManyRedirects(n) => "too many redirects (${n})" BodyTooLarge => "body too large" Closed => "connection closed" Canceled => "canceled" Blocked(m) => "blocked: ${m}" Other(m) => m } match @source { Some(UrlParse(e)) => "${base}: ${e}" Some(Net(e)) => "${base}: ${e}" Some(Compress(e)) => "${base}: ${e}" Some(Tls(e)) => "${base}: ${e}" Some(OtherSource(m)) => "${base}: ${m}" None => base } }