diff options
Diffstat (limited to 'internal/admin/owner.go')
| -rw-r--r-- | internal/admin/owner.go | 82 |
1 files changed, 82 insertions, 0 deletions
diff --git a/internal/admin/owner.go b/internal/admin/owner.go new file mode 100644 index 0000000..02288f7 --- /dev/null +++ b/internal/admin/owner.go @@ -0,0 +1,82 @@ +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) +} |
