cangjie_stdx 编码模块 URL 解析异常全解:UrlSyntaxException 的构造、抛出场景与实战捕获
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 包总览。