moby-http-api-server

入口文件:daemon/server/server.go

适用范围:Docker/Moby daemon 暴露的 RESTful HTTP API(即 dockerd 监听的 TCP/Unix socket 接口)

1. 总览

daemon/server/server.go 定义了 Docker daemon HTTP API 的核心 Server 结构体。它本身不绑定监听地址(监听由 daemon 上层负责,比如 unix socket / tcp / fd),它的职责是:

  1. 接收一组 router.Router(每个 router 负责一个资源域,例如 container、image、volume、system、network、swarm 等)。
  1. 把它们的所有 Route注册到 gorilla/mux路由器 ,同时自动为每条路由生成两个 URL:一个带版本前缀 /v{version}/...,一个不带版本前缀 /...
  1. 在路由和最终业务 handler 之间串联一组全局中间件(version、experimental、debug 等)。
  1. 统一错误处理 :handler 返回 error,由 server 转换成 HTTP status code 和 JSON 响应体。

整体调用链(自外向内):

Go 复制代码
HTTP 请求
  └─ otelhttp.NewHandler                       (OpenTelemetry 包装,span = "METHOD path")
      └─ Server.makeHTTPHandler                 (入口:注入 ctx、调用中间件链、错误统一处理)
          └─ Server.handlerWithGlobalMiddlewares (VersionMiddleware → ExperimentalMiddleware → ... → 业务 handler)
              └─ route.Handler()                 (真正的业务函数,例如 getVolumesList)

2. 核心类型与常量

2.1 versionMatcher(版本前缀)

Go 复制代码
const versionMatcher = "/v{version:[0-9.]+}"

这是整个 API 路由的版本协商核心。{version:[0-9.]+}gorilla/mux 的变量捕获:

  • 捕获到的值会出现在 vars["version"] 里;
  • mux 把它注入到 request 的 context(通过 mux.Vars(r));
  • 后续 VersionMiddleware 取出该值,与 minAPIVersion / defaultAPIVersion 比对。

最终暴露的 URL 形如:

Go 复制代码
GET /v1.42/volumes           ← 带版本前缀(客户端显式协商版本)
GET /volumes                 ← 不带版本前缀(使用默认版本)

非常精简,只持有一个全局中间件链。router.Router 不直接挂在 Server 上,而是在 CreateMux 时作为参数传入。这种设计让 Server 与"具体有哪些业务路由"解耦------上层 daemon 负责按需装配 router 集合。

2.3 statusClientClosedRequest = 499

非标准 HTTP 状态码(来自 NGINX 习惯),用于客户端在响应返回前断开连接的场景。仅写入响应以供 OTEL/metrics 看到,并写一条 info 日志,不会真正发给客户端(连接已断)。


3. 路由注册:CreateMux

Go 复制代码
func (s *Server) CreateMux(ctx context.Context, routers ...router.Router) *mux.Router

3.1 注册流程

Go 复制代码
for each apiRouter in routers:
    for each r in apiRouter.Routes():
        f := s.makeHTTPHandler(r)
        m.Path(versionMatcher + r.Path()).Methods(r.Method()).Handler(f)   // /v1.42/...
        m.Path(r.Path()).Methods(r.Method()).Handler(f)                    // /...

要点:

  • 每条路由会被注册两次 ------一次带版本前缀,一次不带。这让客户端既可以显式指定版本(curl /v1.41/info),也可以省略(curl /info)走默认版本。
  • 注册是可中断的 :每条路由注册前都会检查 ctx.Err(),便于 daemon 关闭时提前结束初始化。
  • 逐条 debug 日志 :注册过程在 debug 级别日志下打印每一条 method/path,便于排查"路由没生效"类问题。

3.2 兜底路由(405 / 404)

Go 复制代码
m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler)
m.NotFoundHandler = notFoundHandler
m.MethodNotAllowedHandler = notFoundHandler

所有未匹配的路径(包括带版本前缀但路径不存在的请求)统一返回:

Go 复制代码
HTTP/1.1 404 Not Found
Content-Type: application/json

{"message":"page not found"}

MethodNotAllowedHandler 也被设置为 notFoundHandler ------ 这是有意为之:默认 mux 在方法不匹配时会返回 405,但 Docker 选择把它一并归到 404,避免泄漏"端点存在但方法不对"的信息。

