# 代码提示词术语

Source: https://codewiki.com/zh/ai-era/code-prompt-vocabulary/

> - **what**: 代码提示词术语用于说明改动的可观察契约、有效状态、失败方式和结构边界。
> - **trap**: 「清理」「处理」或「API」等常见词可能暗含多种互不兼容的改动，智能体会从中选择一种看似合理的解释。
> - **fix**: 先明确具体符号和调用方，再说明输入、输出、不变量、失败行为、允许的结构与验收证据。

## 是什么，为什么存在

代码提示词术语是一组工程词汇，用来描述改动，又不必规定实现的每一行。它们为调用方能观察到的行为、始终成立的条件、失败的呈现方式，以及职责归属提供名称。这些名称能帮助开发者和编程智能体区分日常表达中听起来相近的改动。

「让订单辅助函数更健壮」没有明确目标、有效输入、失败结果、兼容要求和完成证据。智能体只能根据邻近代码与常见模式补齐这些空白。生成的代码可能整洁合理，却解决了另一个问题，因为提示词从未区分意图与实现选择。

精确不等于在提示词中堆满术语。只有双方都能把术语对应到具体符号、行为或测试时，它才有用。函数名称明确时，「保留公开函数签名」很精确；「使用更好的抽象」即使用了技术词汇，仍然只是主观判断。

最重要的区别是可观察契约与内部结构。调用方能观察到接受的输入、返回值、错误、副作用、顺序与时序保证。它通常看不到实现使用了一个还是三个辅助函数，除非反射、性能、堆栈信息或仓库约定使这些结构成为实际契约的一部分。

API 契约（API contract）不只属于 HTTP 端点。模块导出的函数、命令行程序的参数与退出码、事件负载、数据库迁移接口都有使用方。提示词应明确这些使用方，因为同一改动在私有辅助函数中可能无害，放到公开边界上却可能造成破坏。

提出功能、缺陷修复、重构、测试、审查或解释请求时，都会用到这些词汇。代码库允许多种局部上合理的设计时，它尤其重要。术语负责减少歧义，示例与可执行检查再证明所选含义符合任务。

### 改动背后的四个问题

大多数代码请求在分别回答四个问题后都会更清楚。把问题分开，可以防止实现偏好悄悄替代必需行为。

| 问题 | 词汇 | 具体形式 |
| --- | --- | --- |
| 调用方可以做什么？ | 接口、签名、形参、返回结构 | `reserveSeats(inventory, requested)` 返回带标签的结果 |
| 什么必须始终成立？ | 前置条件、后置条件、不变量 | 可用座位数绝不为负 |
| 问题怎样呈现？ | 失败模式、异常、错误值、超时 | 无效数量返回 `INVALID_QUANTITY` |
| 逻辑应该放在哪里？ | 边界、职责、依赖、纯函数辅助单元 | 发送操作保留在注入函数之后 |

第五个问题关乎证据：审查者怎样知道改动正确？验收条件、示例、测试、类型检查和命令可以回答它。「完成」是由这些证据推导的结论，不是「代码看起来合理」的另一种说法。

### 使用粒度合适的词汇

应当说出你真正关心的最小稳定对象。需求若涉及 JSON 字段，就说「响应结构」，不要只说「API」。需求若涉及第二次相同请求会不会重复扣款，就说「幂等效果」，不要只说「可安全重试」。

反过来，如果只有行为重要，就不要强制指定内部模式。要求使用工厂、策略或类，可能排除同样满足契约的更小实现。只有结构约束确实保护所有权、可测试性、性能、安全或既有仓库边界时，才应明确提出。

## 工作原理

有效的代码提示词类似一份小型改动契约。它标明目标，说明当前和期望的可观察行为，写出不能改变的内容，限制改动范围，并指定验证方法。智能体仍可在这个框架内调查和选择实现细节。

这些词充当坐标，不是魔法命令。「形参」指向函数定义，「实参」指向调用，「不变量」横跨多个状态，「失败模式」指向操作无法给出正常结果的某条路径。把每个词对应到代码库证据，便能消除大部分歧义。

### 接口词汇

函数签名（function signature）描述可调用接口：形参名称与种类、默认值，以及类型化代码中的注解或声明类型。形参（parameter）是定义中的具名输入槽。实参（argument）是某次调用提供的值或表达式。

