cangjie_stdx 编码模块 URL 解析异常全解:UrlSyntaxException 的构造、抛出场景与实战捕获

原创2026-10-14 15:33:321,987 阅读
文章标签:标准库

cangjie_stdx 编码模块 URL 解析异常全解:UrlSyntaxException 的构造、抛出场景与实战捕获

导读

本文聚焦仓颉标准扩展库 cangjie_stdx 中 stdx.encoding.url 包的 URL 解析异常类 UrlSyntaxException。该异常是所有 URL 解析、编码解码与 Form 表单解析失败时的统一错误出口,贯穿 URL.parse、URL 构造函数、Form 构造以及 HTTP 客户端的 URL 校验等核心路径。读完本文,你将掌握 UrlSyntaxException 的三种构造方式、异常消息的完整格式、它在源码中的全部抛出场景,以及如何在仓颉代码中准确捕获并利用异常信息定位解析失败位置。

URL 解析异常在 url 包中的定位

stdx.encoding.url 包为仓颉提供了完整的 URL 能力,包括解析 URL 各组件、URL 编码/解码、URL 与路径合并等。一个标准 URL 遵循如下格式:

scheme://host[:port]/path[?query][#fragment]

例如 https://www.example.com/blog/post/123?page=2&sort=desc#section-1,其 scheme 为 https、host 为 www.example.com、path 为 /blog/post/123、query 为 ?page=2&sort=desc、fragment 为 #section-1。

当用户传入的 URL 字符串不符合解析规范时,url 包统一抛出 UrlSyntaxException。它是该包 API 列表中唯一的异常类,也是 Form、URL 两个核心类共同声明的错误类型。

UrlSyntaxException 类定义与继承关系

在 url_syntax_exception.cj 中,UrlSyntaxException 定义如下:

public class UrlSyntaxException <: Exception {
    var pos: String = ""
    public init(reason: String) {
        super(reason)
    }
    public init(input: String, reason: String) {
        super(reason + " Input = ${input}.")
    }
    public init(input: String, reason: String, pos: String) {
        super(reason + " Input = ${input} (at '${pos}').")
    }
    protected override func getClassName(): String {
        return "UrlSyntaxException"
    }
}

核心要点:

  • 父类型为 Exception,因此它可以被 catch (e: Exception) 捕获,也可被更精确的 catch (e: UrlSyntaxException) 捕获;
  • 类内维护 pos 字段,用于承载解析失败的具体位置信息;
  • 三个构造函数分别提供"仅原因"、"原因 + 输入"、"原因 + 输入 + 失败位置"三档信息粒度的构造能力;
  • 重写了 getClassName(),返回类名 UrlSyntaxException,保证异常输出时类名准确。

该类的完整 API 声明见 url_package_exceptions.md,与中文版文档 url_package_exceptions.md(中文) 一一对应。

三个构造函数详解与消息格式

init(String):仅指定错误原因

public init(reason: String)
  • 功能:根据错误原因构造异常实例,异常消息即为 reason 本身;
  • 参数:reason: String —— 解析错误的原因;
  • 消息格式:reason。

示例:

import stdx.encoding.url.*

main() {
    try {
        throw UrlSyntaxException("URL格式错误")
    } catch (e: UrlSyntaxException) {
        println("异常信息: ${e.message}")
    }
}

运行结果:

异常信息: URL格式错误

init(String, String):指定 URL 输入与错误原因

public init(input: String, reason: String)
  • 功能:根据 URL 及错误原因构造异常实例;
  • 参数:
    • input: String —— 原生 URL 或其片段;
    • reason: String —— 解析错误的原因;
  • 消息格式:reason + " Input = ${input}.",即把出错的 URL 原文拼接到原因之后,便于开发者直接看到是哪个字符串解析失败。

示例:

import stdx.encoding.url.*

main() {
    try {
        throw UrlSyntaxException("https://example.com%", "URL格式错误")
    } catch (e: UrlSyntaxException) {
        println("异常信息: ${e.message}")
    }
}

运行结果:

异常信息: URL格式错误 Input = https://example.com%.

可以看到消息中完整保留了原始输入 https://example.com%,这对复现与排查输入问题非常关键。

init(String, String, String):指定 URL 输入、错误原因与失败位置

