mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 12:20:52 +08:00
258 lines
7.3 KiB
Go
258 lines
7.3 KiB
Go
// Package middleware provides HTTP middleware components for the CLI Proxy API server.
|
|
// This includes request logging middleware and response writer wrappers that capture
|
|
// request and response data for logging purposes while maintaining zero-latency performance.
|
|
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/luispater/CLIProxyAPI/internal/logging"
|
|
)
|
|
|
|
// RequestInfo holds information about the current request for logging purposes.
|
|
type RequestInfo struct {
|
|
URL string
|
|
Method string
|
|
Headers map[string][]string
|
|
Body []byte
|
|
}
|
|
|
|
// ResponseWriterWrapper wraps gin.ResponseWriter to capture response data for logging.
|
|
// It maintains zero-latency performance by prioritizing client response over logging operations.
|
|
type ResponseWriterWrapper struct {
|
|
gin.ResponseWriter
|
|
body *bytes.Buffer
|
|
isStreaming bool
|
|
streamWriter logging.StreamingLogWriter
|
|
chunkChannel chan []byte
|
|
logger logging.RequestLogger
|
|
requestInfo *RequestInfo
|
|
statusCode int
|
|
headers map[string][]string
|
|
}
|
|
|
|
// NewResponseWriterWrapper creates a new response writer wrapper.
|
|
func NewResponseWriterWrapper(w gin.ResponseWriter, logger logging.RequestLogger, requestInfo *RequestInfo) *ResponseWriterWrapper {
|
|
return &ResponseWriterWrapper{
|
|
ResponseWriter: w,
|
|
body: &bytes.Buffer{},
|
|
logger: logger,
|
|
requestInfo: requestInfo,
|
|
headers: make(map[string][]string),
|
|
}
|
|
}
|
|
|
|
// Write intercepts response data while maintaining normal Gin functionality.
|
|
// CRITICAL: This method prioritizes client response (zero-latency) over logging operations.
|
|
func (w *ResponseWriterWrapper) Write(data []byte) (int, error) {
|
|
// Ensure headers are captured before first write
|
|
// This is critical because Write() may trigger WriteHeader() internally
|
|
w.ensureHeadersCaptured()
|
|
|
|
// CRITICAL: Write to client first (zero latency)
|
|
n, err := w.ResponseWriter.Write(data)
|
|
|
|
// THEN: Handle logging based on response type
|
|
if w.isStreaming {
|
|
// For streaming responses: Send to async logging channel (non-blocking)
|
|
if w.chunkChannel != nil {
|
|
select {
|
|
case w.chunkChannel <- append([]byte(nil), data...): // Non-blocking send with copy
|
|
default: // Channel full, skip logging to avoid blocking
|
|
}
|
|
}
|
|
} else {
|
|
// For non-streaming responses: Buffer complete response
|
|
w.body.Write(data)
|
|
}
|
|
|
|
return n, err
|
|
}
|
|
|
|
// WriteHeader captures the status code and detects streaming responses.
|
|
func (w *ResponseWriterWrapper) WriteHeader(statusCode int) {
|
|
w.statusCode = statusCode
|
|
|
|
// Capture response headers using the new method
|
|
w.captureCurrentHeaders()
|
|
|
|
// Detect streaming based on Content-Type
|
|
contentType := w.ResponseWriter.Header().Get("Content-Type")
|
|
w.isStreaming = w.detectStreaming(contentType)
|
|
|
|
// If streaming, initialize streaming log writer
|
|
if w.isStreaming && w.logger.IsEnabled() {
|
|
streamWriter, err := w.logger.LogStreamingRequest(
|
|
w.requestInfo.URL,
|
|
w.requestInfo.Method,
|
|
w.requestInfo.Headers,
|
|
w.requestInfo.Body,
|
|
)
|
|
if err == nil {
|
|
w.streamWriter = streamWriter
|
|
w.chunkChannel = make(chan []byte, 100) // Buffered channel for async writes
|
|
|
|
// Start async chunk processor
|
|
go w.processStreamingChunks()
|
|
|
|
// Write status immediately
|
|
_ = streamWriter.WriteStatus(statusCode, w.headers)
|
|
}
|
|
}
|
|
|
|
// Call original WriteHeader
|
|
w.ResponseWriter.WriteHeader(statusCode)
|
|
}
|
|
|
|
// ensureHeadersCaptured ensures that response headers are captured at the right time.
|
|
// This method can be called multiple times safely and will always capture the latest headers.
|
|
func (w *ResponseWriterWrapper) ensureHeadersCaptured() {
|
|
// Always capture the current headers to ensure we have the latest state
|
|
w.captureCurrentHeaders()
|
|
}
|
|
|
|
// captureCurrentHeaders captures the current response headers from the underlying ResponseWriter.
|
|
func (w *ResponseWriterWrapper) captureCurrentHeaders() {
|
|
// Initialize headers map if needed
|
|
if w.headers == nil {
|
|
w.headers = make(map[string][]string)
|
|
}
|
|
|
|
// Capture all current headers from the underlying ResponseWriter
|
|
for key, values := range w.ResponseWriter.Header() {
|
|
// Make a copy of the values slice to avoid reference issues
|
|
headerValues := make([]string, len(values))
|
|
copy(headerValues, values)
|
|
w.headers[key] = headerValues
|
|
}
|
|
}
|
|
|
|
// detectStreaming determines if the response is streaming based on Content-Type and request analysis.
|
|
func (w *ResponseWriterWrapper) detectStreaming(contentType string) bool {
|
|
// Check Content-Type for Server-Sent Events
|
|
if strings.Contains(contentType, "text/event-stream") {
|
|
return true
|
|
}
|
|
|
|
// Check request body for streaming indicators
|
|
if w.requestInfo.Body != nil {
|
|
bodyStr := string(w.requestInfo.Body)
|
|
if strings.Contains(bodyStr, `"stream": true`) || strings.Contains(bodyStr, `"stream":true`) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// processStreamingChunks handles async processing of streaming chunks.
|
|
func (w *ResponseWriterWrapper) processStreamingChunks() {
|
|
if w.streamWriter == nil || w.chunkChannel == nil {
|
|
return
|
|
}
|
|
|
|
for chunk := range w.chunkChannel {
|
|
w.streamWriter.WriteChunkAsync(chunk)
|
|
}
|
|
}
|
|
|
|
// Finalize completes the logging process for the response.
|
|
func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
|
|
if !w.logger.IsEnabled() {
|
|
return nil
|
|
}
|
|
|
|
if w.isStreaming {
|
|
// Close streaming channel and writer
|
|
if w.chunkChannel != nil {
|
|
close(w.chunkChannel)
|
|
w.chunkChannel = nil
|
|
}
|
|
|
|
if w.streamWriter != nil {
|
|
return w.streamWriter.Close()
|
|
}
|
|
} else {
|
|
// Capture final status code and headers if not already captured
|
|
finalStatusCode := w.statusCode
|
|
if finalStatusCode == 0 {
|
|
// Get status from underlying ResponseWriter if available
|
|
if statusWriter, ok := w.ResponseWriter.(interface{ Status() int }); ok {
|
|
finalStatusCode = statusWriter.Status()
|
|
} else {
|
|
finalStatusCode = 200 // Default
|
|
}
|
|
}
|
|
|
|
// Ensure we have the latest headers before finalizing
|
|
w.ensureHeadersCaptured()
|
|
|
|
// Use the captured headers as the final headers
|
|
finalHeaders := make(map[string][]string)
|
|
for key, values := range w.headers {
|
|
// Make a copy of the values slice to avoid reference issues
|
|
headerValues := make([]string, len(values))
|
|
copy(headerValues, values)
|
|
finalHeaders[key] = headerValues
|
|
}
|
|
|
|
var apiRequestBody []byte
|
|
apiRequest, isExist := c.Get("API_REQUEST")
|
|
if isExist {
|
|
var ok bool
|
|
apiRequestBody, ok = apiRequest.([]byte)
|
|
if !ok {
|
|
apiRequestBody = nil
|
|
}
|
|
}
|
|
|
|
var apiResponseBody []byte
|
|
apiResponse, isExist := c.Get("API_RESPONSE")
|
|
if isExist {
|
|
var ok bool
|
|
apiResponseBody, ok = apiResponse.([]byte)
|
|
if !ok {
|
|
apiResponseBody = nil
|
|
}
|
|
}
|
|
|
|
// Log complete non-streaming response
|
|
return w.logger.LogRequest(
|
|
w.requestInfo.URL,
|
|
w.requestInfo.Method,
|
|
w.requestInfo.Headers,
|
|
w.requestInfo.Body,
|
|
finalStatusCode,
|
|
finalHeaders,
|
|
w.body.Bytes(),
|
|
apiRequestBody,
|
|
apiResponseBody,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Status returns the HTTP status code of the response.
|
|
func (w *ResponseWriterWrapper) Status() int {
|
|
if w.statusCode == 0 {
|
|
return 200 // Default status code
|
|
}
|
|
return w.statusCode
|
|
}
|
|
|
|
// Size returns the size of the response body.
|
|
func (w *ResponseWriterWrapper) Size() int {
|
|
if w.isStreaming {
|
|
return -1 // Unknown size for streaming responses
|
|
}
|
|
return w.body.Len()
|
|
}
|
|
|
|
// Written returns whether the response has been written.
|
|
func (w *ResponseWriterWrapper) Written() bool {
|
|
return w.statusCode != 0
|
|
}
|