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 响应体。

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

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.Router

3.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 方法

NewGetRoute(path, handler, opts...)

GET

NewPostRoute(...)

POST

NewPutRoute(...)

PUT

NewDeleteRoute(...)

DELETE

NewOptionsRoute(...)

OPTIONS

NewHeadRoute(...)

HEAD

NewRoute(method, path, handler, opts...)

任意

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

/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.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.1VersionMiddlewaremiddleware/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 之前隐藏某些字段)。

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

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

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

4.3.3DebugRequestMiddlewaremiddleware/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

// 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", ...) }

三个细节:

  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

创建 spanGET /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. 时序图:注册阶段

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()里追加一行:
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,...}

各资源域具体路由