public init(input: String, reason: String, pos: String)
  • 功能:根据 URL 字符串、错误原因以及解析失败的部分构造异常实例;
  • 参数:
    • input: String —— 原生 URL 或其片段;
    • reason: String —— 解析错误的原因;
    • pos: String —— 给定 URL 字符串中解析失败的部分;
  • 消息格式:reason + " Input = ${input} (at '${pos}').",比两参版本多出 (at '...') 片段,精准标记错误发生的位置。

示例:

import stdx.encoding.url.*

main() {
    try {
        let url = "https://example.com%"
        throw UrlSyntaxException(url, "URL格式错误", url[8..])
    } catch (e: UrlSyntaxException) {
        println("异常信息: ${e.message}")
    }
}

运行结果:

异常信息: URL格式错误 Input = https://example.com% (at 'example.com%').

三参版本在消息中同时给出了完整输入与失败子串 example.com%,适合在需要向调用方精确反馈错误位置的场景中使用。源码中这一构造函数同样被 IPv6 解析等逻辑使用(见下文),例如 UrlSyntaxException(src, "Each colon-separated field must have at least one digit.", s[offset..])。

源码中的抛出场景:何时会遇到 UrlSyntaxException

要真正用好一个异常类,必须先弄清它在哪些场景被抛出。下面按解析阶段归纳 url.cj 与 url_util.cj 中的全部抛出点。

1. 输入层面的防御性校验

  • 反斜杠拒绝:URL.parse 在解析入口首先检查输入是否包含 \,一旦发现即抛出 UrlSyntaxException(_rawUrl, "Backslash is not allowed in URL.")。源码注释明确指出这是为了防止路径穿越攻击(许多 HTTP 服务器和浏览器会把 \ 视同 /);
  • 控制字符拒绝:validateNoControlChars 会检查主机名中是否含 0x00-0x1F 及 0x7F 的控制字符,防止 CRLF 注入等头部注入攻击,违规时抛出 "Invalid '${context}': contains control characters.";
  • 空字节拒绝:checkAndDecode 在解码 %00 时会抛出 "Null byte is not allowed in URL.",避免 C 风格字符串被空字节截断导致解析结果与实际网络请求不一致。

2. scheme(协议)解析

  • scheme 必须以字母开头,否则抛出 "Scheme must start with a letter.";
  • scheme 只允许字母、数字、+、-、.,违规时抛出 "Scheme contains invalid characters: only letters, digits, '+', '-', and '.' allowed."。

3. authority(主机与端口)解析

  • scheme:// 之后缺少 authority 组件时抛出 "Missing authority component.";
  • IPv6 字面量 [...] 中缺少右括号 ] 时抛出 "Missing ']' in host.";
  • IPv6 括号内为空时抛出 "IPv6 address within brackets is empty.";
  • 普通主机名中出现冒号时抛出 "IPv6 address requires square brackets."(即未加方括号的 IPv6 会被拒绝);
  • 端口为空、含非数字字符或超出 0~65535 范围时,分别抛出 "Port is empty."、"Port contains non-digit characters."、"Port out of range."。

4. userinfo 解析

  • userinfo 中出现编码后的 @ 字符时抛出 "Userinfo contains an encoded '@' character.";
  • 包含非法编码序列时抛出 "Userinfo contains an invalid encoding sequence."。

5. path、query、fragment 的转义校验

checkAndDecode 是解码的核心函数,它逐字节检查各组件:

  • 出现该组件字符集之外的 ASCII 字符时抛出 "Invalid URL part.";
  • % 后不是两位十六进制数字(或 % 后面字符不足)时抛出 "Invalid URL escape."。

对应地,url_util.cj 中顶层的 decode 函数在解码查询串时遇到非法的 %xx 转义同样抛出 "Invalid URL escape."。

6. IPv6 专项解析

url_util.cj 中的 parseIPv6 / innerParseIPv6 / readHextet 对 IPv6 地址做了精细校验,抛出场景包括:

  • zone(区域 ID)为空:"Zone is empty.";
  • 冒号分隔的字段缺少数字:"Each colon-separated field must have at least one digit.";
  • 出现意外字符:"Unexpected character, expected colon.";
  • 地址包含非法字符:"Address contains invalid characters.";
  • 省略号 :: 只能出现一次:"IPv6 address can only have one ellipsis.";
  • 字段值超过 65535:"IPv6 field value exceeds 65535.";
  • hextet 过长:"IPv6 hextet is too long." 等。

这些错误几乎都使用三参构造函数,把失败子串作为 pos 传入,便于精确定位。

7. 构造函数与 replace 的约束校验