3.3 router.Router 接口

每个资源域(container、image、volume...)都实现:

Go 复制代码
type Router interface {
    Routes() []Route
}

type Route interface {
    Handler() httputils.APIFunc   // func(ctx,w,r,vars) error
    Method() string               // GET / POST / PUT / DELETE / OPTIONS / HEAD
    Path() string                 // 例如 "/volumes/{name:.*}"
}

构造路由的便捷函数(见 router/local.go):

|--------------------------------------------|---------|
| 函数 | HTTP 方法 |
| NewGetRoute(path, handler, opts...) | GET |
| NewPostRoute(...) | POST |
| NewPutRoute(...) | PUT |
| NewDeleteRoute(...) | DELETE |
| NewOptionsRoute(...) | OPTIONS |
| NewHeadRoute(...) | HEAD |
| NewRoute(method, path, handler, opts...) | 任意 |

opts ...RouteWrapper 是路由级装饰器(注意:与下面的全局 middleware 不同),典型用法:

Go 复制代码
router.NewPostRoute("/volumes/prune", v.postVolumesPrune, router.WithMinimumAPIVersion("1.25"))
router.NewPutRoute("/volumes/{name:.*}", v.putVolumesUpdate, router.WithMinimumAPIVersion("1.42"))

WithMinimumAPIVersion 在 handler 外再套一层"版本不够直接 400"的壳,使旧客户端看不到新端点。注意它返回 400 而不是 404------因为业务 handler 普遍用 404 表示"对象不存在"(例如 GET /volumes/foo 找不到该 volume),如果端点本身也 404,客户端无法区分。

3.4 一个具体例子:volume 路由

Go 复制代码
func (v *volumeRouter) initRoutes() {
    v.routes = []router.Route{
        router.NewGetRoute("/volumes", v.getVolumesList),
        router.NewGetRoute("/volumes/{name:.*}", v.getVolumeByName),
        router.NewPostRoute("/volumes/create", v.postVolumesCreate),
        router.NewPostRoute("/volumes/prune", v.postVolumesPrune, router.WithMinimumAPIVersion("1.25")),
        router.NewPutRoute("/volumes/{name:.*}", v.putVolumesUpdate, router.WithMinimumAPIVersion("1.42")),
        router.NewDeleteRoute("/volumes/{name:.*}", v.deleteVolumes),
    }
}

经过 CreateMux 注册后,最终生效的 URL(节选):

|------|------------------------------------------------|-------------|
| 方法 | URL | 备注 |
| GET | /volumes/v{version}/volumes | 列出卷 |
| GET | /volumes/{name}/v{version}/volumes/{name} | 查单个卷 |
| POST | /volumes/prune/v{version}/volumes/prune | 最低 API 1.25 |
| PUT | /volumes/{name}/v{version}/volumes/{name} | 最低 API 1.42 |


4. 中间件链

4.1 Middleware 接口

Go 复制代码
type Middleware interface {
    WrapHandler(APIFunc) APIFunc
}

任何结构体只要实现 WrapHandler 就是一个中间件。注意它的方向是"洋葱式"的:WrapHandler(next) 返回的新 handler 在调 next 前后做自己的事。

4.2 装配顺序

Go 复制代码
// daemon/server/middleware.go
func (s *Server) handlerWithGlobalMiddlewares(handler httputils.APIFunc) httputils.APIFunc {
    next := handler
    for _, m := range s.middlewares {
        next = m.WrapHandler(next)
    }
    if log.GetLevel() >= log.DebugLevel {
        next = middleware.DebugRequestMiddleware(next)
    }
    return next
}

关键注释原文:

The order of the middlewares is backwards, meaning that the first in the list will be evaluated last.

也就是说,s.middlewares[0]是最外层 (最先看到请求、最后看到响应),s.middlewares[len-1] 是最内层(紧贴业务 handler)。装配时是反向 wrap,所以代码上看是顺序 append,运行时是栈式调用。

如果日志级别 ≥ Debug,会再额外套一层 DebugRequestMiddleware(最内层,靠近业务)。

4.3 三个内置中间件