这种区别在提示词中很重要。「把 `timeoutMs` 形参重命名为 `timeout`，但不更新调用方」存在矛盾，因为调用方可能按名称传参。「在 `loadProfile(userId)` 调用位置，把已认证用户的 ID 作为 `userId` 实参传入」则同时明确了绑定的两端。

应当有意识地使用这些接口词汇：

- **公开 API：** 外部使用方可以依赖的表面；需说明具体使用方群体。
- **签名：** 可调用对象声明的输入表面；需说明是否允许修改。
- **返回结构：** 成功调用返回的字段、分支或对象类型。
- **协议：** 参与方都必须理解的一组操作或消息。
- **调用位置：** 调用目标的位置；应要求检查是否找全静态和动态调用方。

「保留接口」并不完整，除非提示词明确哪些部分稳定。位置调用或许能容忍形参改名，关键字调用却不能。新增可选字段对宽容的使用方可能兼容，却会让严格模式验证器失败。

### 行为词汇

前置条件是在操作有效之前必须成立的条件。后置条件是操作成功完成后作出的承诺。不变量必须在每个相关公开状态转换中都成立，不能只在成功路径成立。

对于类，这种状态规则称为类不变量（class invariant）。对于模块或工作流，「状态不变量」通常更清楚。可以时，应把规则写成谓词：`available >= 0` 比「库存保持有效」更容易测试。

副作用是返回值之外的可观察交互，例如写文件、改变共享状态、记录日志、发送消息或调用远程服务。「改成纯函数」要求不存在这类效果，并且结果由显式输入决定。如果时间、随机数、环境变量或隐藏缓存会影响结果，就把它们变成依赖，或者明确列为允许的输入。

顺序和次数也属于行为。「事务提交后发送收据，每个订单 ID 最多一次」比「发送收据」更明确。对于可重试工作，要区分幂等效果与仅仅两次返回相同值的函数。

### 失败词汇

失败模式是操作失败的一种具体方式：输入无效、数据缺失、冲突、超时、取消、依赖拒绝或部分完成。只说出模式还不够，还要说明表示方式，以及失败出现时哪些副作用可能已经发生。

应当区分预期领域失败、程序员错误与基础设施失败。「需求超过库存时返回 `{ ok: false, error: 'INSUFFICIENT_SEATS' }`，库存格式错误时抛出 `TypeError`，发送依赖拒绝时保持原异常向上传播」赋予三条路径不同含义。「处理错误」则允许智能体吞掉、包装、记录、重试或翻译所有错误。

失败语义通常还包含恢复规则：

- **快速失败：** 前置条件不成立时，在产生副作用前拒绝操作。
- **失败关闭：** 验证结果不确定时拒绝请求或保留更安全的状态。
- **原子性：** 在指定边界只暴露完整转换或完全不转换。
- **可重试：** 明确哪些失败允许再次尝试，以及效果是否幂等。
- **尽力而为：** 继续执行选定的独立工作，同时报告失败部分。

这些术语不能互换。批处理可以在记录之间尽力而为，同时保证每条记录的更新仍有原子性。只有请求带幂等键或其他去重机制时，超时才可能安全重试。

### 结构词汇

架构边界（architecture boundary）把所有权、数据访问、变化或故障影响限制在系统的指定部分。一个组件调用另一个组件时，依赖会跨过这种边界。接缝（seam）是为了测试或替换而可以换掉依赖的位置。

「把验证提取为纯函数辅助单元，把 I/O 留在命令处理器中，并通过现有 `dependencies` 对象注入发送器」说明了职责与允许的依赖方向，却没有指定辅助函数名称或逐行控制流。智能体因此知道哪些代码可以移动，哪些必须留在边缘。

常见的结构动词会产生不同效果：

| 动词 | 请求的转换 | 契约提醒 |
| --- | --- | --- |
| 重命名 | 改变符号名称 | 查找反射、字符串、导入与关键字调用方 |
| 提取 | 把逻辑移到一个新单元之后 | 保留求值顺序与副作用 |
| 内联 | 用单元主体替换具名单元 | 保留复用、递归与可见性需求 |
| 包装 | 在已有操作周围增加行为 | 定义顺序与错误传播方式 |
| 替换 | 换掉某个实现或依赖 | 说明兼容与迁移范围 |
| 弃用 | 保持可用，同时阻止新增使用 | 定义警告、替代方案与移除计划 |

