From 7e15181c0b49694187ee8c87ec1bb85c4c2d0c73 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:26:48 +0800 Subject: [PATCH 1/9] feat(render): add Buf variants for JSON/GOB/WANF/HTML Add buffered rendering methods that encode to a buffer first, then write the response. This allows returning a proper 500 status code if encoding fails, unlike the streaming variants which must write the status code before encoding (an inherent HTTP constraint). New methods: - JSONBuf(code int, obj any) - GOBBuf(code int, obj any) - WANFBuf(code int, obj any) - HTMLBuf(code int, name string, obj any) Trade-off: one extra memory allocation per call in exchange for correct error status codes on encoding failure. --- context.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/context.go b/context.go index c37371f..885b10f 100644 --- a/context.go +++ b/context.go @@ -417,6 +417,21 @@ func (c *Context) JSON(code int, obj any) { } } +// JSONBuf 先将 JSON 编码到 buffer, 成功后再写入状态码和响应体. +// 与 JSON 相比, 编码失败时可以正确返回 500 状态码, 代价是多一次内存分配. +func (c *Context) JSONBuf(code int, obj any) { + data, err := json.Marshal(obj) + if err != nil { + c.AddError(fmt.Errorf("failed to marshal JSON: %w", err)) + c.Errorf("failed to marshal JSON: %s", err) + c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to marshal JSON: %w", err)) + return + } + c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") + c.Writer.WriteHeader(code) + c.Writer.Write(data) +} + // GOB 向响应写入GOB数据 // 设置 Content-Type 为 application/octet-stream func (c *Context) GOB(code int, obj any) { @@ -431,6 +446,20 @@ func (c *Context) GOB(code int, obj any) { } } +// GOBBuf 先将 GOB 编码到 buffer, 成功后再写入状态码和响应体. +func (c *Context) GOBBuf(code int, obj any) { + var buf bytes.Buffer + encoder := gob.NewEncoder(&buf) + if err := encoder.Encode(obj); err != nil { + c.AddError(fmt.Errorf("failed to encode GOB: %w", err)) + c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode GOB: %w", err)) + return + } + c.Writer.Header().Set("Content-Type", "application/octet-stream") + c.Writer.WriteHeader(code) + c.Writer.Write(buf.Bytes()) +} + // WANF向响应写入WANF数据 // 设置 application/vnd.wjqserver.wanf; charset=utf-8 func (c *Context) WANF(code int, obj any) { @@ -445,6 +474,19 @@ func (c *Context) WANF(code int, obj any) { } } +// WANFBuf 先将 WANF 编码到 buffer, 成功后再写入状态码和响应体. +func (c *Context) WANFBuf(code int, obj any) { + data, err := wanf.Marshal(obj) + if err != nil { + c.AddError(fmt.Errorf("failed to encode WANF: %w", err)) + c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode WANF: %w", err)) + return + } + c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8") + c.Writer.WriteHeader(code) + c.Writer.Write(data) +} + // HTML 渲染 HTML 模板 // 如果 Engine 配置了 HTMLRender,则使用它进行渲染 // 否则,会进行简单的字符串输出 @@ -469,6 +511,29 @@ func (c *Context) HTML(code int, name string, obj any) { c.Writer.Write(fmt.Appendf(nil, "\n
%v
", name, obj)) } +// HTMLBuf 先将 HTML 模板渲染到 buffer, 成功后再写入状态码和响应体. +func (c *Context) HTMLBuf(code int, name string, obj any) { + if c.engine != nil && c.engine.HTMLRender != nil { + if tpl, ok := c.engine.HTMLRender.(*template.Template); ok { + var buf bytes.Buffer + err := tpl.ExecuteTemplate(&buf, name, obj) + if err != nil { + c.AddError(fmt.Errorf("failed to render HTML template '%s': %w", name, err)) + c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to render HTML template '%s': %w", name, err)) + return + } + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteHeader(code) + c.Writer.Write(buf.Bytes()) + return + } + } + // 默认简单输出 + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteHeader(code) + c.Writer.Write(fmt.Appendf(nil, "\n
%v
", name, obj)) +} + // Redirect 执行 HTTP 重定向 // code 应为 3xx 状态码 (如 http.StatusMovedPermanently, http.StatusFound) func (c *Context) Redirect(code int, location string) { From 6e33bc48aa0249eb0b49a9d69dd577d007fb07d4 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:45:08 +0800 Subject: [PATCH 2/9] fix: simplify error handling in Buf methods Consolidate error wrapping to avoid redundant fmt.Errorf calls. Follows PR #73 review feedback. --- context.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 885b10f..2c51e99 100644 --- a/context.go +++ b/context.go @@ -418,13 +418,14 @@ func (c *Context) JSON(code int, obj any) { } // JSONBuf 先将 JSON 编码到 buffer, 成功后再写入状态码和响应体. -// 与 JSON 相比, 编码失败时可以正确返回 500 状态码, 代价是多一次内存分配. +// 与 JSON 相比,编码失败时可以正确返回 500 状态码,代价是多一次内存分配. func (c *Context) JSONBuf(code int, obj any) { data, err := json.Marshal(obj) if err != nil { - c.AddError(fmt.Errorf("failed to marshal JSON: %w", err)) - c.Errorf("failed to marshal JSON: %s", err) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to marshal JSON: %w", err)) + errMsg := fmt.Errorf("failed to marshal JSON: %w", err) + c.AddError(errMsg) + c.Errorf("%s", errMsg) + c.ErrorUseHandle(http.StatusInternalServerError, errMsg) return } c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") @@ -451,8 +452,9 @@ func (c *Context) GOBBuf(code int, obj any) { var buf bytes.Buffer encoder := gob.NewEncoder(&buf) if err := encoder.Encode(obj); err != nil { - c.AddError(fmt.Errorf("failed to encode GOB: %w", err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode GOB: %w", err)) + errMsg := fmt.Errorf("failed to encode GOB: %w", err) + c.AddError(errMsg) + c.ErrorUseHandle(http.StatusInternalServerError, errMsg) return } c.Writer.Header().Set("Content-Type", "application/octet-stream") @@ -478,8 +480,9 @@ func (c *Context) WANF(code int, obj any) { func (c *Context) WANFBuf(code int, obj any) { data, err := wanf.Marshal(obj) if err != nil { - c.AddError(fmt.Errorf("failed to encode WANF: %w", err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode WANF: %w", err)) + errMsg := fmt.Errorf("failed to encode WANF: %w", err) + c.AddError(errMsg) + c.ErrorUseHandle(http.StatusInternalServerError, errMsg) return } c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8") From b09595e745572d2660030858671bdb3b768eec5c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 29 Mar 2026 23:43:29 +0800 Subject: [PATCH 3/9] fix: address PR #73 review feedback - Remove redundant c.Errorf call in JSONBuf - Consolidate error wrapping in HTMLBuf to avoid duplicate fmt.Errorf calls - Keep error handling consistent across all Buf methods --- context.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 2c51e99..22f67ea 100644 --- a/context.go +++ b/context.go @@ -424,7 +424,6 @@ func (c *Context) JSONBuf(code int, obj any) { if err != nil { errMsg := fmt.Errorf("failed to marshal JSON: %w", err) c.AddError(errMsg) - c.Errorf("%s", errMsg) c.ErrorUseHandle(http.StatusInternalServerError, errMsg) return } @@ -521,8 +520,9 @@ func (c *Context) HTMLBuf(code int, name string, obj any) { var buf bytes.Buffer err := tpl.ExecuteTemplate(&buf, name, obj) if err != nil { - c.AddError(fmt.Errorf("failed to render HTML template '%s': %w", name, err)) - c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to render HTML template '%s': %w", name, err)) + errMsg := fmt.Errorf("failed to render HTML template '%s': %w", name, err) + c.AddError(errMsg) + c.ErrorUseHandle(http.StatusInternalServerError, errMsg) return } c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") From b4e45610b23f9c290cbb452c7313f3e6fd858290 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:29:50 +0800 Subject: [PATCH 4/9] refactor(HTMLBuf): delegate fallback to HTML() method Reduce code duplication by calling c.HTML() for fallback cases: - When template rendering fails - When HTMLRender is not configured - When HTMLRender is not a *template.Template This ensures consistent behavior between HTMLBuf and HTML methods. --- context.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/context.go b/context.go index 22f67ea..e1cb4f6 100644 --- a/context.go +++ b/context.go @@ -514,27 +514,28 @@ func (c *Context) HTML(code int, name string, obj any) { } // HTMLBuf 先将 HTML 模板渲染到 buffer, 成功后再写入状态码和响应体. +// 如果模板渲染失败或不支持缓冲渲染,则回退到标准的 HTML 方法. func (c *Context) HTMLBuf(code int, name string, obj any) { if c.engine != nil && c.engine.HTMLRender != nil { if tpl, ok := c.engine.HTMLRender.(*template.Template); ok { var buf bytes.Buffer err := tpl.ExecuteTemplate(&buf, name, obj) - if err != nil { - errMsg := fmt.Errorf("failed to render HTML template '%s': %w", name, err) - c.AddError(errMsg) - c.ErrorUseHandle(http.StatusInternalServerError, errMsg) + if err == nil { + // 渲染成功,写入响应 + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteHeader(code) + c.Writer.Write(buf.Bytes()) return } - c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - c.Writer.WriteHeader(code) - c.Writer.Write(buf.Bytes()) - return + // 渲染失败,记录错误后回退到 HTML() + errMsg := fmt.Errorf("failed to render HTML template '%s': %w", name, err) + c.AddError(errMsg) + c.ErrorUseHandle(http.StatusInternalServerError, errMsg) + // 继续执行回退逻辑 } } - // 默认简单输出 - c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - c.Writer.WriteHeader(code) - c.Writer.Write(fmt.Appendf(nil, "\n
%v
", name, obj)) + // 回退到标准 HTML 方法(处理无模板引擎或其他渲染器的情况) + c.HTML(code, name, obj) } // Redirect 执行 HTTP 重定向 From 45c6d36748adc473c7c05de96dccf33b238eb15c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:02:37 +0800 Subject: [PATCH 5/9] fix(HTMLBuf): return 500 on template error, no content - Remove fallback to HTML() on template rendering failure - Return 500 error without writing any content on error - Only fallback to HTML() when renderer is nil or unsupported type - Prevents multiple response writes --- context.go | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/context.go b/context.go index e1cb4f6..cf22333 100644 --- a/context.go +++ b/context.go @@ -514,27 +514,32 @@ func (c *Context) HTML(code int, name string, obj any) { } // HTMLBuf 先将 HTML 模板渲染到 buffer, 成功后再写入状态码和响应体. -// 如果模板渲染失败或不支持缓冲渲染,则回退到标准的 HTML 方法. +// 如果模板渲染失败,则返回 500 错误且不写入任何内容. func (c *Context) HTMLBuf(code int, name string, obj any) { - if c.engine != nil && c.engine.HTMLRender != nil { - if tpl, ok := c.engine.HTMLRender.(*template.Template); ok { - var buf bytes.Buffer - err := tpl.ExecuteTemplate(&buf, name, obj) - if err == nil { - // 渲染成功,写入响应 - c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") - c.Writer.WriteHeader(code) - c.Writer.Write(buf.Bytes()) - return - } - // 渲染失败,记录错误后回退到 HTML() + if c.engine == nil || c.engine.HTMLRender == nil { + // 没有渲染器,回退到简单输出 + c.HTML(code, name, obj) + return + } + + if tpl, ok := c.engine.HTMLRender.(*template.Template); ok { + var buf bytes.Buffer + err := tpl.ExecuteTemplate(&buf, name, obj) + if err != nil { + // 渲染失败,记录错误并返回 500,不写入任何内容 errMsg := fmt.Errorf("failed to render HTML template '%s': %w", name, err) c.AddError(errMsg) c.ErrorUseHandle(http.StatusInternalServerError, errMsg) - // 继续执行回退逻辑 + return } + // 渲染成功,写入响应 + c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8") + c.Writer.WriteHeader(code) + c.Writer.Write(buf.Bytes()) + return } - // 回退到标准 HTML 方法(处理无模板引擎或其他渲染器的情况) + + // 不支持的渲染器类型,回退到简单输出 c.HTML(code, name, obj) } From d0fa14c3c529a05f37b3cb802919d295fe9f7e68 Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:17:32 +0800 Subject: [PATCH 6/9] Update context.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- context.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index cf22333..4d1f033 100644 --- a/context.go +++ b/context.go @@ -420,16 +420,17 @@ func (c *Context) JSON(code int, obj any) { // JSONBuf 先将 JSON 编码到 buffer, 成功后再写入状态码和响应体. // 与 JSON 相比,编码失败时可以正确返回 500 状态码,代价是多一次内存分配. func (c *Context) JSONBuf(code int, obj any) { - data, err := json.Marshal(obj) - if err != nil { + var buf bytes.Buffer + if err := json.MarshalWrite(&buf, obj); err != nil { errMsg := fmt.Errorf("failed to marshal JSON: %w", err) c.AddError(errMsg) c.ErrorUseHandle(http.StatusInternalServerError, errMsg) return } + c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.WriteHeader(code) - c.Writer.Write(data) + c.Writer.Write(buf.Bytes()) } // GOB 向响应写入GOB数据 From fba6fedfc50b0cee17906de58e366072344ac35a Mon Sep 17 00:00:00 2001 From: WJQSERVER <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:17:59 +0800 Subject: [PATCH 7/9] Update context.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- context.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 4d1f033..09035b2 100644 --- a/context.go +++ b/context.go @@ -478,8 +478,9 @@ func (c *Context) WANF(code int, obj any) { // WANFBuf 先将 WANF 编码到 buffer, 成功后再写入状态码和响应体. func (c *Context) WANFBuf(code int, obj any) { - data, err := wanf.Marshal(obj) - if err != nil { + var buf bytes.Buffer + encoder := wanf.NewStreamEncoder(&buf) + if err := encoder.Encode(obj); err != nil { errMsg := fmt.Errorf("failed to encode WANF: %w", err) c.AddError(errMsg) c.ErrorUseHandle(http.StatusInternalServerError, errMsg) @@ -487,7 +488,7 @@ func (c *Context) WANFBuf(code int, obj any) { } c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8") c.Writer.WriteHeader(code) - c.Writer.Write(data) + c.Writer.Write(buf.Bytes()) } // HTML 渲染 HTML 模板 From 7be49b96c88fe928a272e0bc56d3b99da9e9c9a2 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:33:00 +0800 Subject: [PATCH 8/9] feat(cookie): add SameSite support to SetCookie method --- context.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/context.go b/context.go index 09035b2..f1d0968 100644 --- a/context.go +++ b/context.go @@ -1160,17 +1160,22 @@ func (c *Context) SetSameSite(samesite http.SameSite) { } // SetCookie 设置一个 HTTP cookie -func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) { +// sameSite 参数是可选的,如果不提供则使用通过 SetSameSite 设置的值 +func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool, sameSite ...http.SameSite) { if path == "" { path = "/" } + site := c.sameSite + if len(sameSite) > 0 { + site = sameSite[0] + } http.SetCookie(c.Writer, &http.Cookie{ Name: name, Value: url.QueryEscape(value), MaxAge: maxAge, Path: path, Domain: domain, - SameSite: c.sameSite, + SameSite: site, Secure: secure, HttpOnly: httpOnly, }) From 9f210deadf5ea3fe4a2ac3d3ed863090e3f60654 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:42:10 +0800 Subject: [PATCH 9/9] fix(cookie): add warning log when multiple SameSite values provided --- context.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/context.go b/context.go index f1d0968..2e4d2bb 100644 --- a/context.go +++ b/context.go @@ -1167,6 +1167,9 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, } site := c.sameSite if len(sameSite) > 0 { + if len(sameSite) > 1 { + c.Warnf("SetCookie: only the first SameSite value will be used, got %d values", len(sameSite)) + } site = sameSite[0] } http.SetCookie(c.Writer, &http.Cookie{