package service import ( "apigo.cc/go/cast" "apigo.cc/go/file" "apigo.cc/go/jsmod" "fmt" "io" "net/http" "reflect" ) // Response 封装 http.ResponseWriter type Response struct { Id string Writer http.ResponseWriter `js:"-"` Code int body []byte `js:"-"` outLen int `js:"-"` changed bool `js:"-"` filteredWrite bool `js:"-"` headerWritten bool `js:"-"` dontLog200 bool `js:"-"` dontLogArgs []string `js:"-"` ProxyHeader *http.Header `js:"-"` server *WebServer `js:"-"` } func (r *Response) SetCookie(cookie *Cookie) { if cookie == nil { return } http.SetCookie(r.Writer, &http.Cookie{ Name: cookie.Name, Value: cookie.Value, Path: cookie.Path, Domain: cookie.Domain, MaxAge: cookie.MaxAge, Secure: cookie.Secure, HttpOnly: cookie.HttpOnly, }) } // NewResponse 创建 Response 包装 func NewResponse(writer http.ResponseWriter, server *WebServer) *Response { return &Response{ Writer: writer, Code: http.StatusOK, server: server, } } // Header 获取响应头部 func (r *Response) Header() *Header { if r.ProxyHeader != nil { return &Header{H: *r.ProxyHeader} } return &Header{H: r.Writer.Header()} } // Write 写入响应内容 func (r *Response) Write(bytes []byte) (int, error) { r.checkWriteHeader() r.changed = true r.outLen += len(bytes) // 缓冲 body 用于日志记录 r.keepBody(bytes) if r.ProxyHeader != nil { r.copyProxyHeader() } n, err := r.Writer.Write(bytes) if err != nil { return n, jsmod.MakeError(err) } return n, nil } // WriteFiltered buffers bytes for registered output filters. Direct Write calls // remain streamable and bypass output filters. func (r *Response) WriteFiltered(bytes []byte) (int, error) { if r.server == nil || !r.server.hasOutFilter { return r.Write(bytes) } r.checkWriteHeader() r.changed = true r.filteredWrite = true r.outLen += len(bytes) r.body = append(r.body, bytes...) return len(bytes), nil } // WriteBytes writes a byte array received through a dynamic runtime without // converting its bytes to text first. This keeps UTF-8 stream chunks intact. func (r *Response) WriteBytes(value any) (int, error) { bytes, err := responseBytes(value) if err != nil { return 0, jsmod.MakeError(err) } return r.Write(bytes) } func responseBytes(value any) ([]byte, error) { if value == nil { return nil, nil } if bytes, ok := value.([]byte); ok { return bytes, nil } if text, ok := value.(string); ok { return []byte(text), nil } rv := reflect.ValueOf(value) if rv.Kind() != reflect.Array && rv.Kind() != reflect.Slice { return nil, fmt.Errorf("response bytes must be a string or byte array, got %T", value) } bytes := make([]byte, rv.Len()) for i := 0; i < rv.Len(); i++ { item := rv.Index(i) for item.IsValid() && (item.Kind() == reflect.Interface || item.Kind() == reflect.Pointer) { if item.IsNil() { return nil, fmt.Errorf("response byte at index %d is nil", i) } item = item.Elem() } if !item.IsValid() { return nil, fmt.Errorf("response byte at index %d is nil", i) } switch item.Kind() { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if item.Uint() > 255 { return nil, fmt.Errorf("response byte at index %d is out of range", i) } bytes[i] = byte(item.Uint()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if item.Int() < 0 || item.Int() > 255 { return nil, fmt.Errorf("response byte at index %d is out of range", i) } bytes[i] = byte(item.Int()) case reflect.Float32, reflect.Float64: n := item.Float() if n < 0 || n > 255 || n != float64(byte(n)) { return nil, fmt.Errorf("response byte at index %d is out of range", i) } bytes[i] = byte(n) default: return nil, fmt.Errorf("response byte at index %d has type %s", i, item.Kind()) } } return bytes, nil } // keepBody 缓冲数据用于日志记录,限制大小防止内存问题 func (r *Response) keepBody(bytes []byte) { maxBuf := 200 if r.server != nil && r.server.Config.LogOutputMaxSize > 0 { maxBuf = r.server.Config.LogOutputMaxSize } if len(r.body) < maxBuf { space := maxBuf - len(r.body) if len(bytes) <= space { r.body = append(r.body, bytes...) } else { r.body = append(r.body, bytes[:space]...) } } } // PhysicalWrite 物理写入网线,绕过过滤器缓冲逻辑 func (r *Response) PhysicalWrite(bytes []byte) (int, error) { r.checkWriteHeader() if r.ProxyHeader != nil { r.copyProxyHeader() } n, err := r.Writer.Write(bytes) if err != nil { return n, jsmod.MakeError(err) } return n, nil } // WriteString 写入字符串响应 func (r *Response) WriteString(s string) (int, error) { n, err := r.Write([]byte(s)) if err != nil { return n, jsmod.MakeError(err) } return n, nil } // WriteHeader 设置响应状态码 func (r *Response) WriteHeader(code int) { r.changed = true r.Code = code if r.ProxyHeader != nil && (r.Code == http.StatusBadGateway || r.Code == http.StatusServiceUnavailable || r.Code == http.StatusGatewayTimeout) { return } if r.ProxyHeader != nil { r.copyProxyHeader() } } func (r *Response) checkWriteHeader() { if !r.headerWritten { r.headerWritten = true if r.Code != http.StatusOK { r.Writer.WriteHeader(r.Code) } } } func (r *Response) copyProxyHeader() { src := *r.ProxyHeader dst := r.Writer.Header() for k, vv := range src { for _, v := range vv { dst.Add(k, v) } } r.ProxyHeader = nil } // Flush 刷新响应缓冲区 func (r *Response) Flush() { if flusher, ok := r.Writer.(http.Flusher); ok { flusher.Flush() } } // GetStatusCode 获取当前状态码 func (r *Response) GetStatusCode() int { return r.Code } // GetBody 获取响应内容 func (r *Response) GetBody() []byte { return r.body } // ClearBody 清空响应内容缓冲区 (用于过滤器替换内容) func (r *Response) ClearBody() { r.body = nil r.outLen = 0 // 注意:这里我们不重置 headerWritten 和 Code,因为 Header 已经发出去了。 // 但是在某些测试环境下(如 httptest.Recorder),我们可以尝试“假装”没写过。 // 实际上,生产环境下 Header 发出去就收不回来了,所以注入只能发生在 Body 层面。 } // DontLog200 标记不记录 200 状态码的日志 func (r *Response) DontLog200() { r.dontLog200 = true } // Location 设置重定向地址 func (r *Response) Location(location string) { r.WriteHeader(http.StatusFound) r.Header().Set("Location", location) } // SendFile 发送文件 func (r *Response) SendFile(contentType, filename string) { r.Header().Set("Content-Type", contentType) if data, err := file.ReadBytes(filename); err == nil { _, _ = r.Write(data) } } // DownloadFile 下载文件 func (r *Response) DownloadFile(contentType, filename string, data any) { if contentType == "" { contentType = "application/octet-stream" } r.Header().Set("Content-Type", contentType) if filename != "" { r.Header().Set("Content-Disposition", "attachment; filename="+filename) } var outBytes []byte var reader io.Reader switch v := data.(type) { case []byte: outBytes = v case string: outBytes = []byte(v) case io.Reader: reader = v default: outBytes, _ = cast.ToJSONBytes(data) } if outBytes != nil { r.Header().Set("Content-Length", cast.String(len(outBytes))) _, _ = r.Write(outBytes) } else if reader != nil { _, _ = io.Copy(r, reader) } }