重构会修改内部结构，同时保留指定的可观察行为。如果输出、失败行为、时序保证或受支持的调用形式有意改变，这项任务就不只是重构。把行为变更与结构清理分开，才能分别根据对应证据审查。

### 兼容性词汇

破坏性变更（breaking change）可能让以前有效的使用方交互失败或产生不同含义。向后兼容（backward compatibility）表示新提供方仍满足旧契约下有效的交互。两者都取决于哪些使用方和契约版本属于当前范围。

源码兼容、二进制兼容、线协议兼容和行为兼容是不同主张。在 JavaScript 中，把返回数组改为可迭代对象，可能保留 `for...of` 调用方，却破坏读取 `.length` 或序列化结果的代码。应要求具体的兼容维度，而不是只说「不要破坏任何东西」。

当前行为缺乏文档时，特征测试（characterization test）会在结构改动前记录使用方能观察到的行为。它并不表示旧行为中的每个怪异细节都值得保留。提示词应说明哪些记录用例受保护，哪个缺陷需要改变。

### 验收词汇

验收条件把意图转成可判定的观察。每项条件都应给出准备、操作与结果，或者指向已经编码这些步骤的现有命令。反例很重要，因为许多有歧义的提示词在成功路径上会得出相同结果。

紧凑的请求可以按以下顺序组织：

1. **目标：** 指明文件、符号、端点、命令或使用方边界。
2. **行为：** 用具体示例说明成功输入与输出。
3. **约束：** 说明不变量、兼容性、顺序与禁止的副作用。
4. **失败：** 定义重要的无效、缺失、冲突、超时与依赖失败路径。
5. **结构：** 只在承载设计意图时说明所需边界与模式。
6. **证据：** 指明用于判定完成的测试、命令、差异与人工检查。

不是每个请求都需要六个部分。私有代码的一行重命名可能只需要目标、范围和测试。支付重试或认证改动则需要明确失败与副作用语义，因为依赖看似合理的默认选择风险太高。

## 示例

下面的示例展示精确的提示词术语会在代码中产生什么结果。它们不是某个特定模型的对话记录，而是无需猜测即可审查契约的可运行目标。所有输出都由本地 Node 24 实际执行得到。

### 明确函数契约

假设请求要新增 `formatShipmentId(prefix, sequence)`。提示词给出公开签名、两个前置条件、确切返回格式和不同异常类型。这些词确定了边界行为，同时把验证布局与字符串构造留给实现决定。

<!-- quick -->

```javascript
// file: shipment_label.js
function formatShipmentId(prefix, sequence) {
  if (!/^[A-Z]{2,4}$/.test(prefix)) {
    throw new TypeError("prefix must be 2-4 uppercase letters");
  }
  if (!Number.isSafeInteger(sequence) || sequence < 0) {
    throw new RangeError("sequence must be a non-negative safe integer");
  }
  return `${prefix}-${String(sequence).padStart(6, "0")}`;
}

for (const [prefix, sequence] of [
  ["EU", 42],
  ["RET", 7],
  ["e", 3],
  ["EU", -1],
]) {
  try {
    console.log(formatShipmentId(prefix, sequence));
  } catch (error) {
    console.log(`${error.name}: ${error.message}`);
  }
}
```

```text
EU-000042
RET-000007
TypeError: prefix must be 2-4 uppercase letters
RangeError: sequence must be a non-negative safe integer
```


<!-- /quick -->

这里的六位规则表示最小宽度，而不是最大宽度，`padStart()` 不会截断更长的数字。如果提示词要求「恰好六位」，还必须设置上限。第三个破坏用例暴露了另一个决定：正则表达式对非字符串返回 false，因此代码会给出与格式错误字符串相同的 `TypeError` 消息。

形参名称属于定义，而 `"EU"` 与 `42` 是某个调用位置的实参。有了这种区别，后续请求就可以说「保留两个形参，但增加一个 sequence 为 `0` 的调用位置测试」，不会把输入槽与一次提供的值混为一谈。

### 在失败路径中保护不变量

座位预订契约把 `available >= 0` 声明为不变量。预期领域失败返回带标签的错误值，格式错误的存储状态会抛出异常，成功路径则返回新库存对象而不修改输入。这些选择使失败转换可观察、可测试。

