style: fix lint errors in response.failed SSE writer

Errcheck flagged three unchecked strings.Builder.WriteString calls and
gofmt rejected over-aligned trailing comment in the route table.

Rewrite writeResponsesFailedSSE with json.Marshal on typed structs
instead of Builder+strconv.Quote. Same wire format, but:
- no unchecked Write returns to silence
- strict JSON escaping (strconv.Quote emits \a and \v which are not
  valid JSON; Marshal handles all runes correctly)
- omitempty model field via struct tag instead of conditional Builder
- consistent with the json.Marshal style used elsewhere in handler/

Collapse trailing comment whitespace in stream_error_event_test.go to
satisfy gofmt.

All 30+ subtests in the package still pass.
This commit is contained in:
shaw 2026-05-25 18:16:46 +08:00
parent a18738b29e
commit 53acde1efd
2 changed files with 44 additions and 20 deletions

View File

@ -1,9 +1,9 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
@ -11,6 +11,30 @@ import (
"github.com/google/uuid"
)
// responsesFailedError 对齐 OpenAI Responses 协议 error 子对象。
type responsesFailedError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// responsesFailedBody 对齐 apicompat.makeResponsesCompletedEvent 输出的 response 子对象字段集。
// Output 用空 slice不是 nil确保 marshal 为 `[]` 而非 `null`。
type responsesFailedBody struct {
ID string `json:"id"`
Object string `json:"object"`
Model string `json:"model,omitempty"`
Status string `json:"status"`
Output []any `json:"output"`
Error responsesFailedError `json:"error"`
}
// responsesFailedEvent 是写入 SSE data 行的顶层结构。
// 故意不带 sequence_numberspec 标记可选,且本函数被调用时无法可靠拿到 last seq。
type responsesFailedEvent struct {
Type string `json:"type"`
Response responsesFailedBody `json:"response"`
}
// writeResponsesFailedSSE emits a `response.failed` SSE event in the OpenAI
// Responses API protocol after the stream has already started.
//
@ -33,27 +57,27 @@ func writeResponsesFailedSSE(c *gin.Context, errType, message string) bool {
if !ok {
return false
}
rid := synthesizeResponseID(c)
model := requestModel(c)
code := mapResponsesErrorCode(errType)
var b strings.Builder
b.Grow(256 + len(message) + len(model))
b.WriteString(`{"type":"response.failed","response":{`)
b.WriteString(`"id":`)
b.WriteString(strconv.Quote(rid))
b.WriteString(`,"object":"response"`)
if model != "" {
b.WriteString(`,"model":`)
b.WriteString(strconv.Quote(model))
payload, err := json.Marshal(responsesFailedEvent{
Type: "response.failed",
Response: responsesFailedBody{
ID: synthesizeResponseID(c),
Object: "response",
Model: requestModel(c),
Status: "failed",
Output: []any{},
Error: responsesFailedError{
Code: mapResponsesErrorCode(errType),
Message: message,
},
},
})
if err != nil {
_ = c.Error(err)
return true
}
b.WriteString(`,"status":"failed","output":[],"error":{"code":`)
b.WriteString(strconv.Quote(code))
b.WriteString(`,"message":`)
b.WriteString(strconv.Quote(message))
b.WriteString(`}}}`)
if _, err := fmt.Fprintf(c.Writer, "event: response.failed\ndata: %s\n\n", b.String()); err != nil {
if _, err := fmt.Fprintf(c.Writer, "event: response.failed\ndata: %s\n\n", payload); err != nil {
_ = c.Error(err)
return true
}

View File

@ -186,7 +186,7 @@ func TestInboundIsResponses_CoversAllRoutes(t *testing.T) {
}{
{"/v1/responses", true},
{"/v1/responses/compact", true},
{"/responses", true}, // <-- 用户 16 实际走这条
{"/responses", true}, // <-- 用户 16 实际走这条
{"/responses/compact", true},
{"/backend-api/codex/responses", true},
{"/backend-api/codex/responses/compact", true},