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),它的职责是:
- 接收一组
router.Router(每个 router 负责一个资源域,例如 container、image、volume、system、network、swarm 等)。
- 把它们的所有
Route注册到gorilla/mux路由器,同时自动为每条路由生成两个 URL:一个带版本前缀/v{version}/...,一个不带版本前缀/...。
- 在路由和最终业务 handler 之间串联一组全局中间件(version、experimental、debug 等)。
- 统一错误处理:handler 返回
error,由 server 转换成 HTTP status code 和 JSON 响应体。
整体调用链(自外向内):
HTTP 请求 └─ otelhttp.NewHandler (OpenTelemetry 包装,span = "METHOD path") └─ Server.makeHTTPHandler (入口:注入 ctx、调用中间件链、错误统一处理) └─ Server.handlerWithGlobalMiddlewares (VersionMiddleware → ExperimentalMiddleware → … → 业务 handler) └─ route.Handler() (真正的业务函数,例如 getVolumesList)2. 核心类型与常量
2.1versionMatcher(版本前缀)
const versionMatcher = "/v{version:[0-9.]+}"这是整个 API 路由的版本协商核心。{version:[0-9.]+}是gorilla/mux的变量捕获:
- 捕获到的值会出现在
vars["version"]里;
mux把它注入到request的 context(通过mux.Vars(r));
- 后续
VersionMiddleware取出该值,与minAPIVersion/defaultAPIVersion比对。
最终暴露的 URL 形如:
GET /v1.42/volumes ← 带版本前缀(客户端显式协商版本) GET /volumes ← 不带版本前缀(使用默认版本)非常精简,只持有一个全局中间件链。router.Router不直接挂在Server上,而是在CreateMux时作为参数传入。这种设计让Server与"具体有哪些业务路由"解耦——上层 daemon 负责按需装配 router 集合。
2.3statusClientClosedRequest = 499
非标准 HTTP 状态码(来自 NGINX 习惯),用于客户端在响应返回前断开连接的场景。仅写入响应以供 OTEL/metrics 看到,并写一条 info 日志,不会真正发给客户端(连接已断)。
3. 路由注册:CreateMux
func (s *Server) CreateMux(ctx context.Context, routers ...router.Router) *mux.Router3.1 注册流程
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)
m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler) m.NotFoundHandler = notFoundHandler m.MethodNotAllowedHandler = notFoundHandler所有未匹配的路径(包括带版本前缀但路径不存在的请求)统一返回:
HTTP/1.1 404 Not Found Content-Type: application/json {"message":"page not found"}MethodNotAllowedHandler也被设置为notFoundHandler—— 这是有意为之:默认mux在方法不匹配时会返回 405,但 Docker 选择把它一并归到 404,避免泄漏"端点存在但方法不对"的信息。
3.3router.Router接口
每个资源域(container、image、volume…)都实现:
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 方法 |
| GET |
| POST |
| PUT |
| DELETE |
| OPTIONS |
| HEAD |
| 任意 |
opts ...RouteWrapper是路由级装饰器(注意:与下面的全局 middleware 不同),典型用法:
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 路由
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 |
| 列出卷 |
GET |
| 查单个卷 |
POST |
| 最低 API 1.25 |
PUT |
| 最低 API 1.42 |
4. 中间件链
4.1Middleware接口
type Middleware interface { WrapHandler(APIFunc) APIFunc }任何结构体只要实现WrapHandler就是一个中间件。注意它的方向是"洋葱式"的:WrapHandler(next)返回的新 handler 在调next前后做自己的事。
4.2 装配顺序
// 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.1VersionMiddleware(middleware/version.go)
最关键的中间件,承担三件事:
- 写响应头:
Server: Docker/<serverVersion> (<GOOS>)、Api-Version: <defaultAPIVersion>、Ostype: <GOOS>。
- 版本协商:从
vars["version"]取客户端请求的版本;为空时用defaultAPIVersion兜底。
- 版本范围检查:低于
minAPIVersion或高于defaultAPIVersion都返回versionUnsupportedError(带InvalidParameter()标记 → 400)。
- 把版本塞进 ctx:
ctx = context.WithValue(ctx, APIVersionKey{}, apiVersion),业务 handler 通过httputils.VersionFromContext(ctx)读取,按版本走不同分支(典型场景:API 1.44 之前隐藏某些字段)。
构造函数会做完整性校验:
NewVersionMiddleware(serverVersion, defaultAPIVersion, minAPIVersion) // 要求:minAPIVersion ≤ defaultAPIVersion // 且都在 [config.MinAPIVersion, config.MaxAPIVersion] 区间内4.3.2ExperimentalMiddleware(middleware/experimental.go)
只做一件事:在每个响应上加Docker-Experimental: true|false头。客户端可以据此判断当前 daemon 是否开启了实验特性。
4.3.3DebugRequestMiddleware(middleware/debug.go)
仅在 debug 日志级别生效。会:
- 打印每条请求的
method、request-url、vars;
- 对 POST 请求,若 Content-Type 是 JSON 且 body ≤ 4KB,会读取并解析 body,屏蔽敏感字段后(
password、secret、data、jointoken、signingcakey、unlockkey)输出到form-data字段;
- handler 出错时把
error-response和status也加进日志字段。
安全提示:敏感字段屏蔽是基于字段名的"白名单 + 大小写不敏感"匹配。新增加密字段的 API 端点必须同步更新scrub列表,否则明文会出现在 debug 日志里。
4.4 路由级装饰器 vs 全局中间件
容易混淆,这里区分一下:
维度 | 全局 | 路由级 |
定义位置 |
|
|
装配方式 |
|
|
执行时机 | 在 | 在路由构造时就把原 handler 包了一层 |
运行顺序 | 全局中间件链 → 路由级 wrapper → 真正业务 handler | 见左 |
4.5 实验性路由:Experimental
// router/experimental.go func Experimental(r Route) Route把任意 route 标记为实验性。被标记后默认走experimentalHandler,直接返回:
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
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 串联中间件并执行
handlerFunc := s.handlerWithGlobalMiddlewares(handler) vars := mux.Vars(r) if err := handlerFunc(ctx, w, r, vars); err != nil { ... }5.3 客户端断连分支
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 错误转换
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", ...) }三个细节:
httpstatus.FromError根据 error 实现的标记接口(NotFound()、InvalidParameter()、Conflict()等)映射到 HTTP status。比如errdefs.NotFound(err)→ 404,errdefs.InvalidParameter(err)→ 400,gRPC 错误和 distribution registry 错误也有专门分支。
- API < 1.24 走纯文本:Docker 已经不再支持 1.24 以下,但万一有极老客户端连进来,给它返回 JSON 会显示成一坨原始 JSON 文本,体验更差。注释里写"Let's be nice"——对已经不支持的客户端仍尽量给可读错误。
- 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 |
| 创建 span |
2 |
| 命中 |
3 |
| 注入 ctx(UA、baggage) |
4 |
| 校验 1.42 ∈ [min, default];写 |
5 |
| 写 |
6 |
| 打印请求字段 |
7 |
| 解析 form、调用 |
8 | 返回 | span 收尾,状态码 200 |
如果是请求/volumes(无版本前缀),步骤 4 中vars["version"] == "",自动落到defaultAPIVersion,其余流程一致。
7. 时序图:注册阶段
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 路由
- 找到对应资源域的 router(如
daemon/server/router/volume/volume.go)。
- 在
initRoutes()里追加一行:
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 的兼容性硬约束。
9. 文件索引
文件 | 作用 |
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,...} | 各资源域具体路由 |