入门

ConnectRPC(原 Connect)是一组用于构建浏览器和 gRPC 兼容 HTTP API 的库:编写简短的 Protocol Buffer schema 并实现业务逻辑,Connect 会生成代码来处理序列化、路由、压缩和内容协商,并为所有受支持的语言生成惯用、类型安全的客户端。在 Go 中,Connect 只有一个包(connect-go),短到可以一个下午读完。

多协议支持

Connect 服务器和客户端支持三种协议:

  1. gRPC:完全支持,包括流式、trailers 和错误详情。任何语言的 gRPC 客户端都可以调用 Connect 服务器,Connect 客户端也可以调用任何 gRPC 服务器(官方用扩展版的 Google 互操作性测试验证兼容性)
  2. gRPC-Web:直接支持,无需依赖 Envoy 之类的转换代理
  3. Connect 协议:基于 HTTP 的简单协议,可运行在 HTTP/1.1、HTTP/2 和 HTTP/3 上,默认同时支持 JSON 和二进制 Protobuf 编码

服务器默认接受全部三种协议的入口;客户端默认使用 Connect 协议,通过配置即可切换到 gRPC 或 gRPC-Web,无需修改其他代码。错误、header、trailer 和流式 API 都是协议无关的。

可以用 cURL 直接体验官方 demo 服务(Eliza 聊天机器人):

curl \
    --header "Content-Type: application/json" \
    --data '{"sentence": "I feel happy."}' \
    https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say

压缩和序列化

Connect 支持多种压缩和序列化选项,协议层支持 identity、gzip、br、zstd 四种 Content-Encoding。默认情况下,Connect 处理程序使用标准库的 compress/gzip 提供 gzip 压缩。Connect 客户端默认发送未压缩的请求并请求 gzip 压缩的响应。如果您知道服务器支持 gzip,则还可以在客户端构建期间使用 WithSendGzip 选项来压缩请求。

// 服务端不需要进行什么选项设置 参考https://github.com/connectrpc/connect-go/issues/773
handler := greetv1connect.NewGreetServiceHandler(
    &GreetServer{},
)

// 客户端配置压缩,默认
client := greetv1connect.NewGreetServiceClient(
    http.DefaultClient,
    "http://localhost:8080",
    connect.WithSendGzip(),
)

自定义压缩实现:

type CustomCompressor struct{}

func (c *CustomCompressor) Name() string { return "custom" }
func (c *CustomCompressor) Compress(w io.Writer) (io.WriteCloser, error)
func (c *CustomCompressor) Decompress(r io.Reader) (io.Reader, error)

connect的brotli压缩 (第三方包,使用时通过 connect.WithSendCompression(brotli.Name) 指定)

JSON 序列化注意点

  • proto3 的 JSON 映射中 int64、fixed64、uint64 会序列化为字符串(因为 JavaScript 的 Number 无法精确表示 64 位整数)
  • Connect 客户端会自动完成数值与字符串的转换,但用 cURL、浏览器 fetch 等纯 HTTP 工具直接调用时要留意
  • Connect 客户端和服务端会忽略未知 JSON 字段,这样 schema 可以平滑演进而不破坏旧客户端

Get 请求

Connect 支持通过 HTTP GET 进行无副作用的请求,这使得可以在浏览器、CDN 或代理中缓存某些类型的请求。

在 proto 文件中标记方法,使用[MethodOptions.IdempotencyLevel ](https://github.com/protocolbuffers/protobuf/blob/e5679c01e8f47e8a5e7172444676bda1c2ada875/src/google/protobuf/descriptor.proto#L795] 选项将其标记为无副作用。

service ElizaService {
  rpc Say(SayRequest) returns (SayResponse) {
    option idempotency_level = NO_SIDE_EFFECTS;
  }
}

客户端启用 GET 请求:

client := elizav1connect.NewElizaServiceClient(
    http.DefaultClient,
    connect.WithHTTPGet(),
)

GET 请求会把消息编码进 URL query 参数(v1.20.0 起参数顺序符合规范建议):

GET /connectrpc.greet.v1.GreetService/Greet?encoding=json&message=%7B%22name%22%3A%22Buf%22%7D

仅当使用 Connect 协议(将 Connect 客户端与 Connect 服务一起使用)时,才支持此功能。将 gRPC 客户端与 Connect 服务器一起使用,或将 Connect 客户端与 gRPC 服务器一起使用时,所有请求都将使用 HTTP POST。如果您在与原版 gRPC 服务器通信时需要 HTTP GET 支持,则可以使用代理。Envoy 支持使用 Connect-gRPC Bridge 在 Connect 客户端和 gRPC 服务器之间进行转换。

进阶

HTTP/2 与 HTTP/3

  1. HTTP/2 支持:

服务端(h2c,明文 HTTP/2 升级)

package main

import (
  "net/http"

  "golang.org/x/net/http2"
  "golang.org/x/net/http2/h2c"
)

func main() {
  mux := http.NewServeMux()
  // Mount some handlers here.
  server := &http.Server{
    Addr: ":http",
    Handler: h2c.NewHandler(mux, &http2.Server{}),
    // Don't forget timeouts!
  }
}

客户端

package main

import (
  "crypto/tls"
  "net"
  "net/http"

  "golang.org/x/net/http2"
)

func newInsecureClient() *http.Client {
  return &http.Client{
    Transport: &http2.Transport{
      AllowHTTP: true,
      DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
        // If you're also using this client for non-h2c traffic, you may want
        // to delegate to tls.Dial if the network isn't TCP or the addr isn't
        // in an allowlist.
        return net.Dial(network, addr)
      },
      // Don't forget timeouts!
    },
  }
}

