Files
agent-evals/backend/internal/api/handlers.go
Evan Reichard 10f584f9a8 fix(api): ensure files array is never null in API response
Add null safety checks to prevent TypeError when backend returns null
instead of empty array for files list. Initialize empty slices on backend
and add null coalescing on frontend when accessing files state.

- Backend: Initialize files slice to always return [] instead of null
- Frontend: Add null checks for files state in all map/filter operations
2026-02-06 08:59:11 -05:00

177 lines
4.5 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/evanreichard/markdown-editor/internal/logging"
"github.com/evanreichard/markdown-editor/internal/storage"
)
// Handlers contains the HTTP handlers
type Handlers struct {
storage *storage.Storage
}
// New creates a new Handlers instance
func New(s *storage.Storage) *Handlers {
logging.Logger.Info("API handlers initialized")
return &Handlers{storage: s}
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
}
// sendError sends a JSON error response
func (h *Handlers) sendError(w http.ResponseWriter, r *http.Request, statusCode int, err error) {
logging.Logger.WithFields(map[string]interface{}{
"path": r.URL.Path,
"status": statusCode,
"error": err.Error(),
}).Warn("API error")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(ErrorResponse{Error: err.Error()})
}
// ListFiles handles GET /api/files
func (h *Handlers) ListFiles(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
files, err := h.storage.List()
if err != nil {
h.sendError(w, r, http.StatusInternalServerError, err)
return
}
// Ensure we always encode an array, never null
if files == nil {
files = []*storage.File{}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(files)
}
// GetFile handles GET /api/files/:name
func (h *Handlers) GetFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/files/")
if name == "" {
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required"))
return
}
file, err := h.storage.Get(name)
if err != nil {
if err == storage.ErrFileNotFound {
h.sendError(w, r, http.StatusNotFound, err)
return
}
h.sendError(w, r, http.StatusBadRequest, err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(file)
}
// CreateFile handles POST /api/files
func (h *Handlers) CreateFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
var file storage.File
if err := json.NewDecoder(r.Body).Decode(&file); err != nil {
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
result, err := h.storage.Create(file.Name, file.Content)
if err != nil {
if err == storage.ErrInvalidName {
h.sendError(w, r, http.StatusBadRequest, err)
return
}
h.sendError(w, r, http.StatusConflict, err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(result)
}
// UpdateFile handles PUT /api/files/:name
func (h *Handlers) UpdateFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/files/")
if name == "" {
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required"))
return
}
var file storage.File
if err := json.NewDecoder(r.Body).Decode(&file); err != nil {
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
result, err := h.storage.Update(name, file.Content)
if err != nil {
if err == storage.ErrFileNotFound {
h.sendError(w, r, http.StatusNotFound, err)
return
}
h.sendError(w, r, http.StatusBadRequest, err)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// DeleteFile handles DELETE /api/files/:name
func (h *Handlers) DeleteFile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
h.sendError(w, r, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
name := strings.TrimPrefix(r.URL.Path, "/api/files/")
if name == "" {
h.sendError(w, r, http.StatusBadRequest, fmt.Errorf("file name required"))
return
}
err := h.storage.Delete(name)
if err != nil {
if err == storage.ErrFileNotFound {
h.sendError(w, r, http.StatusNotFound, err)
return
}
h.sendError(w, r, http.StatusBadRequest, err)
return
}
w.WriteHeader(http.StatusNoContent)
}