Implement HTTP timeout configurations and enhance API documentation
- Added optional HTTP server timeout configurations (`HNHMAP_READ_TIMEOUT`, `HNHMAP_WRITE_TIMEOUT`, `HNHMAP_IDLE_TIMEOUT`) to `.env.example` and updated the server initialization in `main.go` to utilize these settings. - Enhanced API documentation for the `rebuildZooms` endpoint to clarify its background processing and polling mechanism for status updates. - Updated `configuration.md` to include new timeout environment variables for better configuration guidance. - Improved error handling in the client for large request bodies, ensuring appropriate responses for oversized payloads.
This commit is contained in:
@@ -321,6 +321,7 @@ func (h *Handlers) APIAdminHideMarker(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
// APIAdminRebuildZooms handles POST /map/api/admin/rebuildZooms.
|
||||
// It starts the rebuild in the background and returns 202 Accepted immediately.
|
||||
func (h *Handlers) APIAdminRebuildZooms(rw http.ResponseWriter, req *http.Request) {
|
||||
if !h.requireMethod(rw, req, http.MethodPost) {
|
||||
return
|
||||
@@ -328,11 +329,22 @@ func (h *Handlers) APIAdminRebuildZooms(rw http.ResponseWriter, req *http.Reques
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
if err := h.Admin.RebuildZooms(req.Context()); err != nil {
|
||||
HandleServiceError(rw, err)
|
||||
h.Admin.StartRebuildZooms()
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// APIAdminRebuildZoomsStatus handles GET /map/api/admin/rebuildZooms/status.
|
||||
// Returns {"running": true|false} so the client can poll until the rebuild finishes.
|
||||
func (h *Handlers) APIAdminRebuildZoomsStatus(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodGet {
|
||||
JSONError(rw, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
if h.requireAdmin(rw, req) == nil {
|
||||
return
|
||||
}
|
||||
running := h.Admin.RebuildZoomsRunning()
|
||||
JSON(rw, http.StatusOK, map[string]bool{"running": running})
|
||||
}
|
||||
|
||||
// APIAdminExport handles GET /map/api/admin/export.
|
||||
@@ -426,6 +438,8 @@ func (h *Handlers) APIAdminRoute(rw http.ResponseWriter, req *http.Request, path
|
||||
h.APIAdminWipe(rw, req)
|
||||
case path == "rebuildZooms":
|
||||
h.APIAdminRebuildZooms(rw, req)
|
||||
case path == "rebuildZooms/status":
|
||||
h.APIAdminRebuildZoomsStatus(rw, req)
|
||||
case path == "export":
|
||||
h.APIAdminExport(rw, req)
|
||||
case path == "merge":
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
)
|
||||
|
||||
// maxClientBodySize is the maximum size for position and marker update request bodies.
|
||||
const maxClientBodySize = 2 * 1024 * 1024 // 2 MB
|
||||
|
||||
var clientPath = regexp.MustCompile(`client/([^/]+)/(.*)`)
|
||||
|
||||
// ClientRouter handles /client/* requests with token-based auth.
|
||||
@@ -112,12 +115,16 @@ func (h *Handlers) clientGridUpload(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func (h *Handlers) clientPositionUpdate(rw http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
buf, err := io.ReadAll(req.Body)
|
||||
buf, err := io.ReadAll(io.LimitReader(req.Body, maxClientBodySize+1))
|
||||
if err != nil {
|
||||
slog.Error("error reading position update", "error", err)
|
||||
JSONError(rw, http.StatusBadRequest, "failed to read body", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if len(buf) > maxClientBodySize {
|
||||
JSONError(rw, http.StatusRequestEntityTooLarge, "request body too large", "PAYLOAD_TOO_LARGE")
|
||||
return
|
||||
}
|
||||
if err := h.Client.UpdatePositions(req.Context(), buf); err != nil {
|
||||
slog.Error("position update failed", "error", err)
|
||||
HandleServiceError(rw, err)
|
||||
@@ -126,12 +133,16 @@ func (h *Handlers) clientPositionUpdate(rw http.ResponseWriter, req *http.Reques
|
||||
|
||||
func (h *Handlers) clientMarkerUpdate(rw http.ResponseWriter, req *http.Request) {
|
||||
defer req.Body.Close()
|
||||
buf, err := io.ReadAll(req.Body)
|
||||
buf, err := io.ReadAll(io.LimitReader(req.Body, maxClientBodySize+1))
|
||||
if err != nil {
|
||||
slog.Error("error reading marker update", "error", err)
|
||||
JSONError(rw, http.StatusBadRequest, "failed to read body", "BAD_REQUEST")
|
||||
return
|
||||
}
|
||||
if len(buf) > maxClientBodySize {
|
||||
JSONError(rw, http.StatusRequestEntityTooLarge, "request body too large", "PAYLOAD_TOO_LARGE")
|
||||
return
|
||||
}
|
||||
if err := h.Client.UploadMarkers(req.Context(), buf); err != nil {
|
||||
slog.Error("marker update failed", "error", err)
|
||||
HandleServiceError(rw, err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -677,3 +678,38 @@ func TestClientRouter_InvalidToken(t *testing.T) {
|
||||
t.Fatalf("expected 401, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRouter_PositionUpdate_BodyTooLarge(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
env.createUser(t, "alice", "pass", app.Auths{app.AUTH_UPLOAD})
|
||||
tokens := env.auth.GenerateTokenForUser(context.Background(), "alice")
|
||||
if len(tokens) == 0 {
|
||||
t.Fatal("expected token")
|
||||
}
|
||||
// Body larger than maxClientBodySize (2MB)
|
||||
bigBody := bytes.Repeat([]byte("x"), 2*1024*1024+1)
|
||||
req := httptest.NewRequest(http.MethodPost, "/client/"+tokens[0]+"/positionUpdate", bytes.NewReader(bigBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
env.h.ClientRouter(rr, req)
|
||||
if rr.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("expected 413, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientRouter_MarkerUpdate_BodyTooLarge(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
env.createUser(t, "alice", "pass", app.Auths{app.AUTH_UPLOAD})
|
||||
tokens := env.auth.GenerateTokenForUser(context.Background(), "alice")
|
||||
if len(tokens) == 0 {
|
||||
t.Fatal("expected token")
|
||||
}
|
||||
bigBody := bytes.Repeat([]byte("x"), 2*1024*1024+1)
|
||||
req := httptest.NewRequest(http.MethodPost, "/client/"+tokens[0]+"/markerUpdate", bytes.NewReader(bigBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
env.h.ClientRouter(rr, req)
|
||||
if rr.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("expected 413, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user