```javascript
// file: reserve_seats.js
function reserveSeats(inventory, requested) {
  if (!Number.isSafeInteger(inventory.available) || inventory.available < 0) {
    throw new RangeError("inventory.available must be non-negative");
  }
  if (!Number.isSafeInteger(requested) || requested <= 0) {
    return { ok: false, error: "INVALID_QUANTITY" };
  }
  if (requested > inventory.available) {
    return { ok: false, error: "INSUFFICIENT_SEATS" };
  }
  return {
    ok: true,
    inventory: { ...inventory, available: inventory.available - requested },
  };
}

const initial = Object.freeze({ eventId: "conf-2026", available: 3 });
let current = initial;

for (const requested of [2, 2, 0]) {
  const result = reserveSeats(current, requested);
  console.log(JSON.stringify(result));
  if (result.ok) current = result.inventory;
}

console.log(`initial=${initial.available}, current=${current.available}`);
```

```text
{"ok":true,"inventory":{"eventId":"conf-2026","available":1}}
{"ok":false,"error":"INSUFFICIENT_SEATS"}
{"ok":false,"error":"INVALID_QUANTITY"}
initial=3, current=1
```

第一次转换建立后置条件 `current.available === 1`。库存不足和数量无效的请求都不改变 `current`，所以不变量在两条预期失败路径中仍成立。冻结 `initial` 只是这里的诊断手段；不修改输入的保证来自返回新对象，不能依赖每个调用方自觉冻结输入。

如果实际需求允许部分预订，同样的词会产生错误行为。提示词必须说明只剩一个座位时请求两个座位，是原子性失败、预订一个并报告余量，还是把需求加入队列。仅说「防止库存为负」无法在这些方案中作出选择。

### 分离编排与副作用

收据任务把 `sendReceipt` 定义为编排逻辑，并把模板加载与发送保留在注入的依赖函数之后。返回状态与发送副作用相互独立。借助这个接缝，测试无需访问网络服务就能观察确切消息。

```javascript
// file: send_receipt.js
async function sendReceipt(order, { loadTemplate, deliver }) {
  const template = await loadTemplate(order.locale);
  const body = template
    .replace("{customer}", order.customer)
    .replace("{total}", order.total);

  await deliver({
    to: order.email,
    subject: `Receipt ${order.id}`,
    body,
  });

  return { status: "sent", orderId: order.id };
}

const deliveries = [];
const dependencies = {
  loadTemplate: async (locale) =>
    locale === "fr" ? "Bonjour {customer}: {total}" : "Hello {customer}: {total}",
  deliver: async (message) => deliveries.push(message),
};

const result = await sendReceipt(
  {
    id: "A-17",
    customer: "Mina",
    email: "mina@example.test",
    locale: "fr",
    total: "24.00 EUR",
  },
  dependencies,
);

console.log(JSON.stringify(result));
console.log(JSON.stringify(deliveries));
```

```text
{"status":"sent","orderId":"A-17"}
[{"to":"mina@example.test","subject":"Receipt A-17","body":"Bonjour Mina: 24.00 EUR"}]
```

这个边界已经足够精确，可以测试，但失败契约仍不完整。生产提示词还必须说明模板加载或发送被拒绝时怎么办、发送能否重试，以及是否允许重复收据。成功路径输出无法回答这些产品问题。

「模拟邮件服务」会把请求绑定到一种测试技术。「把发送操作保留在注入的可调用对象之后，并断言穿过该接缝的消息」说明了可替换边界与观察方式。手写替身、侦测对象或框架 mock 都可以满足要求。

## 陷阱

### 使用没有观察结果的宽泛动词

> **陷阱:** 「修复」「处理」「支持」「优化」与「清理」没有说明调用方会看到什么变化。智能体可能在产品需要拒绝时增加回退，也可能在数据必须保持新鲜时缓存结果。

**修复方法：** 在动词后写出前后观察结果。至少说明一个输入、输出或副作用，以及一个重要反例。把「处理用户缺失」改成「用户不存在时返回 `null`；数据库失败保持向上传播，且不要记录查询成功」。

### 混淆行为与偏好的实现

> **陷阱:** 「使用类让重试可靠」只规定结构，没有定义可靠性。生成的类仍可能重复副作用、重试永久失败，或者忽略取消操作。

