feat(03-01): HTTP handler POST /hook with validation and body size limit
- handleHook validates POST method (405 on others) - MaxBytesReader limits body to 64KB (400 on overflow) - JSON decode errors return 400 - Valid payloads update registry via processHookEvent - 4 tests cover OK, method not allowed, bad JSON, body too large Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
26
hook.go
26
hook.go
@@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// HookEvent represents the JSON payload sent by Claude Code hooks.
|
// HookEvent represents the JSON payload sent by Claude Code hooks.
|
||||||
type HookEvent struct {
|
type HookEvent struct {
|
||||||
@@ -16,6 +20,26 @@ type HookEvent struct {
|
|||||||
ToolName string `json:"tool_name,omitempty"`
|
ToolName string `json:"tool_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleHook is the HTTP handler for POST /hook.
|
||||||
|
// Validates method, limits body size, decodes JSON, and dispatches to processHookEvent.
|
||||||
|
func (d *Daemon) handleHook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, 64*1024)
|
||||||
|
|
||||||
|
var event HookEvent
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.processHookEvent(event)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
// processHookEvent maps a Claude Code hook event to a registry update.
|
// processHookEvent maps a Claude Code hook event to a registry update.
|
||||||
// Ignores empty session IDs and unknown event types.
|
// Ignores empty session IDs and unknown event types.
|
||||||
func (d *Daemon) processHookEvent(event HookEvent) {
|
func (d *Daemon) processHookEvent(event HookEvent) {
|
||||||
|
|||||||
75
hook_test.go
75
hook_test.go
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,3 +228,75 @@ func TestUpdateFromHookClearsWaitingSince(t *testing.T) {
|
|||||||
t.Errorf("WaitingSince should be nil after returning to Working, got %v", list[0].WaitingSince)
|
t.Errorf("WaitingSince should be nil after returning to Working, got %v", list[0].WaitingSince)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleHookPostOK(t *testing.T) {
|
||||||
|
d := newTestDaemon(t)
|
||||||
|
|
||||||
|
body := `{
|
||||||
|
"session_id": "sess-1",
|
||||||
|
"hook_event_name": "Notification",
|
||||||
|
"notification_type": "permission_prompt",
|
||||||
|
"cwd": "/home/user/project"
|
||||||
|
}`
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/hook", strings.NewReader(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
d.handleHook(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
list := d.registry.List()
|
||||||
|
if len(list) != 1 {
|
||||||
|
t.Fatalf("registry len = %d, want 1", len(list))
|
||||||
|
}
|
||||||
|
if list[0].WaitType != "permission" {
|
||||||
|
t.Errorf("WaitType = %q, want %q", list[0].WaitType, "permission")
|
||||||
|
}
|
||||||
|
if list[0].State != "Needs Input" {
|
||||||
|
t.Errorf("State = %q, want %q", list[0].State, "Needs Input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleHookMethodNotAllowed(t *testing.T) {
|
||||||
|
d := newTestDaemon(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/hook", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
d.handleHook(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("status = %d, want 405", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleHookBadJSON(t *testing.T) {
|
||||||
|
d := newTestDaemon(t)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/hook", strings.NewReader("not json"))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
d.handleHook(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("status = %d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleHookBodyTooLarge(t *testing.T) {
|
||||||
|
d := newTestDaemon(t)
|
||||||
|
|
||||||
|
// 65KB body exceeds the 64KB limit
|
||||||
|
bigBody := strings.Repeat("x", 65*1024)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/hook", strings.NewReader(bigBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
d.handleHook(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("status = %d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user