URL 构造函数和 replace 方法还会做结构性约束校验,例如:

  • "Hostname exists but scheme is empty."(有主机名却无协议);
  • "Scheme exists but hostname and path are empty."(有协议但主机名和路径都为空);
  • "Absolute URL requires an absolute path."(绝对 URL 要求路径以 / 开头);
  • "Port exists but hostname is empty."、"Userinfo exists but hostname is empty." 等。

8. Form 表单解析

Form 的带参构造函数 Form(queryComponent) 在解析 URL 编码的查询字符串时,若遇到非法转义字符同样抛出 UrlSyntaxException(见 form.cj 中 init(queryComponent) 对 decode(kv[0], isQuery: true) 的调用)。因此用 UrlSyntaxException 捕获表单解析错误同样成立。

在真实调用链中捕获异常:URL.parse 实战

URL.parse 是 url 包最常用的入口,其签名如下(见 url.cj):

public static func parse(rawUrl: String): URL

该方法要求输入符合 RFC3986 规范,解析失败时抛出 UrlSyntaxException(IllegalArgumentException 仅用于输入本身含非法 UTF-8 引导字节的极端场景)。一个完整的带异常处理的解析示例:

import stdx.encoding.url.*

main(): Int64 {
    let urlString = "http://www.example.com:80/path%E4%BB%93%E9%A2%89?key=value%E4%BB%93%E9%A2%89#%E4%BD%A0%E5%A5%BD"
    try {
        let url = URL.parse(urlString)
        println("协议 (scheme): ${url.scheme}")
        println("主机 (host): ${url.host}")
        println("主机名 (hostName): ${url.hostName}")
        println("端口 (port): ${url.port}")
        println("路径 (path): ${url.path}")
        println("查询参数 (query): ${url.query.getOrThrow()}")
        println("片段 (fragment): ${url.fragment.getOrThrow()}")
        println("原始路径 (rawPath): ${url.rawPath}")
        println("完整 URL: ${url}")
    } catch (e: UrlSyntaxException) {
        println("URL 解析失败: ${e.message}")
    }
    return 0
}

上述正常路径的运行结果为:

协议 (scheme): http
主机 (host): www.example.com:80
主机名 (hostName): www.example.com
端口 (port): 80
路径 (path): /path仓颉
查询参数 (query): key=value仓颉
片段 (fragment): 你好
原始路径 (rawPath): /path%E4%BB%93%E9%A2%89
完整 URL: http://www.example.com:80/path%E4%BB%93%E9%A2%89?key=value%E4%BB%93%E9%A2%89#%E4%BD%A0%E5%A5%BD

而如果输入换成非法字符串,例如 http://example.com:99999(端口越界)或 http://example.com/%ZZ(非法转义),catch (e: UrlSyntaxException) 就会捕获到带有原始输入与失败位置的完整消息,示例中的输出模式如:

URL 解析失败: Port out of range. Input = http://example.com:99999.
URL 解析失败: Invalid URL escape. Input = http://example.com/%ZZ.

跨包使用:HTTP 客户端中的 UrlSyntaxException

UrlSyntaxException 的作用范围不止于 encoding 包。在 http_package_classes.md 中,HTTP 客户端与服务端的多个接口都声明:当传入的 url 参数不符合 URL 解析规范时抛出 UrlSyntaxException;当请求中的 form 不符合 Form 格式时同样抛出该异常。这意味着你在使用 stdx.net.http 构造请求 URL 时,也需要用同一异常类型来处理 URL 非法的问题,捕获与排查方式与上文完全一致。

异常消息格式速查表

构造函数 消息格式 适用场景
init(reason) reason 仅需要说明错误原因
init(input, reason) reason Input = ${input}. 需要反馈出错的原生 URL
init(input, reason, pos) reason Input = ${input} (at '${pos}'). 需要同时反馈完整输入与失败子串

小结

UrlSyntaxException 是 cangjie_stdx 中 URL 能力(解析、编码解码、表单解析、HTTP URL 校验)统一的异常出口。它的三个构造函数提供了从"仅原因"到"原因 + 输入 + 失败位置"的递进式信息粒度,而源码中的十余处抛出点覆盖了反斜杠、控制字符、空字节、scheme、IPv6、端口、userinfo、转义序列等全部解析失败分支。掌握它的构造方式与抛出场景,即可在仓颉应用中快速定位 URL 解析问题,写出健壮的异常处理代码。更多完整 API 细节可查阅 URL 类文档 与 url 包总览。

登录后查看全文
cangjie_stdx