**修复方法：** 先说明可重试失败类别、尝试次数上限、退避输入、取消行为与幂等效果。只有对象标识、协议实现、状态所有权或仓库约定使类成为设计约束时，才要求使用类。

### 说「保持 API 不变」却不说明使用方

> **陷阱:** API 可能指函数签名、JSON 模式、HTTP 状态、事件主题、CLI 输出或全部内容。一项改动可能保留一个表面，却通过错误文本、字段缺失或调用顺序破坏另一个使用方。

**修复方法：** 列出受保护的使用方与可观察维度。例如：「保留对 `loadUser(id, timeout=...)` 的位置调用和关键字调用，维持返回字段与异常类型，不改变 CLI 包装器的退出码。」

### 说明不变量却不说明边界

> **陷阱:** 如果待处理冻结、并发请求或外部账本可以暂时不一致，「余额绝不为负」就有歧义。实现可能在错误阶段执行规则，或者只在内存中检查。

**修复方法：** 明确权威状态与观察时刻。可以写成：「每次 `withdraw` 事务提交后，数据行的可用余额非负；被拒绝的事务既不写账本记录，也不修改余额。」操作可能重叠时，应增加并发测试。

### 把异常、错误结果与日志当作同义词

> **陷阱:** 要求「返回错误」可能产生抛出的异常、被拒绝的 Promise、带标签的值、哨兵、HTTP 响应或日志消息。每种表示都会让调用方采取不同行为，而仅记录日志不会把失败传给调用方。

**修复方法：** 指定通道、类型、稳定代码、消息要求与传播规则。区分预期领域结果、无效的程序员输入与不可用依赖。说明失败出现前是否可能产生任何副作用。

### 在提示词中堆积未解释的术语

> **陷阱:** 密集的请求可以同时使用「适配器」「仓库」「原子性」和「纯函数」，但团队对这些词的理解可能不同。此时词汇只会制造虚假的确定感，不能形成共同含义。

**修复方法：** 把每个会影响结果的术语对应到文件、符号、谓词或示例。复用仓库文档中已有的名称。如果某个词会改变架构，却找不到证据说明其含义，应让智能体在编辑前报告歧义。

<!-- deep -->

## 把词汇变成改动契约

精确的提示词仍不是形式化规格。自然语言术语会继承仓库约定，有些需求也离不开产品判断。目标是尽早暴露这些决定，让智能体可以询问、调查或停止，而不是把猜测藏进代码。

### 可观察行为优先

先画出使用方边界。对每类使用方，列出穿过边界的内容，以及使用方能区分的现象。只有所有受保护观察都保持等价时，两个内部实现对当前任务才算等价。

有用的观察包括：

- 接受与拒绝的输入域，包括空值、零、最大值、重复值和格式错误值；
- 返回值、模式、顺序、标识、可变性与精度；
- 异常、带标签的失败、状态码、stderr 与退出码；
- 写入、网络调用、消息、日志、指标及其顺序；
- 只有实测阈值属于契约时，才包括延迟、内存或吞吐量。

这份列表能防止「保持行为」沦为含糊的认可，也会显示兼容性在哪里终止。私有辅助函数的堆栈信息可能无关紧要，而 CLI 的确切 stderr 可能被测试或脚本使用，因此需要明确决定。

### 不变量横跨状态转换

不变量比一个预期结果更强。它必须经受构造、成功、预期失败、依赖失败、取消和所有允许的并发。要让不变量可执行，应说明状态所有者、能改变它的转换，以及观察方可以检查它的时刻。

对于 `available >= 0`，要问 `available` 归谁所有、预订是否原子提交，以及待处理冻结是否计入。随后在实际存储边界测试成功扣减、超额请求、重复请求与重叠请求。只测试算术的单元测试无法证明数据库转换。

有些规则是前置条件，不是不变量。「`requested` 必须为正」约束一次调用，却不必在每个对象状态中成立。有些规则是后置条件：「成功后，可用数量恰好减少 requested。」说出类别，才能让时间边界可测试。

### 失败是契约分支

失败路径需要与成功路径同等精确。应记录触发条件、通道、稳定标识、重试规则、已提交的副作用，以及可安全暴露的信息。这样可防止实现把每个捕获的异常都当成同一种领域结果。