4.3.1 VersionMiddlewaremiddleware/version.go

最关键的中间件,承担三件事:

  1. 写响应头Server: Docker/<serverVersion> (<GOOS>)Api-Version: <defaultAPIVersion>Ostype: <GOOS>
  1. 版本协商 :从 vars["version"] 取客户端请求的版本;为空时用 defaultAPIVersion 兜底。
  1. 版本范围检查 :低于 minAPIVersion 或高于 defaultAPIVersion 都返回 versionUnsupportedError(带 InvalidParameter() 标记 → 400)。
  1. 把版本塞进 ctxctx = context.WithValue(ctx, APIVersionKey{}, apiVersion),业务 handler 通过 httputils.VersionFromContext(ctx) 读取,按版本走不同分支(典型场景:API 1.44 之前隐藏某些字段)。

构造函数会做完整性校验:

Go 复制代码
NewVersionMiddleware(serverVersion, defaultAPIVersion, minAPIVersion)
// 要求:minAPIVersion ≤ defaultAPIVersion
// 且都在 [config.MinAPIVersion, config.MaxAPIVersion] 区间内
4.3.2 ExperimentalMiddlewaremiddleware/experimental.go

只做一件事:在每个响应上加 Docker-Experimental: true|false 头。客户端可以据此判断当前 daemon 是否开启了实验特性。

4.3.3 DebugRequestMiddlewaremiddleware/debug.go

仅在 debug 日志级别生效。会:

  • 打印每条请求的 methodrequest-urlvars
  • 对 POST 请求,若 Content-Type 是 JSON 且 body ≤ 4KB,会读取并解析 body,屏蔽敏感字段后passwordsecretdatajointokensigningcakeyunlockkey)输出到 form-data 字段;
  • handler 出错时把 error-responsestatus 也加进日志字段。

安全提示:敏感字段屏蔽是基于字段名的"白名单 + 大小写不敏感"匹配。新增加密字段的 API 端点必须同步更新 scrub 列表,否则明文会出现在 debug 日志里。

4.4 路由级装饰器 vs 全局中间件

容易混淆,这里区分一下:

