--- HTTP Handler ---
(w http.ResponseWriter, r *http.Request)
| 257 | |
| 258 | // --- HTTP Handler --- |
| 259 | func handler(w http.ResponseWriter, r *http.Request) { |
| 260 | requestStartTime := time.Now() |
| 261 | ctx := r.Context() |
| 262 | |
| 263 | defer func() { |
| 264 | requestDuration.Record(ctx, time.Since(requestStartTime).Seconds()) |
| 265 | }() |
| 266 | |
| 267 | // Method Check |
| 268 | if r.Method != http.MethodPost { |
| 269 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) |
| 270 | slog.Info("request discarded", "reason", "method_not_allowed", "method", r.Method, "remote_addr", r.RemoteAddr) |
| 271 | requestsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("status", "method_not_allowed"))) |
| 272 | return |
| 273 | } |
| 274 | |
| 275 | // Read Body & Size Check |
| 276 | r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) |
| 277 | body, err := io.ReadAll(r.Body) |
| 278 | if err != nil { |
| 279 | var maxBytesErr *http.MaxBytesError |
| 280 | if errors.As(err, &maxBytesErr) { |
| 281 | http.Error(w, fmt.Sprintf("Request body exceeds limit (%d bytes)", maxRequestBodySize), http.StatusRequestEntityTooLarge) |
| 282 | slog.Info("request discarded", "reason", "body_too_large", "limit", maxRequestBodySize, "remote_addr", r.RemoteAddr) |
| 283 | requestsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("status", "body_too_large"))) |
| 284 | } else { |
| 285 | http.Error(w, "Error reading request", http.StatusInternalServerError) |
| 286 | slog.Error("request discarded", "reason", "error_reading_body", "error", err) |
| 287 | requestsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("status", "failed_to_read"))) |
| 288 | } |
| 289 | return |
| 290 | } |
| 291 | |
| 292 | // JSON Validation - Attempt to unmarshal directly to map (requires object) |
| 293 | var fullData map[string]interface{} |
| 294 | if err := json.Unmarshal(body, &fullData); err != nil { |
| 295 | http.Error(w, "Invalid JSON", http.StatusBadRequest) |
| 296 | bodyDetail := "" |
| 297 | if slog.Default().Enabled(context.Background(), slog.LevelDebug) { |
| 298 | bodyDetail = fmt.Sprintf(", Body: %s", string(body)) |
| 299 | } else { |
| 300 | bodyDetail = fmt.Sprintf(", Body snippet: %s", limitString(string(body), 100)) |
| 301 | } |
| 302 | slog.Warn("request discarded", "reason", "invalid_json", "error", err.Error(), "body_detail", bodyDetail) |
| 303 | bytesReceived.Add(ctx, int64(len(body)), metric.WithAttributes(attribute.String("status", "invalid_json"))) |
| 304 | requestsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("status", "invalid_json"))) |
| 305 | return |
| 306 | } |
| 307 | |
| 308 | bytesReceived.Add(ctx, int64(len(body)), metric.WithAttributes(attribute.String("status", "success"))) |
| 309 | |
| 310 | // Add Cloudflare Headers to all requests, excluding IP addresses for GDPR compliance |
| 311 | cfHeaders := make(map[string]string) |
| 312 | cfHeaderPrefixes := []string{"CF-IPCountry", "CF-Ray", "CF-IPCity", "CF-IPContinent", "CF-IPRegion", "CF-IPTimeZone", "CF-IPCOLO"} |
| 313 | // Explicitly excluding IP-related headers: CF-Connecting-IP, CF-IPLatitude, CF-IPLongitude, CF-Visitor |
| 314 | for _, name := range cfHeaderPrefixes { |
| 315 | if value := r.Header.Get(name); value != "" { |
| 316 | key := strings.TrimPrefix(name, "CF-") |
searching dependent graphs…