Go 1.24+ 的标准库 net/http 已原生支持 HTTP/2(TLS 下通过 ALPN 自动协商),connect-go v1.19.0 起也不再依赖 golang.org/x/net/http2。上面的 h2c 方案仅在需要明文 HTTP/2(无 TLS)时才需要 x/net/http2/h2c。

  1. HTTP/3 支持:

connect-go 本身不内置 HTTP/3,但 Connect 协议不依赖特定 HTTP 版本,官方 FAQ 推荐通过 quic-go 的 http3 包为服务器和客户端提供 HTTP/3 支持。

  1. CORS 配置:
corsHandler := cors.New(cors.Options{
    AllowedMethods: []string{
        http.MethodGet,
        http.MethodPost,
    },
    AllowedHeaders: []string{
        "Accept-Encoding",
        "Content-Type",
        "Connect-Protocol-Version",
        "Connect-Timeout-Ms",
        "X-User-Agent",
    },
    ExposedHeaders: []string{
        "Grpc-Status",
        "Grpc-Message",
        "Grpc-Status-Details-Bin",
    },
    AllowedOrigins: []string{"*"},
})
handler := corsHandler.Handler(mux)

注意:

  • 只允许请求头还不够,还必须 expose 响应头(Grpc-Status、Grpc-Message、Grpc-Status-Details-Bin),否则浏览器端 Web 客户端拿不到错误码,只会看到"CORS 缺失"之类的协议错误
  • 官方提供了 connectrpc.com/cors 包,可以更省心地配置
  • 生产环境避免使用 * 通配符(通配符在带凭据的请求下不生效)

Header 和 Trailer

处理 Headers:

func (s *Server) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    // 读取请求头
    tenantID := req.Header().Get("Tenant-ID")
    
    // 设置响应头
    res := connect.NewResponse(&greetv1.GreetResponse{})
    res.Header().Set("Version", "v1")
    return res, nil
}

Headers 处理和grpc的差异和相似点:

  1. 相似点:
  • 都使用 HTTP headers
  • 都支持二进制 headers (使用 -Bin 后缀)
  • 都有保留的 header 前缀限制
  1. 不同点:
  • Connect 使用更简单的 API,直接通过 Request 和 Response 结构访问
  • gRPC 通常通过 context 传递 metadata
  • Connect 的 header 命名更灵活,只要符合 HTTP header 规范即可

关键限制 Header 命名限制:

  • 保留前缀:Connect- 和 Grpc-
  • 只能包含 ASCII 字母、数字、下划线、连字符和点
  • 值只能包含可打印 ASCII 和空格

二进制 Headers:

// 编码二进制头
res.Header().Set(
    "Binary-Data-Bin",
    connect.EncodeBinaryHeader([]byte("data")),
)

// 解码二进制头
if data, err := connect.DecodeBinaryHeader(
    req.Header().Get("Binary-Data-Bin"),
); err == nil {
    // 使用解码后的数据
}

Trailer

Trailer 必须在响应返回前设置

func (s *Server) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    res := connect.NewResponse(&greetv1.GreetResponse{})
    // Trailer 必须在返回前设置
    res.Trailer().Set("Greet-Version", "v1")
    return res, nil
}
Trailer 限制:
  • 一旦响应返回,无法再修改 Trailer
  • 对于流式响应,可以在流结束前的任何时候设置 Trailer
  • 建议在非流式响应中使用 Header 而不是 Trailer
和grpc的差异和相似点
  1. 编码差异:
// Connect 的 Trailer 处理
func (s *Server) Greet(ctx context.Context, req *connect.Request[greetv1.GreetRequest]) (*connect.Response[greetv1.GreetResponse], error) {
    res := connect.NewResponse(&greetv1.GreetResponse{})
    // Connect 会自动添加 Trailer- 前缀
    res.Trailer().Set("Greet-Version", "v1")
    return res, nil
}
  1. 协议差异:
  • gRPC: 总是使用 HTTP trailers
  • gRPC-Web: 将 trailers 编码在响应体的最后部分
  • Connect:
    • 对于一元调用:使用 Trailer- 前缀的 HTTP headers
    • 对于流式调用:类似 gRPC-Web 的处理方式
  1. 使用建议:
  • 一元调用建议使用 Headers 而不是 Trailers
  • Trailers 主要用于流式调用,在发送消息后需要传递元数据的场景

关键限制

  1. Trailer 设置时机:
  • 必须在响应返回前设置
  • 一旦响应返回,无法修改 Trailer
  • 流式响应可以在流结束前的任何时候设置

这些差异主要是为了提供更好的 HTTP 兼容性和更简单的 API 使用体验。

代理与负载均衡

一元 RPC 与流式 RPC 的代理要求不同:

  • 一元 RPC:不需要端到端 HTTP/2,NGINX 无需特殊配置即可代理
  • 流式 RPC:通常需要端到端 HTTP/2。NGINX 现在也支持,但默认未开启:

Envoy、Apache 和 TCP 级负载均衡器(如 HAProxy)也都支持完整的 Connect 协议。

错误处理

  1. 标准错误处理:
if err != nil {
    return nil, connect.NewError(
        connect.CodeInvalidArgument,
        fmt.Errorf("invalid request: %w", err),
    )
}

错误模型说明:

  • Connect 沿用 gRPC 的错误码体系(Code* 常量),而不是直接使用 HTTP 状态码——因为 gRPC 与 HTTP 状态码的映射是有损的,为了无缝支持 gRPC 协议只能统一
  • 一元请求会映射为有意义的 HTTP 状态码(如 400、404、500);流式请求响应总是 HTTP 200,错误编码在响应体的最后一段(envelope 中)
  1. 错误详情:
// 创建带详情的错误
err := connect.NewError(
    connect.CodeNotFound,
    errors.New("resource not found"),
)
err.Meta().Set("resource-id", "123")

// 处理错误
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
    fmt.Println(connectErr.Code())
    fmt.Println(connectErr.Meta().Get("resource-id"))
}

也可以使用标准错误详情机制(基于 google.rpc.Status 的 details 字段):

// 服务端附加结构化详情
detail := &myv1.ErrorDetail{Reason: "rate-limited"}
return nil, connect.NewError(
    connect.CodeResourceExhausted,
    errors.New("rate limit exceeded"),
).WithDetail(detail)

// 客户端提取
var d myv1.ErrorDetail
if connect.AsErrorDetail(err, &d) {
    fmt.Println(d.Reason)
}

版本与生态现状(2026-09)

  • connect-go:v1.x 稳定,最新 v1.20.0(2026-05,要求 Go 1.25+)。v1.19.0 起 protoc-gen-connect-go 提供 --simple 标志,生成去掉 Request/Response 包装的简洁接口,并用 context 传递元数据;同时支持 Protobuf Editions 2024
  • v2.0.0-alpha.1(2026-09):connect-go 的新主版本,只改 Go API 不改 wire protocol(gRPC 兼容性和生态不变),v1 继续完全支持,官方提供了自动化迁移工具
  • connect-es(TypeScript/JavaScript):稳定,被 Buf 等公司在生产环境使用,支持浏览器和 Node.js
  • 移动端:connect-swift 稳定;connect-kotlin 仍为 beta
  • Python:connect-py 仍为 beta
  • 官方 roadmap 在 GitHub discussions 置顶

注意事项

  1. 流式调用与服务器超时:http.Server 的 ReadTimeout/WriteTimeout 作用于整个请求周期,流式调用稍长就会被切断(报 stream error: stream ID ...; INTERNAL_ERROR)。流式服务建议只设置 ReadHeaderTimeout
  2. 客户端超时:可用 connect.WithTimeout 选项,会发送 connect-timeout-ms header,服务器也会据此限制处理时间
  3. 保留前缀:connect- 前缀的 header 保留给 Connect 协议本身使用,业务自定义 header 应避开
  4. Web 端 JSON 调试:connect-es 客户端可通过 useBinaryFormat: false 切换到 JSON 编码,浏览器网络面板里就能直接看到可读的 payload

Connect RPC 提供了简单而强大的 API 构建方式,既保持了与 gRPC 的兼容性,又提供了更现代的开发体验。通过合理使用其提供的功能,可以构建高效、可靠的微服务系统。