package admin import ( "bytes" "errors" "io" "net/http" ) const maxAdminRequestBytes = 8 << 20 // ownerMiddleware is the single execution boundary for ALL editor API routes, // including reads (which must not observe partial edits) and undo/redo. Planning, // file writes, runtime callbacks, reloads and history updates stay in one task. // Register new API routes behind this boundary; handlers must never redispatch. // Network I/O happens outside the task so slow browsers cannot block gameplay. func (s *AdminServer) ownerMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxAdminRequestBytes)) r.Body.Close() if err != nil { status := http.StatusBadRequest var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { status = http.StatusRequestEntityTooLarge } writeJSONError(w, "cannot read request body: "+err.Error(), status) return } if r.Context().Err() != nil { return } r = r.Clone(r.Context()) r.Body = io.NopCloser(bytes.NewReader(body)) response := &bufferedResponse{header: make(http.Header)} var panicValue any if !s.runOnOwner(func() { // Preserve net/http's panic recovery on the HTTP goroutine; // a handler panic must not kill the game's owner loop. defer func() { panicValue = recover() }() if r.Context().Err() == nil { next.ServeHTTP(response, r) } }) { writeJSONError(w, "game is shutting down", http.StatusServiceUnavailable) return } if panicValue != nil { panic(panicValue) } for key, values := range response.header { w.Header()[key] = values } if response.status != 0 { w.WriteHeader(response.status) } _, _ = w.Write(response.body.Bytes()) }) } // bufferedResponse deliberately provides no streaming/hijacking interfaces: // editor API handlers produce finite responses, never network streams. type bufferedResponse struct { header http.Header status int body bytes.Buffer } func (w *bufferedResponse) Header() http.Header { return w.header } func (w *bufferedResponse) WriteHeader(status int) { if w.status == 0 { w.status = status } } func (w *bufferedResponse) Write(p []byte) (int, error) { if w.status == 0 { w.WriteHeader(http.StatusOK) } return w.body.Write(p) }