Files
Charon/docs/plans/current_spec.md
GitHub Actions f936c93896 fix: add missing field handlers in proxy host Update endpoint
Add handlers for enable_standard_headers, forward_auth_enabled, and waf_disabled fields
in the proxy host Update function. These fields were defined in the model but were not
being processed during updates, causing:

- 500 errors when saving proxy host configurations
- Auth pass-through failures for apps like Seerr/Overseerr due to missing X-Forwarded-* headers

Changes:
- backend: Add field handlers for 3 missing fields in proxy_host_handler.go
- backend: Add 5 comprehensive unit tests for field handling
- frontend: Update TypeScript ProxyHost interface with missing fields
- docs: Document fixes in CHANGELOG.md

Tests: All 1147 tests pass (backend 85.6%, frontend 87.7% coverage)
Security: No vulnerabilities (Trivy + govulncheck clean)

Fixes #16 (auth pass-through)
Fixes #17 (500 error on save)
2025-12-20 01:55:52 +00:00

387 lines
12 KiB
Markdown

# Trace Analysis: Proxy Host Update Handler - Missing Fields
**Date:** December 20, 2025
**Status:** Ready for Implementation
**Related Bugs:**
1. **Auth Pass-through Failure**: User can login to Seerr via IP:port but NOT through proxied domain
2. **500 Error on Proxy Host Save**: When editing/saving proxy host, returns 500 Internal Server Error
---
## Executive Summary
After comprehensive trace analysis of both data flows, I have identified **THREE missing fields** in the Update handler and **one configuration issue**:
### Critical Finding: Three Missing Fields in Update Handler
The `Update` handler in [proxy_host_handler.go](../../backend/internal/api/handlers/proxy_host_handler.go#L155) **does not process the following fields** from the payload:
| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `enable_standard_headers` | `*bool` (nullable) | `true` | Controls X-Real-IP, X-Forwarded-* headers |
| `forward_auth_enabled` | `bool` | `false` | Enables forward auth via Charon |
| `waf_disabled` | `bool` | `false` | Disables WAF for this specific host |
This means:
1. When the frontend sends these fields during edit/save, the backend ignores them
2. The fields remain at their default/previous values
3. This could cause a 500 error if downstream code expects properly set values
4. Missing headers break auth pass-through for apps like Seerr/Overseerr
### Secondary Finding: WebSocket Header Configuration is Correct
The WebSocket and auth header pass-through configuration in [types.go](../../backend/internal/caddy/types.go#L127) appears correctly implemented. However, **if `enable_standard_headers` isn't being persisted**, then the generated Caddy config may be missing required headers.
---
## Implementation Plan
### Phase 1: Backend Handler Fix (All 3 Fields)
**Target File:** [backend/internal/api/handlers/proxy_host_handler.go](../../backend/internal/api/handlers/proxy_host_handler.go)
**Location:** After line ~220 (after `enabled` field handling, before `certificate_id`)
#### 1.1 Add `enable_standard_headers` Handler (Nullable Bool Pattern)
This field uses a nullable boolean (`*bool`), following the same pattern as `certificate_id`:
```go
// Handle enable_standard_headers (nullable bool - uses pointer pattern like certificate_id)
if v, ok := payload["enable_standard_headers"]; ok {
if v == nil {
host.EnableStandardHeaders = nil // Explicit null → use default behavior
} else if b, ok := v.(bool); ok {
host.EnableStandardHeaders = &b // Explicit true/false
}
}
```
#### 1.2 Add `forward_auth_enabled` Handler (Regular Bool)
```go
// Handle forward_auth_enabled (regular bool)
if v, ok := payload["forward_auth_enabled"].(bool); ok {
host.ForwardAuthEnabled = v
}
```
#### 1.3 Add `waf_disabled` Handler (Regular Bool)
```go
// Handle waf_disabled (regular bool)
if v, ok := payload["waf_disabled"].(bool); ok {
host.WAFDisabled = v
}
```
### Phase 2: Unit Tests
**Target File:** `backend/internal/api/handlers/proxy_host_handler_test.go`
#### 2.1 TestUpdate_EnableStandardHeaders
```go
func TestUpdate_EnableStandardHeaders(t *testing.T) {
// Setup: Create host with enable_standard_headers = nil (default)
// Test 1: PUT with enable_standard_headers: true → verify DB has true
// Test 2: PUT with enable_standard_headers: false → verify DB has false
// Test 3: PUT with enable_standard_headers: null → verify DB has nil
// Test 4: PUT without field → verify value unchanged
}
```
#### 2.2 TestUpdate_ForwardAuthEnabled
```go
func TestUpdate_ForwardAuthEnabled(t *testing.T) {
// Setup: Create host with forward_auth_enabled = false (default)
// Test 1: PUT with forward_auth_enabled: true → verify DB has true
// Test 2: PUT with forward_auth_enabled: false → verify DB has false
// Test 3: PUT without field → verify value unchanged
}
```
#### 2.3 TestUpdate_WAFDisabled
```go
func TestUpdate_WAFDisabled(t *testing.T) {
// Setup: Create host with waf_disabled = false (default)
// Test 1: PUT with waf_disabled: true → verify DB has true
// Test 2: PUT with waf_disabled: false → verify DB has false
// Test 3: PUT without field → verify value unchanged
}
```
#### 2.4 Integration Test: Create → Update → Verify Caddy Config
```go
func TestUpdate_IntegrationCaddyConfig(t *testing.T) {
// 1. Create proxy host with enable_standard_headers: false
// 2. Verify Caddy config does NOT have X-Forwarded-* headers
// 3. Update host with enable_standard_headers: true
// 4. Verify Caddy config NOW has X-Real-IP, X-Forwarded-Proto, etc.
}
```
#### 2.5 Regression Test: Existing Hosts Without Fields
```go
func TestUpdate_ExistingHostsBackwardCompatibility(t *testing.T) {
// Simulate existing host created before these fields existed
// 1. Insert host directly into DB without these fields (NULL values)
// 2. Perform GET → should return without error
// 3. Perform PUT with unrelated field change → should succeed
// 4. Verify all three fields remain at defaults
}
```
### Phase 3: Integration Verification
After implementation, run full integration tests:
```bash
# Run backend tests with coverage
scripts/go-test-coverage.sh
# Run integration tests
scripts/integration-test.sh
```
---
## Null Handling Specification
### `enable_standard_headers` (Nullable Bool - `*bool`)
This field follows the same pattern as `certificate_id`:
| JSON Value | Go Value | Database | Caddy Behavior |
|------------|----------|----------|----------------|
| `"enable_standard_headers": true` | `*bool → true` | `1` | Add X-Real-IP, X-Forwarded-* headers |
| `"enable_standard_headers": false` | `*bool → false` | `0` | No standard headers |
| `"enable_standard_headers": null` | `*bool → nil` | `NULL` | Use default (true for new hosts) |
| Field omitted | No change | Unchanged | Unchanged |
**Default Behavior:** When `nil`, treated as `true` in config generation:
```go
// From config.go line 338
enableStdHeaders := host.EnableStandardHeaders == nil || *host.EnableStandardHeaders
```
### `forward_auth_enabled` and `waf_disabled` (Regular Bool)
| JSON Value | Go Value | Database |
|------------|----------|----------|
| `"field": true` | `bool → true` | `1` |
| `"field": false` | `bool → false` | `0` |
| Field omitted | No change | Unchanged |
---
## Files to Modify
### Backend (Must Modify)
| File | Change |
|------|--------|
| [proxy_host_handler.go](../../backend/internal/api/handlers/proxy_host_handler.go) | Add handlers for all 3 missing fields |
| [proxy_host_handler_test.go](../../backend/internal/api/handlers/proxy_host_handler_test.go) | Add unit tests for all 3 fields |
### Backend (Verify Only - No Changes Expected)
| File | Verification |
|------|--------------|
| [proxy_host.go](../../backend/internal/models/proxy_host.go) | ✅ All 3 fields exist with correct GORM tags |
| [proxyhost_service.go](../../backend/internal/services/proxyhost_service.go) | ✅ Uses `Select("*")` - passes all fields to DB |
| [config.go](../../backend/internal/caddy/config.go) | ✅ Correctly uses `EnableStandardHeaders` |
| [types.go](../../backend/internal/caddy/types.go) | ✅ `ReverseProxyHandler` handles `enableStandardHeaders` param |
### Frontend (Verify Only - No Changes Expected)
| File | Verification |
|------|--------------|
| [proxyHosts.ts](../../frontend/src/api/proxyHosts.ts) | ⚠️ Missing `forward_auth_enabled` and `waf_disabled` in TypeScript interface |
---
## Verification Checklist
### Pre-Implementation
- [ ] Read and understand all 3 field definitions in `proxy_host.go`
- [ ] Confirm `certificate_id` pattern for nullable bool handling
- [ ] Review existing test patterns in `proxy_host_handler_test.go`
### Implementation
- [ ] Add `enable_standard_headers` handler (nullable bool pattern)
- [ ] Add `forward_auth_enabled` handler (regular bool)
- [ ] Add `waf_disabled` handler (regular bool)
- [ ] Write unit tests for all 3 fields
- [ ] Write integration test for Caddy config generation
### Post-Implementation Verification
- [ ] Database value persists after PUT request (all 3 fields)
- [ ] GET returns updated value (all 3 fields)
- [ ] Caddy config reflects `enable_standard_headers` change
- [ ] All existing tests pass (`go test ./...`)
- [ ] Coverage threshold maintained (≥85%)
### Regression Testing
- [ ] Existing hosts without these fields still work
- [ ] Create new host → verify default values
- [ ] Update existing host → verify partial update works
- [ ] Frontend form submission → backend processes correctly
---
## Root Cause Analysis
### Why These Fields Were Missed
The `Update` handler uses a `map[string]interface{}` payload pattern with manual field extraction. When new fields were added to the model, they were not added to the handler's extraction logic.
**Evidence from proxy_host_handler.go:**
```go
// Lines 177-220: Only these fields are handled
if v, ok := payload["name"].(string); ok { host.Name = v }
if v, ok := payload["domain_names"].(string); ok { host.DomainNames = v }
// ... more fields ...
if v, ok := payload["enabled"].(bool); ok { host.Enabled = v }
// ⚠️ GAP: enable_standard_headers, forward_auth_enabled, waf_disabled NOT HERE
```
### Model Definition (Correct)
**From proxy_host.go:**
```go
// Forward Auth - Line 37
ForwardAuthEnabled bool `json:"forward_auth_enabled" gorm:"default:false"`
// WAF override - Line 40
WAFDisabled bool `json:"waf_disabled" gorm:"default:false"`
// Standard headers - Line 54
EnableStandardHeaders *bool `json:"enable_standard_headers,omitempty" gorm:"default:true"`
```
---
## Complete Handler Field List
Fields handled in Update (proxy_host_handler.go):
**String Fields:**
- [x] name
- [x] domain_names
- [x] forward_scheme
- [x] forward_host
- [x] application
- [x] advanced_config
**Integer Fields:**
- [x] forward_port
**Boolean Fields:**
- [x] ssl_forced
- [x] http2_support
- [x] hsts_enabled
- [x] hsts_subdomains
- [x] block_exploits
- [x] websocket_support
- [x] enabled
- [ ] **forward_auth_enabled** ← MISSING
- [ ] **waf_disabled** ← MISSING
**Nullable Fields:**
- [x] certificate_id
- [x] access_list_id
- [x] security_header_profile_id
- [ ] **enable_standard_headers** ← MISSING
**Complex Fields:**
- [x] locations (JSON array)
---
## Frontend TypeScript Interface Gap
**Current interface in proxyHosts.ts:**
```typescript
export interface ProxyHost {
// ... existing fields ...
enable_standard_headers?: boolean; // ✅ Exists
// ⚠️ MISSING: forward_auth_enabled
// ⚠️ MISSING: waf_disabled
}
```
**Recommendation:** After backend fix, update TypeScript interface:
```typescript
export interface ProxyHost {
// ... existing fields ...
enable_standard_headers?: boolean;
forward_auth_enabled?: boolean; // ADD
waf_disabled?: boolean; // ADD
}
```
---
## Recent Related Changes
### Commit: 81085ec (Dec 19, 2025)
**Title:** feat: add standard proxy headers with backward compatibility
This commit introduced the `enable_standard_headers` feature but **missed adding the handler code for updates**:
- Added field to model ✓
- Added UI checkbox ✓
- Modified config generation ✓
- **Missing: Update handler code** ✗
---
## Appendix: Caddy Config Generation
When `enable_standard_headers` is properly saved and set to `true`, the generated config includes:
```json
{
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Real-IP": ["{http.request.remote.host}"],
"X-Forwarded-Proto": ["{http.request.scheme}"],
"X-Forwarded-Host": ["{http.request.host}"],
"X-Forwarded-Port": ["{http.request.port}"]
}
}
}
}
```
These headers are **required** for:
- Plex SSO authentication pass-through
- Seerr/Overseerr login via proxy
- Any app that needs client's real IP
- Proper protocol detection (HTTPS)