| 失败问题 | 精确答案示例 |
| --- | --- |
| 什么会触发失败？ | 请求座位数超过已提交的可用数量 |
| 怎样表示？ | `{ ok: false, error: 'INSUFFICIENT_SEATS' }` |
| 调用方能否重试？ | 可用数量变化后可以；相同重试没有效果 |
| 已经发生了什么？ | 没有库存写入，也没有确认消息 |
| 哪些内容可观察？ | 稳定错误代码，不暴露数据库内部文本 |

「保持原样传播」与「翻译」是一组有用的反义词。保持传播会让调用方继续获得依赖失败的标识与堆栈。翻译会把它映射成边界自有错误，因此需要明确原因保留策略，避免悄悄丢失调试信息或暴露敏感细节。

### 结构必须有理由

结构词汇应说明所有权与依赖方向。纯函数辅助单元隔离确定性策略，适配器在接口间转换，编排逻辑安排协作者顺序，仓库边界拥有持久化操作。只有代码库已经赋予这些标签稳定含义时，它们才有价值。

要求新抽象前，应说清它要容纳的变化或风险。注入时钟可以在测试中隔离时间。发送接缝可以防止领域策略直接打开网络连接。独立解析器可以限制不可信文本验证范围。缺少这类理由时，增加层次只会提高导航成本，不会让契约更清楚。

内部名称仍会影响维护。应检查新单元是否具有无需使用「并且」就能说清的单一职责，依赖是否指向仓库允许的方向，以及接口是否小于被隐藏的实现。这些检查比「让架构更整洁」具体得多。

### 兼容性是集合，不是口号

评估兼容性时，需要定义过去有效的交互集合。只有相对于这个集合及其观察，新改动才可能向后兼容。存在未知使用方时，无法作出确定保证，所以提示词可以要求搜索、弃用、遥测或迁移，而不是没有依据的承诺。

新增可选形参对许多位置调用方保持源码兼容，却可能破坏反射、生成绑定或接口实现。新增响应字段可能兼容宽容的解码器，却不兼容封闭模式。重排语义等价的结果也可能破坏快照测试或直接展示结果的调用方。

应说明任务是否允许引入兼容层。兼容层可以同时接受新旧形式、发出警告，并在迁移期间集中转换逻辑。它也创造了第二条路径，需要明确移除条件；没有负责人、期限或使用信号的「临时」实现往往会永久存在。

### 证据闭合词汇循环

每个重要名词都应指向检查对象，每项行为主张都应指向证据。签名指向定义与调用位置，不变量指向跨转换谓词，失败模式指向反例测试，边界指向导入关系与副作用轨迹。

先用示例消除规则歧义，再在争议边缘附近加入反例。`formatShipmentId('EU', 42) === 'EU-000042'` 这条示例只确定一个用例的填充方向与宽度。小写前缀、负数和七位数的反例会暴露单个示例尚未决定的问题。

生成的测试可能重复同一个误解，所以每条断言都要与原始改动契约比较。名为「处理无效输入」的测试如果只断言不抛异常，几乎什么也没证明。应要求预期错误通道、稳定代码、状态不变，以及不存在被禁止的副作用。

最终审查应能从提示词术语指向代码库事实：

1. 接口词汇对应具名定义与使用方。
2. 行为词汇对应谓词、示例与反例。
3. 失败词汇对应通道、恢复规则与副作用轨迹。
4. 结构词汇对应文件、依赖方向与所有权。
5. 验收词汇对应实际执行的命令与观察输出。

如果某个术语找不到这种目标，它很可能只是装饰或仍未定义。删除它，在当前上下文中给出定义，或者把它变成需要产品负责人回答的问题。

<!-- /deep -->

[检查点: ai-era/code-prompt-vocabulary](https://codewiki.com/zh/ai-era/code-prompt-vocabulary/#checkpoint)

## 延伸阅读

- [GitHub 文档：GitHub Copilot Chat 提示工程](https://docs.github.com/en/copilot/concepts/prompting/prompt-engineering)
- [Python 文档：形参与实参术语](https://docs.python.org/3/glossary.html#term-parameter)
- [语义化版本 2.0.0：公开 API 与不兼容变更](https://semver.org/)
- [RFC 9110：幂等方法](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2)
