167 lines
5.0 KiB
Go
167 lines
5.0 KiB
Go
package relay
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const maxCaptureSize = 1 << 18 // 256KB total capture budget
|
|
const headLimit = maxCaptureSize * 3 / 4 // 192KB for head (first bytes)
|
|
const tailLimit = maxCaptureSize / 4 // 64KB for tail ring buffer (last bytes)
|
|
|
|
// ResponseCaptureWriter 包装 http.ResponseWriter,在写入客户端的同时捕获响应体。
|
|
// 当响应超过 headLimit 时,使用环形缓冲区持续捕获尾部数据,
|
|
// 确保流式响应末尾的 usage、finish_reason 等重要信息不会丢失。
|
|
type ResponseCaptureWriter struct {
|
|
http.ResponseWriter
|
|
mu sync.Mutex
|
|
buf []byte // head: first headLimit bytes
|
|
tailBuf []byte // ring buffer: last tailLimit bytes (lazily allocated)
|
|
tailPos int // next write position in tailBuf
|
|
tailWrapped bool // whether tailBuf has wrapped around
|
|
status int
|
|
bytesWritten atomic.Int64
|
|
}
|
|
|
|
// NewResponseCaptureWriter 创建响应体捕获 writer
|
|
func NewResponseCaptureWriter(w http.ResponseWriter) *ResponseCaptureWriter {
|
|
return &ResponseCaptureWriter{ResponseWriter: w}
|
|
}
|
|
|
|
// WriteHeader 捕获状态码并委托给底层 writer
|
|
func (w *ResponseCaptureWriter) WriteHeader(code int) {
|
|
w.mu.Lock()
|
|
if w.status != 0 {
|
|
w.mu.Unlock()
|
|
return
|
|
}
|
|
w.status = code
|
|
w.mu.Unlock()
|
|
w.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
// Write 写入客户端并同时捕获到 buffer(head + tail 环形缓冲区)
|
|
func (w *ResponseCaptureWriter) Write(b []byte) (int, error) {
|
|
w.mu.Lock()
|
|
if w.status == 0 {
|
|
w.status = http.StatusOK
|
|
}
|
|
w.mu.Unlock()
|
|
|
|
n, err := w.ResponseWriter.Write(b)
|
|
w.bytesWritten.Add(int64(n))
|
|
w.mu.Lock()
|
|
if len(w.buf) < headLimit {
|
|
remaining := headLimit - len(w.buf)
|
|
if len(b) <= remaining {
|
|
w.buf = append(w.buf, b...)
|
|
} else {
|
|
// 切点回退到多字节字符的起始字节(UTF-8 最长 4 字节,最多回退 3 字节),
|
|
// 整字符归 tail,避免 head 末尾留下半个字符产生无效 UTF-8。
|
|
// 跨 Write 到达的割裂字符无法在此对齐,由 Body() 出口清洗兜底。
|
|
cut := remaining
|
|
for cut > 0 && remaining-cut < 3 && !utf8.RuneStart(b[cut]) {
|
|
cut--
|
|
}
|
|
w.buf = append(w.buf, b[:cut]...)
|
|
w.appendToTail(b[cut:])
|
|
}
|
|
} else {
|
|
w.appendToTail(b)
|
|
}
|
|
w.mu.Unlock()
|
|
return n, err
|
|
}
|
|
|
|
// appendToTail 写入环形缓冲区,始终保留最后 tailLimit 字节
|
|
func (w *ResponseCaptureWriter) appendToTail(b []byte) {
|
|
if w.tailBuf == nil {
|
|
w.tailBuf = make([]byte, tailLimit)
|
|
}
|
|
for len(b) > 0 {
|
|
remaining := tailLimit - w.tailPos
|
|
if len(b) <= remaining {
|
|
copy(w.tailBuf[w.tailPos:], b)
|
|
w.tailPos += len(b)
|
|
break
|
|
}
|
|
copy(w.tailBuf[w.tailPos:], b[:remaining])
|
|
b = b[remaining:]
|
|
w.tailPos = 0
|
|
w.tailWrapped = true
|
|
}
|
|
}
|
|
|
|
// Flush 实现 http.Flusher 接口,立即将缓冲数据推送到客户端。
|
|
func (w *ResponseCaptureWriter) Flush() {
|
|
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
|
f.Flush()
|
|
}
|
|
}
|
|
|
|
// Body 返回已捕获的响应体内容。
|
|
// 短响应(<= headLimit)直接返回;长响应保留 head + truncation marker + tail。
|
|
// 两个无效 UTF-8 来源都在出口兜底:分块到达的流可能在任意字节边界切开多字节字符
|
|
// (跨 Write 的割裂无法在写入侧对齐),上游也可能直接输出非法字节;不清洗会导致
|
|
// 审计日志 INSERT 被 UTF8 编码的 PostgreSQL 以 invalid byte sequence 拒绝、整条丢失。
|
|
func (w *ResponseCaptureWriter) Body() string {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.tailBuf == nil {
|
|
return strings.ToValidUTF8(string(w.buf), string(utf8.RuneError))
|
|
}
|
|
var sb strings.Builder
|
|
sb.Write(w.buf)
|
|
sb.WriteString("\n...[truncated]...\n")
|
|
if w.tailWrapped {
|
|
// 环形缓冲区起点落在残行中间:残行前半已被覆盖、不可恢复,
|
|
// 跳到下一行行首,让 tail 从完整 SSE 行开始;找不到换行则原样输出。
|
|
tail := w.tailBuf[w.tailPos:]
|
|
if idx := bytes.IndexByte(tail, '\n'); idx >= 0 {
|
|
tail = tail[idx+1:]
|
|
}
|
|
sb.Write(tail)
|
|
sb.Write(w.tailBuf[:w.tailPos])
|
|
} else {
|
|
sb.Write(w.tailBuf[:w.tailPos])
|
|
}
|
|
return strings.ToValidUTF8(sb.String(), string(utf8.RuneError))
|
|
}
|
|
|
|
// StatusCode 返回捕获的 HTTP 状态码
|
|
func (w *ResponseCaptureWriter) StatusCode() int {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.status == 0 {
|
|
return 200
|
|
}
|
|
return w.status
|
|
}
|
|
|
|
// BytesWritten 返回写入客户端的总字节数
|
|
func (w *ResponseCaptureWriter) BytesWritten() int64 {
|
|
return w.bytesWritten.Load()
|
|
}
|
|
|
|
// ResponseCommitted reports whether headers or body bytes have already been sent.
|
|
func (w *ResponseCaptureWriter) ResponseCommitted() bool {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.status != 0 || w.bytesWritten.Load() > 0
|
|
}
|
|
|
|
// ResponseHeaders 返回响应头快照(调用时从底层 ResponseWriter 提取)
|
|
func (w *ResponseCaptureWriter) ResponseHeaders() map[string]string {
|
|
headers := make(map[string]string)
|
|
for k, vals := range w.ResponseWriter.Header() {
|
|
if len(vals) > 0 {
|
|
headers[k] = vals[0]
|
|
}
|
|
}
|
|
return headers
|
|
}
|