Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ Exec requests default to backwards-compatible shell mode. Set `mode: "argv"` wit
| `GET` | `/sandboxes/{id}/files?path=` | Read a file |
| `DELETE` | `/sandboxes/{id}/files?path=` | Delete a file (`recursive=true` for dirs) |
| `GET` | `/sandboxes/{id}/files/list?path=` | List a directory |
| `POST` | `/sandboxes/{id}/files/move` | Move/rename |
| `POST` | `/sandboxes/{id}/files/move` | Move/rename (body: `old_path`, `new_path`) |
| `POST` | `/sandboxes/{id}/files/chmod` | Change permissions |
| `GET` | `/sandboxes/{id}/files/stat?path=` | File metadata |
| `GET` | `/sandboxes/{id}/files/glob?pattern=` | Glob pattern matching |
Expand Down
15 changes: 15 additions & 0 deletions internal/api/middleware/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ func (rw *responseWriter) Write(b []byte) (int, error) {
return n, err
}

// Flush forwards to the underlying writer when it supports flushing.
// Required so streaming handlers (SSE, NDJSON) can flush per chunk after
// the logging middleware has wrapped the writer.
func (rw *responseWriter) Flush() {
if f, ok := rw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}

// Unwrap exposes the underlying writer so http.ResponseController can reach
// optional interfaces (Flusher, Hijacker, etc.) past this wrapper.
func (rw *responseWriter) Unwrap() http.ResponseWriter {
return rw.ResponseWriter
}

func Logging(logger zerolog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
54 changes: 54 additions & 0 deletions internal/api/middleware/logging_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package middleware

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/rs/zerolog"
)

// Regression: the logging wrapper used to swallow http.Flusher so SSE handlers
// hit "streaming not supported". The wrapper must now expose Flush() and
// Unwrap() so both type-assertion and http.ResponseController work.
func TestLoggingWrapperExposesFlusher(t *testing.T) {
var fc flushCapture

handler := Logging(zerolog.Nop())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := w.(http.Flusher); !ok {
t.Fatalf("wrapped writer does not implement http.Flusher")
}

rc := http.NewResponseController(w)
if err := rc.Flush(); err != nil {
t.Fatalf("ResponseController.Flush returned error: %v", err)
}

w.(http.Flusher).Flush()
}))

req := httptest.NewRequest(http.MethodGet, "/api/v1/events", nil)
handler.ServeHTTP(&fc, req)

if fc.flushed < 2 {
t.Fatalf("expected at least 2 flushes, got %d", fc.flushed)
}
}

// flushCapture is a minimal ResponseWriter that records flush invocations.
type flushCapture struct {
header http.Header
flushed int
status int
}

func (f *flushCapture) Header() http.Header {
if f.header == nil {
f.header = make(http.Header)
}
return f.header
}

func (f *flushCapture) Write(b []byte) (int, error) { return len(b), nil }
func (f *flushCapture) WriteHeader(status int) { f.status = status }
func (f *flushCapture) Flush() { f.flushed++ }
Loading