Encore 原始端点(Raw Endpoints)完全指南:在 Go 后端中直接操作 HTTP 请求
【免费下载链接】encoreThe infrastructure platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/encor/encore
本指南基于 Encore 官方 Go 文档 raw-endpoints.md,深入讲解如何通过//encore:api public raw注解定义原始端点,绕过 Encore 的类型化请求/响应封装,直接访问底层http.ResponseWriter与*http.Request,并配合仓库源码(解析器校验、代码生成与运行时实现)剖析其工作原理。读完本文,你将掌握 raw endpoints 的定义方式、签名约束、路由规则、认证与安全注意事项,以及它在接收 Webhook、处理 WebSocket 等场景下的落地用法。
为什么要用 Raw Endpoints
Encore 常规 API 端点(Regular Endpoints)提供了高度抽象化的开发体验:函数签名只关心业务参数与返回值,Encore 自动负责请求反序列化、响应序列化、路由匹配、参数校验等工作。
但有些场景需要你降低抽象层级,直接访问原始 HTTP 请求与响应。典型场景包括:
- 接收第三方服务(如 GitHub、Stripe、Slack)推送的Webhook,这些回调往往带有自定义头部、签名校验或非 JSON 的请求体;
- 实现WebSocket升级握手,需要直接操作
http.ResponseWriter; - 需要读取请求的原始字节流、自定义响应的每一个字节;
- 需要处理非标准 Content-Type 的请求体。
对于这类需求,Encore 提供了Raw Endpoints(原始端点)。
定义一个 Raw Endpoint
定义方式非常直观:在普通函数上添加//encore:api注解并带上raw选项,同时把函数签名改为标准的 Go HTTP handler 形式:
package service import "net/http" // Webhook receives incoming webhooks from Some Service That Sends Webhooks. //encore:api public raw func Webhook(w http.ResponseWriter, req *http.Request) { // ... operate on the raw HTTP request ... }有经验的 Go 开发者会立刻认出:这就是一个标准的 Go HTTP handler(http.HandlerFunc)。你可以完全按照net/http包的方式读取请求体、检查头部、向w写入响应。
签名约束(源码级校验)
虽然写法与net/http一致,但 Encore 解析器会对签名做严格校验。在 v2/parser/apis/api/api.go 的initRawRPC函数中,raw endpoint 的签名被限定为恰好两个参数且无返回值:
// Ensure signature is func(http.ResponseWriter, *http.Request). if !schemautil.IsNamed(params[0].Type, "net/http", "ResponseWriter") { errs.Add(errRawNotResponeWriter.AtGoNode(params[0].AST)) } if deref, n := schemautil.Deref(params[1].Type); n != 1 || !schemautil.IsNamed(deref, "net/http", "Request") { errs.Add(errRawNotRequest.AtGoNode(params[1].AST)) }对应的编译期错误(定义于 v2/parser/apis/api/errors.go)包括:
| 违反规则 | 错误提示 |
|---|---|
| 参数数量不是 2 | Raw APIs must have a two parameters of type http.ResponseWriter and *http.Request, got %d parameters. |
| 声明了返回值 | Raw APIs must not return any results, got %d results. |
第一个参数不是http.ResponseWriter | Raw APIs must have a first parameter of type http.ResponseWriter. |
第二个参数不是*http.Request | Raw APIs must have a second parameter of type *http.Request. |
这些错误还会附带统一的提示hint: signature must be func(http.ResponseWriter, *http.Request),并在编译阶段(encore build/encore run)直接报错,而不是等到运行时才暴露问题。
Raw 与 Private 的冲突
需要注意一个限制:raw endpoints 不能声明为 private。在 api.go 中有明确的检查逻辑:
if endpoint.Access == Private && endpoint.Raw { // We don't support private raw APIs for now. errs.Add(errRawEndpointCantBePrivate.AtGoNode(rawTag, ...)) return nil, false }对应错误为Private APIs cannot be declared as raw endpoints.(见 errors.go)。因此 raw endpoint 目前只支持public与auth两种访问级别。
路由与 URL
和其他 Encore API 端点一样,raw endpoint 部署后同样会被暴露在统一的 URL 之下:
https://<env>-<app-id>.encr.app/service.Webhook路由规则也完全一致:路径中默认使用函数名,且支持:id参数段与*wildcard通配段。例如:
//encore:api public raw path=/hooks/:id/* func Webhook(w http.ResponseWriter, req *http.Request) {}在解析器层面,路径解析通过 api.go 中的resourcepaths.Parse完成,并显式开启AllowWildcard与AllowFallback,同时要求以/开头(PrefixSlash: true)。
HTTP 方法默认值
raw endpoint 如果没有显式指定method,默认会匹配所有 HTTP 方法。在 api.go 中可以看到这个默认值逻辑:
if len(rpc.HTTPMethods) == 0 { if rpc.Raw { rpc.HTTPMethods = []string{"*"} } else { // For non-raw endpoints, if there's a request payload // default to POST-only. if rpc.Request != nil { rpc.HTTPMethods = []string{"POST"} } else { rpc.HTTPMethods = []string{"GET", "POST"} } } }这非常符合 Webhook 场景的需求——大多数 Webhook 服务商会用 POST 推送,但你也可以在注解中通过method字段精确限定,例如//encore:api public raw method=POST。注意method值必须全部大写(见 api.go 的 ALLCAPS 校验)。
路径测试用例佐证
仓库的解析器测试 v2/parser/apis/api/api_test.go 验证了 raw endpoint 的完整解析结果:
{ name: "raw", imports: []string{"net/http"}, def: ` //encore:api public raw path=/raw func Raw(w http.ResponseWriter, req *http.Request) {} `, want: &Endpoint{ Name: "Raw", Access: Public, Raw: true, HTTPMethods: []string{"*"}, }, },从中可以看到:raw选项被正确解析、访问级别为public、HTTP 方法默认展开为*。
底层实现:从解析到运行的完整链路
为了让读者更透彻地理解 raw endpoint 并非“黑魔法”,下面结合仓库源码还原其完整生命周期。
1. 注解解析
在 api.go 中,//encore:api指令的可选选项列表中包含了raw:
endpoint := &Endpoint{ Raw: dir.HasOption("raw"), } accessOptions := []string{"public", "private", "auth"} ok := directive.Validate(errs, dir, directive.ValidateSpec{ AllowedOptions: append([]string{"raw", "sensitive"}, accessOptions...), AllowedFields: []string{"path", "method"}, ... })raw与public/private/auth/sensitive一起作为合法选项被识别,并通过HasOption("raw")设置Endpoint.Raw标志。
2. 元数据标记
在应用元数据生成阶段(v2/app/legacymeta/legacymeta.go),raw endpoint 会被标记为特殊的 RPC 类型:
if ep.Raw { rpc.Proto = meta.RPC_RAW }这使得下游的代码生成器、API 文档与跟踪系统都能识别出这是一个原始端点。
3. 代码生成
在代码生成阶段(v2/codegen/apigen/endpointgen/handlers.go),为 raw endpoint 生成的 handler 是一个标准func(w http.ResponseWriter, req *http.Request),直接调用你的业务函数:
func (h *handlerDesc) Raw() *Statement { ep := h.ep if !ep.Raw { return Nil() } return Func().Params( Id("w").Qual("net/http", "ResponseWriter"), Id("req").Op("*").Qual("net/http", "Request"), ).BlockFunc(func(g *Group) { // If we have a service struct, initialize it first. if ss, ok := h.svcStruct.Get(); ok && ep.Recv.Present() { g.List(Id("svc"), Id("initErr")).Op(":=").Add(ss.Qual()).Dot("Get").Call() g.If(Id("initErr").Op("!=").Nil()).Block( Qual("encore.dev/beta/errs", "HTTPErrorWithCode").Call(Id("w"), Id("initErr"), Lit(0)), Return(), ) fnExpr = Id("svc").Dot(ep.Name) } else { fnExpr = Id(ep.Name) } g.Add(fnExpr).Call(Id("w"), Id("req")) }) }注意这段代码揭示了一个细节:如果函数定义在带服务结构体的服务中,Encore 会先通过svc.Get()初始化服务结构体,再调用结构体上的方法;初始化失败时通过encore.dev/beta/errs的HTTPErrorWithCode直接向响应流写错误。
4. 运行时分发
运行时层面(runtimes/go/appruntime/apisdk/api/handler.go)通过Desc结构区分两种端点:
// If raw is true, RawHandler is set and AppHandler and EncodeResp are nil. Raw bool RawHandler func(http.ResponseWriter, *http.Request)请求到达时,运行时根据d.Raw分流(见 handler.go):
if d.Raw { respCapturer = newRawResponseCapturer(c.w, c.req) return d.invokeHandlerRaw(mwReq, c, respCapturer) } else { return d.invokeHandlerNonRaw(mwReq, reqData, d.AppHandler) }invokeHandlerRaw的实现(handler.go)会将你的处理函数包装成http.HandlerFunc直接执行——不做请求体反序列化、不做响应体序列化,一切交给你的代码自行处理:
func (d *Desc[Req, Resp]) invokeHandlerRaw(mwReq middleware.Request, c IncomingContext, capturer *rawResponseCapturer) (mwResp middleware.Response) { ... capturer.InvokeHandler(http.HandlerFunc(d.RawHandler), httpReq) ... }同时,raw endpoint 不参与 service-to-service 的内部调用——从代码中可以看到TODO: we don't currently support service-to-service calls of raw endpoints的注释(handler.go),对应的编译期错误为Raw APIs cannot be called from within an Encore application.(见 errors.go)。
5. 流量捕获
即使对于 raw endpoint,Encore 的追踪系统依然会尝试捕获请求与响应的原始内容用于开发面板展示。相关限制定义在 runtimes/go/appruntime/apisdk/api/capture.go:
// MaxRawRequestCaptureLen is the maximum buffer size to keep for // capturing the request body in the Encore development dashboard. MaxRawRequestCaptureLen = 10 << 10 // 10 KiB MaxRawResponseCaptureLen = 100 << 10 // 100 KiB即请求体最多捕获 10 KiB、响应体最多捕获 100 KiB,超出部分会被截断,避免大流量撑爆内存。
实战:接收 Webhook
回到最初的动机——接收 Webhook。一个完整的例子如下:
package webhooks import ( "encoding/json" "io" "net/http" ) // GitHubWebhook receives push events from GitHub. //encore:api public raw method=POST func GitHubWebhook(w http.ResponseWriter, req *http.Request) { // 1. 校验签名(例如 X-Hub-Signature-256) // 2. 读取并解析请求体 body, err := io.ReadAll(req.Body) if err != nil { http.Error(w, "failed to read body", http.StatusBadRequest) return } defer req.Body.Close() var event map[string]any if err := json.Unmarshal(body, &event); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } // 3. 处理业务逻辑…… // 4. 向 Webhook 服务商返回 2xx 确认接收 w.WriteHeader(http.StatusOK) }由于函数签名为标准的 Go handler,你可以在函数体内自由地:
- 用
req.Header.Get(...)读取签名头并验证请求来源; - 用
req.URL.Query()解析查询参数; - 用
io.ReadAll(req.Body)/json.Decoder读取请求体; - 用
w.Header().Set(...)设置响应头,w.WriteHeader(...)控制状态码,直接w.Write(...)写响应体; - 甚至执行 WebSocket 升级(
http.Hijacker等标准手段)。
关于接收 Webhook 与 WebSockets 的更多进阶内容(包括对 Webhook 请求的加密签名验证),可继续阅读 receiving regular HTTP requests guide。另外,Encote 官方 Slack Bot 示例应用就是使用 raw endpoints 接收 Webhook 的典型参考实现。
关键注意事项小结
| 要点 | 说明 |
|---|---|
| 注解写法 | //encore:api public raw(可再叠加method=POST、path=...) |
| 函数签名 | func(w http.ResponseWriter, req *http.Request),不允许返回值 |
| 访问级别 | 仅支持public和auth,不支持 private |
| HTTP 方法 | 默认*(全部方法),可用method字段限定(必须全大写) |
| 路由能力 | 支持:param与*wildcard路径段 |
| 内部调用 | 不能在 Encore 应用内部以 service-to-service 方式调用 |
| 序列化 | Encore 不做任何请求/响应自动编解码,全部由你的代码处理 |
| 流量捕获 | 开发面板最多捕获 10 KiB 请求体 / 100 KiB 响应体 |
总结
Raw Endpoints 是 Encore 在“抽象”与“控制”之间提供的灵活性出口:当类型化的 API 封装无法满足 Webhook、WebSocket 等特殊场景时,只需在注解中加一个raw选项,即可把端点变成标准的 Go HTTP handler,获得对 HTTP 层的完全控制权。从仓库源码可以看到,这一能力从解析器签名校验、代码生成到运行时分发都有完整且严格的实现支撑,既保留了 Encore 统一路由、统一 URL、自动部署与追踪的优势,又不牺牲 Go 开发者熟悉的net/http编程体验。
【免费下载链接】encoreThe infrastructure platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/encor/encore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考