diff --git a/content/en/blog/releases/Hertz/release-v0_10_0.md b/content/en/blog/releases/Hertz/release-v0_10_0.md new file mode 100644 index 00000000000..bae82a8a8b8 --- /dev/null +++ b/content/en/blog/releases/Hertz/release-v0_10_0.md @@ -0,0 +1,36 @@ +--- +title: "Hertz Release v0.10.0" +linkTitle: "Release v0.10.0" +projects: ["Hertz"] +date: 2025-05-21 +description: > +--- + +The Hertz v0.10.0 release adds two features and some fixes. + +1. Integrate SSE functionality. Refer to [SSE](/docs/hertz/tutorials/basic-feature/sse) for usage. +2. Added http.Handler adaptor, extending Hertz using the official net/http ecosystem. Refer to [Adaptor](/docs/hertz/tutorials/basic-feature/http-adaptor) for usage. + +## Feature +1. [[#1327](https://github.com/cloudwego/hertz/pull/1327)] feat(adaptor): new HertzHandler for http.Handler +2. [[#1349](https://github.com/cloudwego/hertz/pull/1349)] feat(sse): SetLastEventID +3. [[#1343](https://github.com/cloudwego/hertz/pull/1343)] feat(sse): reader supports cancel stream +4. [[#1341](https://github.com/cloudwego/hertz/pull/1341)] feat(server): detect request race +5. [[#1339](https://github.com/cloudwego/hertz/pull/1339)] feat(sse): add LastEventID helpers +6. [[#1335](https://github.com/cloudwego/hertz/pull/1335)] feat(protocol): new sse pkg +7. [[#1322](https://github.com/cloudwego/hertz/pull/1322)] feat: std transport sense client connection close + +## Fix +1. [[#1340](https://github.com/cloudwego/hertz/pull/1340)] fix: only use netpoll & sonic on amd64/arm64 linux/darwin +2. [[#1333](https://github.com/cloudwego/hertz/pull/1333)] fix(protocol): unexpected set resp.bodyStream +3. [[#1329](https://github.com/cloudwego/hertz/pull/1329)] fix(client): stream body for sse instead of timeout +4. [[#1332](https://github.com/cloudwego/hertz/pull/1332)] fix(server): Shutdown checks ExitWaitTimeout +5. [[#1316](https://github.com/cloudwego/hertz/pull/1316)] fix: prioritize custom validator + +## Tests +1. [[#1336](https://github.com/cloudwego/hertz/pull/1336)] test(protocol): fix hardcoded listen addr + +## Chore +1. [[#1353](https://github.com/cloudwego/hertz/pull/1353)] chore: update netpoll dependency +2. [[#1337](https://github.com/cloudwego/hertz/pull/1337)] chore(hz): update hz tool v0.9.7 +3. [[#1328](https://github.com/cloudwego/hertz/pull/1328)] ci: disable codecov annotations diff --git a/content/en/docs/hertz/tutorials/basic-feature/_index.md b/content/en/docs/hertz/tutorials/basic-feature/_index.md index 0812a62a9c5..64ca04eab38 100644 --- a/content/en/docs/hertz/tutorials/basic-feature/_index.md +++ b/content/en/docs/hertz/tutorials/basic-feature/_index.md @@ -23,6 +23,8 @@ keywords: "Constants", "Render", "JSON Marshal Library", + "SSE", + "http.Handler adaptor", ] description: "Basic feature of Hertz." --- diff --git a/content/en/docs/hertz/tutorials/basic-feature/http-adaptor.md b/content/en/docs/hertz/tutorials/basic-feature/http-adaptor.md new file mode 100644 index 00000000000..382439e1526 --- /dev/null +++ b/content/en/docs/hertz/tutorials/basic-feature/http-adaptor.md @@ -0,0 +1,50 @@ +--- +title: "http.Handler adaptor" +date: 2025-05-21 +weight: 20 +keywords: ["adaptor"] +description: "" +--- + +## Background + +- Phasing out the poorly maintained hertz app.FS implementation +- Providing a compatible and performant fs implementation through the official net/http ecosystem +- Extending hertz functionality through the official net/http ecosystem to reduce custom implementations + +## What is adaptor.HertzHandler + +- Allows you to convert existing http.HandlerFunc methods directly to HertzHandler +- Enables direct use of standard library methods like http.FileServer and embed.FS +- Even allows direct use of github.com/gorilla/websocket in Hertz + +## Example + +```go +package main + +import ( + "embed" + "net/http" + + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/adaptor" +) + +//go:embed static/* +var staticFiles embed.FS + +func main() { + h := server.Default() + + helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello hertz!")) + }) + h.GET("/hello", adaptor.HertzHandler(helloHandler)) + + staticFS := adaptor.HertzHandler(http.FileServer(http.FS(staticFiles))) + h.GET("/static/*filepath", staticFS) + h.HEAD("/static/*filepath", staticFS) + h.Spin() +} +``` diff --git a/content/en/docs/hertz/tutorials/basic-feature/sse.md b/content/en/docs/hertz/tutorials/basic-feature/sse.md new file mode 100644 index 00000000000..6e5140b4f35 --- /dev/null +++ b/content/en/docs/hertz/tutorials/basic-feature/sse.md @@ -0,0 +1,107 @@ +--- +title: "SSE" +date: 2025-05-21 +weight: 19 +keywords: ["SSE"] +description: "SSE capabilities provided by Hertz" +--- +```code: https://github.com/cloudwego/hertz/tree/develop/pkg/protocol/sse``` + +## What is Server-Sent Events (SSE) + +- SSE is an HTML standard used to describe the implementation of browser EventSource API. Strictly speaking, it's not an HTTP protocol. +- Popular protocols like Model Context Protocol (MCP) or Agent2Agent (A2A) are based on SSE to some extent + - https://www.anthropic.com/news/model-context-protocol + - https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/ + +## Server-side Implementation + +### Returning Data + +```go +import "github.com/cloudwego/hertz/pkg/protocol/sse" + +func HandleSSE(ctx context.Context, c *app.RequestContext) { + println("Server Got LastEventID", sse.GetLastEventID(&c.Request)) + w := sse.NewWriter(c) + for i := 0; i < 5; i++ { + w.WriteEvent("id-x", "message", []byte("hello\n\nworld")) + time.Sleep(10 * time.Millisecond) + } + w.Close() // Send the last chunk of data to ensure graceful exit. Optional, Hertz will automatically call it after the Handler returns. + + // Please ensure the writer's lifecycle is consistent with the handler. Don't use it asynchronously in the background. +} +``` + +### Common Issues + +#### connection has been closed when flush + +Reason: +Client connection is disconnected, possibly due to no response data for too long. Please check the relevant configuration. + +## Client-side Implementation + +### Initiating a Request + +It's exactly the same as initiating a regular HTTP request with Hertz. + +Note: New Hertz versions will automatically identify SSE streams, so you don't need to explicitly set WithResponseBodyStream(true) + +**Some Optional Headers** + +```go +import "github.com/cloudwego/hertz/pkg/protocol/sse" + +sse.AddAcceptMIME(req) // Some SSE Servers may require explicitly adding Accept: text/event-stream +sse.SetLastEventID(req, "id-123") // For stateful services, you need to tell the Server through SetLastEventID +``` + +### Handling Responses + +```go +import "github.com/cloudwego/hertz/pkg/protocol/sse" + +func HandleSSE(ctx context.Context, resp *protocol.Response) error { + r, err := sse.NewReader(resp) + if err != nil { + return err + } + + // You can also manually call the r.Read method + err = r.ForEach(ctx, func(e *Event) error { + println("Event:", e.String()) + return nil + }) + if err != nil { // If the Server disconnects normally, err == nil, no error will be reported + // Other IO errors or ctx cancelled + return err + } + println("Client LastEventID", r.LastEventID()) // Can be used to save the last received Event ID + return nil +} +``` + +### Common Issues + +#### How to implement multi-level SSE / SSE Proxy? + +- You can refer to both Client-side and Server-side implementations +- Enable `WithSenseClientDisconnection` on the Server and pass the context to the next hop's Reader ForEach + - This way, when the Client disconnects, it will be automatically detected and the entire stream will be interrupted + +## Event Structure + +```go +// Event represents a Server-Sent Event (SSE). +type Event struct { + ID string // This is the Event ID, sse.Reader will automatically record the last Event ID, which can be obtained using LastEventID() + Type string // This is the Event Type, common ones like "message" + Data []byte // For convenience with Unmarshal/Marshal, []byte is used here, but according to the spec, this field must be a utf8 string + + // Not recommended to use, mainly for controlling the retry strategy of the browser's SourceEvent. + // If using Hertz as a Client, you can refer to: https://www.cloudwego.io/docs/hertz/tutorials/basic-feature/retry/ + Retry time.Duration +} +``` diff --git a/content/zh/blog/releases/Hertz/release-v0_10_0.md b/content/zh/blog/releases/Hertz/release-v0_10_0.md new file mode 100644 index 00000000000..e251e9f0d62 --- /dev/null +++ b/content/zh/blog/releases/Hertz/release-v0_10_0.md @@ -0,0 +1,36 @@ +--- +title: "Hertz Release v0.10.0" +linkTitle: "Release v0.10.0" +projects: ["Hertz"] +date: 2025-05-21 +description: > +--- + +Hertz v0.10.0 版本新增两项功能并修复了一些问题。 + +1. 集成 SSE 功能。使用方法请参阅 [SSE](/docs/hertz/tutorials/basic-feature/sse)。 +2. 添加 http.Handler 适配器,使用官方 net/http 生态系统扩展 Hertz。使用方法请参阅 [Adaptor](/docs/hertz/tutorials/basic-feature/http-adaptor)。 + +## Feature +1. [[#1327](https://github.com/cloudwego/hertz/pull/1327)] feat(adaptor): 为 http.Handler 添加新的 HertzHandler +2. [[#1349](https://github.com/cloudwego/hertz/pull/1349)] feat(sse): SetLastEventID +3. [[#1343](https://github.com/cloudwego/hertz/pull/1343)] feat(sse): reader 支持取消流 +4. [[#1341](https://github.com/cloudwego/hertz/pull/1341)] feat(server): 检测请求 race +5. [[#1339](https://github.com/cloudwego/hertz/pull/1339)] feat(sse): 添加 LastEventID helper +6. [[#1335](https://github.com/cloudwego/hertz/pull/1335)] feat(protocol): 新的 sse 包 +7. [[#1322](https://github.com/cloudwego/hertz/pull/1322)] feat: server 使用标准 go net 传输时感知客户端连接关闭 + +## Fix +1. [[#1340](https://github.com/cloudwego/hertz/pull/1340)] fix:仅在 amd64/arm64 linux/darwin 上使用 netpoll 和 sonic +2. [[#1333](https://github.com/cloudwego/hertz/pull/1333)] fix(protocol): 非预期的设置 resp.bodyStream +3. [[#1329](https://github.com/cloudwego/hertz/pull/1329)] fix(client): sse 场景下自动切换为 stream body 模式 +4. [[#1332](https://github.com/cloudwego/hertz/pull/1332)] fix(server): server 关闭时检查 ExitWaitTimeout +5. [[#1316](https://github.com/cloudwego/hertz/pull/1316)] fix: 优先使用自定义 validator + +## Tests +1. [[#1336](https://github.com/cloudwego/hertz/pull/1336)] test(protocol): 修复硬编码的监听地址 + +## Chore +1. [[#1353](https://github.com/cloudwego/hertz/pull/1353)] chore:更新 netpoll 依赖 +2. [[#1337](https://github.com/cloudwego/hertz/pull/1337)] chore(hz): 更新 hz 工具 v0.9.7 +3. [[#1328](https://github.com/cloudwego/hertz/pull/1328)] ci: 禁用 codecov 注释 diff --git a/content/zh/docs/hertz/tutorials/basic-feature/_index.md b/content/zh/docs/hertz/tutorials/basic-feature/_index.md index 116f91c8a73..53a3fe18508 100644 --- a/content/zh/docs/hertz/tutorials/basic-feature/_index.md +++ b/content/zh/docs/hertz/tutorials/basic-feature/_index.md @@ -23,6 +23,8 @@ keywords: "常量", "渲染", "JSON Marshal 库", + "SSE", + "http.Handler adaptor", ] description: "Hertz 基本特性。" --- diff --git a/content/zh/docs/hertz/tutorials/basic-feature/http-adaptor.md b/content/zh/docs/hertz/tutorials/basic-feature/http-adaptor.md new file mode 100644 index 00000000000..1f37fd5a6bd --- /dev/null +++ b/content/zh/docs/hertz/tutorials/basic-feature/http-adaptor.md @@ -0,0 +1,50 @@ +--- +title: "http.Handler adaptor" +date: 2025-05-21 +weight: 20 +keywords: ["adaptor"] +description: "" +--- + +## 背景 + +- 淘汰长期没有良好维护的 hertz app.FS 实现 +- 通过官方 net/http 生态提供一个兼容性较好,性能也不差的 fs 实现。 +- 通过官方 net/http 生态扩展 hertz 功能,减少大量扩展实现 + +## 什么是 adaptor.HertzHandler + +- 允许你将现有的 http.HandlerFunc 方法直接转为 HertzHandler +- 可以直接使用 http.FileServer embed.FS 等标准库方法 +- 甚至可以直接在 Hertz 使用 github.com/gorilla/websocket + +## 具体例子 + +```go +package main + +import ( + "embed" + "net/http" + + "github.com/cloudwego/hertz/pkg/app/server" + "github.com/cloudwego/hertz/pkg/common/adaptor" +) + +//go:embed static/* +var staticFiles embed.FS + +func main() { + h := server.Default() + + helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello hertz!")) + }) + h.GET("/hello", adaptor.HertzHandler(helloHandler)) + + staticFS := adaptor.HertzHandler(http.FileServer(http.FS(staticFiles))) + h.GET("/static/*filepath", staticFS) + h.HEAD("/static/*filepath", staticFS) + h.Spin() +} +``` diff --git a/content/zh/docs/hertz/tutorials/basic-feature/sse.md b/content/zh/docs/hertz/tutorials/basic-feature/sse.md new file mode 100644 index 00000000000..94a6b40f12e --- /dev/null +++ b/content/zh/docs/hertz/tutorials/basic-feature/sse.md @@ -0,0 +1,107 @@ +--- +title: "SSE" +date: 2025-05-21 +weight: 19 +keywords: ["SSE"] +description: "Hertz 提供的 SSE 能力" +--- +```代码:https://github.com/cloudwego/hertz/tree/develop/pkg/protocol/sse``` + +## 什么是 Server-Sent Events (SSE) + +- SSE 是一个 HTML 标准,用于描述浏览器 EventSource API 的实现。严格说,不是 http 协议。 +- 流行的 Model Context Protocol (MCP) 或 Agent2Agent (A2A) 协议都一定程度基于 SSE + - https://www.anthropic.com/news/model-context-protocol + - https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/ + +## Server 端实现 + +### 返回数据 + +```go +import "github.com/cloudwego/hertz/pkg/protocol/sse" + +func HandleSSE(ctx context.Context, c *app.RequestContext) { + println("Server Got LastEventID", sse.GetLastEventID(&c.Request)) + w := sse.NewWriter(c) + for i := 0; i < 5; i++ { + w.WriteEvent("id-x", "message", []byte("hello\n\nworld")) + time.Sleep(10 * time.Millisecond) + } + w.Close() // 发送最后的 chunk 数据,确保优雅退出。可选,Hertz 在 Handler 返回后会自动调用。 + + // 请确保 writer 的生命周期和 handler 一致。不要 go 异步后台使用。 +} +``` + +### 常见问题 + +#### connection has been closed when flush + +原因: +Client 连接断开,可能是太久没有响应数据,请检查相应配置。 + +## Client 端实现 + +### 发起请求 + +与正常 Hertz 发起一个普通的 http 请求完全一致。 + +注意:新 hertz 版本会自动识别 SSE 流,不用显式设置 WithResponseBodyStream(true) + +**部分可选 Header** + +```go +import "github.com/cloudwego/hertz/pkg/protocol/sse" + +sse.AddAcceptMIME(req) // 部分 SSE Server 可能会要求显式增加 Accept: text/event-stream +sse.SetLastEventID(req, "id-123") // 对于有状态服务,需要通过 SetLastEventID 告诉 Server +``` + +### 处理返回 + +```go +import "github.com/cloudwego/hertz/pkg/protocol/sse" + +func HandleSSE(ctx context.Context, resp *protocol.Response) error { + r, err := sse.NewReader(resp) + if err != nil { + return err + } + + // 也可以手动调用 r.Read 方法 + err = r.ForEach(ctx, func(e *Event) error { + println("Event:", e.String()) + return nil + }) + if err != nil { // 如果 Server 正常断开,这里 err == nil,不会报错 + // 其他 io 错误 或 ctx cancelled + return err + } + println("Client LastEventID", r.LastEventID()) // 可用于保存最后接收的 Event ID + return nil +} +``` + +### 常见问题 + +#### 如何实现多级的 SSE / SSE Proxy? + +- 可以同时参考 Client 端和 Server 端的实现 +- Server 启用 `WithSenseClientDisconnection` 并且把 context 传入 下一跳的 Reader ForEach + - 这样当 Client 断开时,会自动识别,并且中断整个流 + +## Event 结构体 + +```go +// Event represents a Server-Sent Event (SSE). +type Event struct { + ID string // 即 Event ID,sse.Reader 会自动记录最后的 Event ID,可使用 LastEventID() 获取 + Type string // 即 Event Type,常见的如 "message" + Data []byte // 为了方便使用 Unmarshal/Marshal,这里使用 []byte,但是按 spec 这个字段必须要 utf8 string + + // 不建议使用,主要是针对浏览器的 SourceEvent 返回该字段控制其重试策略。 + // 如果使用 Hertz 作为 Client 可以参考:https://www.cloudwego.io/docs/hertz/tutorials/basic-feature/retry/ + Retry time.Duration +} +```