|------|-----------------------------------------|------------------------------------------------------------------|
| 维度 | 全局 Middleware | 路由级 RouteWrapper(如 WithMinimumAPIVersion) |
| 定义位置 | middleware/*.go,实现 Middleware 接口 | router/local.gorouter/experimental.go,是 func(Route) Route |
| 装配方式 | Server.UseMiddleware(...),对所有路由生效 | router.NewPostRoute(path, h, opts...),只对这一条路由生效 |
| 执行时机 | 在 handlerWithGlobalMiddlewares 里串联 | 在路由构造时就把原 handler 包了一层 |
| 运行顺序 | 全局中间件链 → 路由级 wrapper → 真正业务 handler | 见左 |

4.5 实验性路由:Experimental

Go 复制代码
// router/experimental.go
func Experimental(r Route) Route

把任意 route 标记为实验性。被标记后默认走 experimentalHandler,直接返回:

Go 复制代码
This experimental feature is disabled by default. Start the Docker daemon in experimental mode in order to enable it.

且实现了 NotImplemented() 接口,会被 httpstatus.FromError 映射为 501 Not Implemented 。daemon 启动时如果开了实验特性,会调用 Enable() 把 handler 切回真正的业务函数。


5. 请求处理:makeHTTPHandler

这是每条路由真正的 http.HandlerFunc。它把 gorilla/mux 风格的 (w, r) 适配到 Docker 内部的 APIFunc(ctx, w, r, vars) 风格,并完成四件事:

5.1 注入 context

Go 复制代码
ua := r.Header.Get("User-Agent")
ctx := baggage.ContextWithBaggage(
    dockerversion.WithUpstreamUserAgent(r.Context(), ua),
    otelutil.MustNewBaggage(
        otelutil.MustNewMemberRaw(otelutil.TriggerKey, "api"),
    ),
)
r = r.WithContext(ctx)
  • User-Agent 写进 ctx(用于追踪"是谁发起的 API 调用",区分 CLI / 客户端库);
  • 添加 OTel baggage,标记 trigger=api,便于 telemetry 区分 API 触发的事件和其他触发源。

5.2 串联中间件并执行

Go 复制代码
handlerFunc := s.handlerWithGlobalMiddlewares(handler)
vars := mux.Vars(r)
if err := handlerFunc(ctx, w, r, vars); err != nil { ... }

5.3 客户端断连分支

Go 复制代码
if r.Context().Err() != nil {
    w.WriteHeader(statusClientClosedRequest) // 499, 仅用于 metrics/OTEL
    log.G(ctx).WithFields(...).Info("request cancelled by client")
    return
}

这是优先级最高的分支:只要请求 context 已经取消(客户端走了),就别再尝试写 JSON 错误体。强行写会污染日志(写失败)、浪费带宽(虽然已断)。这里只写状态码(给中间件的 metrics 看)和一条 info 日志。

5.4 错误转换

Go 复制代码
statusCode := httpstatus.FromError(err)
if v := vars["version"]; v != "" && versions.LessThan(v, "1.24") {
    http.Error(w, err.Error(), statusCode)            // 老客户端:纯文本
} else {
    httputils.WriteJSON(w, statusCode, &common.ErrorResponse{
        Message: err.Error(),                          // 新客户端:JSON
    })
}
if statusCode >= 500 {
    log.G(ctx).Errorf("Handler for %s %s returned error", ...)
}

三个细节:

  1. httpstatus.FromError 根据 error 实现的标记接口(NotFound()InvalidParameter()Conflict() 等)映射到 HTTP status。比如 errdefs.NotFound(err) → 404,errdefs.InvalidParameter(err) → 400,gRPC 错误和 distribution registry 错误也有专门分支。
  1. API < 1.24 走纯文本:Docker 已经不再支持 1.24 以下,但万一有极老客户端连进来,给它返回 JSON 会显示成一坨原始 JSON 文本,体验更差。注释里写"Let's be nice"------对已经不支持的客户端仍尽量给可读错误。
  1. 5xx 才记 error 日志:4xx 是客户端问题(参数错、没权限、对象不存在),算正常业务流量,不污染错误日志。

5.5 OTel 包装

整个 http.HandlerFunc 最外层被 otelhttp.NewHandler(..., operation) 包裹,operation = "METHOD path"(例如 "GET /volumes")。这一层负责创建 span、记录耗时和状态码,与业务逻辑解耦。


6. 典型请求生命周期示例

请求:GET /v1.42/volumes HTTP/1.1

|----|--------------------------------------|--------------------------------------------------------------------------------------|
| 步骤 | 组件 | 动作 |
| 1 | otelhttp | 创建 span GET /volumes |
| 2 | mux 路由匹配 | 命中 /v{version:[0-9.]+}/volumesvolumeRouter.getVolumesList |
| 3 | makeHTTPHandler | 注入 ctx(UA、baggage) |
| 4 | VersionMiddleware | 校验 1.42 ∈ min, default;写 Server / Api-Version / Ostype 响应头;把 1.42 塞进 ctx |
| 5 | ExperimentalMiddleware | 写 Docker-Experimental: false(或 true) |
| 6 | DebugRequestMiddleware(仅 debug 级别) | 打印请求字段 |
| 7 | getVolumesList | 解析 form、调用 backend.List、按版本决定是否查 swarm 集群卷、合并、WriteJSON |
| 8 | 返回 | span 收尾,状态码 200 |

如果是请求 /volumes(无版本前缀),步骤 4 中 vars["version"] == "",自动落到 defaultAPIVersion,其余流程一致。


7. 时序图:注册阶段

Go 复制代码
daemon.NewDaemon
   │
   ├─ 创建各个 backend(container/image/volume/network/swarm/...)
   │
   ├─ 各 router.NewRouter(backend, cluster, ...) ──→ []router.Router
   │
   ├─ server.Server{}.UseMiddleware(versionMiddleware)
   ├─ server.Server{}.UseMiddleware(experimentalMiddleware)
   │
   └─ mux := srv.CreateMux(ctx, allRouters...)
            │
            ├─ for each router, for each route:
            │     makeHTTPHandler(route)               ← 包装 otelhttp + 中间件链
            │     mux.Path("/v{version:[0-9.]+}" + path).Methods(method).Handler(f)
            │     mux.Path(path).Methods(method).Handler(f)
            │
            ├─ NotFoundHandler / MethodNotAllowedHandler → JSON 404
            └─ return mux  (由上层 daemon 绑定到 http.Server 监听)

8. 扩展指南

8.1 新增一条 API 路由

  1. 找到对应资源域的 router(如 daemon/server/router/volume/volume.go)。
  1. initRoutes() 里追加一行:
Go 复制代码
router.NewPostRoute("/volumes/{name}/foo", v.postVolumeFoo,
    router.WithMinimumAPIVersion("1.45"))

3.在同包内实现postVolumeFoo(ctx, w, r, vars) error,返回错误时用errdefs包裹以获得正确的 HTTP status。

4无需修改server.go------CreateMux会自动注册带版本前缀和不带前缀两个 URL。

8.2 新增一个全局中间件

1在daemon/server/middleware/下新建文件,定义实现Middleware接口的结构体。

2在 daemon 启动时调用srv.UseMiddleware(yourMiddleware)。

3注意装配顺序:先 append 的中间件在最外层。VersionMiddleware通常应该最外层(确保所有响应都带版本头),所以它一般第一个被 append。

8.3 新增一个 API 版本

参见daemon/config的MinAPIVersion/MaxAPIVersion。新增版本时要:

1在每个端点上按需追加WithMinimumAPIVersion("x.yy")或在 handler 内用httputils.VersionFromContext(ctx)做版本分支(隐藏新字段或新行为)。

2更新VersionMiddleware的构造参数,使其覆盖新版本。

3不要破坏旧版本的行为------这是 Docker API 的兼容性硬约束。

  1. 文件索引

|------------------------------------------------------------------------|----------------------------------------------------|
| 文件 | 作用 |
| daemon/server/server.go | 入口:Server、CreateMux、makeHTTPHandler |
| daemon/server/middleware.go | handlerWithGlobalMiddlewares(中间件链装配) |
| daemon/server/middleware/middleware.go | Middleware接口 |
| daemon/server/middleware/version.go | 版本协商 + 响应头注入 |
| daemon/server/middleware/experimental.go | Docker-Experimental响应头 |
| daemon/server/middleware/debug.go | debug 日志中间件 + 敏感字段屏蔽 |
| daemon/server/router/router.go | Router/Route接口 |
| daemon/server/router/local.go | localRoute+NewGetRoute等便捷构造 +WithMinimumAPIVersion |
| daemon/server/router/experimental.go | Experimental()路由装饰器(501 切换) |
| daemon/server/httputils/httputils.go | APIFunc类型、APIVersionKey、JSON 读写工具 |
| daemon/server/httpstatus/status.go | FromError:error → HTTP status 映射 |
| daemon/server/router/{container,image,volume,system,network,swarm,...} | 各资源域具体路由 |

相关推荐
namexingyun1 小时前
Scaling Law bug实战启示:从“虚胖“到“精瘦“的算力效率革命
开发语言·网络·人工智能·bug·ai编程
云栖梦泽在1 小时前
原生 IP、机房 IP、住宅 IP、广播 IP 有何不同?从网络身份到 ASN 识别的技术科普
网络·网络协议·tcp/ip·性能优化
AI创界者2 小时前
【实战演练】基于 Kali Linux 的自动化漏洞扫描与安全加固指南
网络·安全·web安全
从零开始的代码生活_3 小时前
Linux epoll 多路转接详解
linux·运维·网络·后端·tcp/ip·计算机网络·php
小鹿软件办公3 小时前
通过调整编码参数使用 VLC 媒体播放器无损压缩视频方法
运维·网络·音视频
Zhan8611243 小时前
西班牙股票行情数据API的WebSocket接入:IBEX35指数实时推送与数据清洗
python·websocket·网络协议
yqcoder5 小时前
什么是 Math 对象
网络·网络协议·udp
Piko61414 小时前
H3C IRF2 堆叠实战:打造高可靠核心交换网络
运维·网络·笔记
茯苓gao17 小时前
嵌入式开发笔记:CANopen相关移位运算与通信协议术语详解
网络·嵌入式硬件·学习·信息与通信