v2.0.4: Fix webhook error handling and logging
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 41s

- Webhooks now return 200 OK even on errors (to prevent retries)
- Improved error handling with proper JSON responses
- Enhanced logging for webhook debugging
- Supervisor logs now visible in docker logs (stdout/stderr)
- Fixed TodoistIntegration error display in UI
This commit is contained in:
poignatov
2026-01-01 18:50:55 +03:00
parent 7704de334c
commit edc29fbd97
5 changed files with 140 additions and 22 deletions

View File

@@ -1 +1 @@
2.0.3 2.0.4

View File

@@ -4886,6 +4886,7 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("=== Todoist Webhook Request ===") log.Printf("=== Todoist Webhook Request ===")
log.Printf("Method: %s", r.Method) log.Printf("Method: %s", r.Method)
log.Printf("URL: %s", r.URL.String()) log.Printf("URL: %s", r.URL.String())
log.Printf("Path: %s", r.URL.Path)
log.Printf("RemoteAddr: %s", r.RemoteAddr) log.Printf("RemoteAddr: %s", r.RemoteAddr)
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
@@ -4899,9 +4900,16 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
// Извлекаем токен из URL // Извлекаем токен из URL
vars := mux.Vars(r) vars := mux.Vars(r)
token := vars["token"] token := vars["token"]
log.Printf("Extracted token from URL: '%s'", token)
if token == "" { if token == "" {
log.Printf("Todoist webhook: missing token in URL") log.Printf("Todoist webhook: missing token in URL")
sendErrorWithCORS(w, "Missing webhook token", http.StatusBadRequest) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Missing webhook token",
"message": "Token required in URL",
})
return return
} }
@@ -4915,11 +4923,23 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
log.Printf("Todoist webhook: invalid token: %s", token) log.Printf("Todoist webhook: invalid token: %s", token)
sendErrorWithCORS(w, "Invalid webhook token", http.StatusUnauthorized) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Invalid webhook token",
"message": "Token not found",
})
return return
} else if err != nil { } else if err != nil {
log.Printf("Error finding user by webhook token: %v", err) log.Printf("Error finding user by webhook token: %v", err)
sendErrorWithCORS(w, "Internal server error", http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Internal server error",
"message": "Database error",
})
return return
} }
@@ -4929,7 +4949,13 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
bodyBytes, err := io.ReadAll(r.Body) bodyBytes, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
log.Printf("Error reading request body: %v", err) log.Printf("Error reading request body: %v", err)
sendErrorWithCORS(w, "Error reading request body", http.StatusBadRequest) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Error reading request body",
"message": "Failed to read request",
})
return return
} }
@@ -4948,7 +4974,13 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Provided secret in header: %v (length: %d)", providedSecret != "", len(providedSecret)) log.Printf("Provided secret in header: %v (length: %d)", providedSecret != "", len(providedSecret))
if providedSecret != todoistWebhookSecret { if providedSecret != todoistWebhookSecret {
log.Printf("Invalid Todoist webhook secret provided (expected length: %d, provided length: %d)", len(todoistWebhookSecret), len(providedSecret)) log.Printf("Invalid Todoist webhook secret provided (expected length: %d, provided length: %d)", len(todoistWebhookSecret), len(providedSecret))
sendErrorWithCORS(w, "Unauthorized", http.StatusUnauthorized) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Unauthorized",
"message": "Invalid webhook secret",
})
return return
} }
log.Printf("Webhook secret validated successfully") log.Printf("Webhook secret validated successfully")
@@ -4959,7 +4991,13 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil { if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil {
log.Printf("Error decoding Todoist webhook: %v", err) log.Printf("Error decoding Todoist webhook: %v", err)
log.Printf("Failed to parse body as JSON: %s", string(bodyBytes)) log.Printf("Failed to parse body as JSON: %s", string(bodyBytes))
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Invalid request body",
"message": "Failed to parse JSON",
})
return return
} }
@@ -5025,7 +5063,13 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("ERROR: Todoist webhook: no content or description found in event_data") log.Printf("ERROR: Todoist webhook: no content or description found in event_data")
log.Printf(" title='%s' (empty: %v), description='%s' (empty: %v)", title, title == "", description, description == "") log.Printf(" title='%s' (empty: %v), description='%s' (empty: %v)", title, title == "", description, description == "")
log.Printf("Available keys in event_data: %v", getMapKeys(webhook.EventData)) log.Printf("Available keys in event_data: %v", getMapKeys(webhook.EventData))
sendErrorWithCORS(w, "Missing 'content' or 'description' in event_data", http.StatusBadRequest) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Missing 'content' or 'description' in event_data",
"message": "No content to process",
})
return return
} }
@@ -5038,7 +5082,13 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
response, err := a.processMessageWithoutTelegram(combinedText, userIDPtr) response, err := a.processMessageWithoutTelegram(combinedText, userIDPtr)
if err != nil { if err != nil {
log.Printf("ERROR processing Todoist message: %v", err) log.Printf("ERROR processing Todoist message: %v", err)
sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": err.Error(),
"message": "Error processing message",
})
return return
} }
@@ -5072,14 +5122,22 @@ func (a *App) todoistWebhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("=== Todoist Webhook Request Completed Successfully ===") log.Printf("=== Todoist Webhook Request Completed Successfully ===")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"ok": true,
"message": "Task processed successfully", "message": "Task processed successfully",
"result": response, "result": response,
}) })
} }
func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) { func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("=== Telegram Webhook Request ===")
log.Printf("Method: %s", r.Method)
log.Printf("URL: %s", r.URL.String())
log.Printf("Path: %s", r.URL.Path)
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
log.Printf("OPTIONS request, returning OK")
setCORSHeaders(w) setCORSHeaders(w)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return return
@@ -5089,9 +5147,16 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
// Извлекаем токен из URL // Извлекаем токен из URL
vars := mux.Vars(r) vars := mux.Vars(r)
token := vars["token"] token := vars["token"]
log.Printf("Extracted token from URL: '%s'", token)
if token == "" { if token == "" {
log.Printf("Telegram webhook: missing token in URL") log.Printf("Telegram webhook: missing token in URL")
sendErrorWithCORS(w, "Missing webhook token", http.StatusBadRequest) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Missing webhook token",
"message": "Token required in URL",
})
return return
} }
@@ -5105,11 +5170,24 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
log.Printf("Telegram webhook: invalid token: %s", token) log.Printf("Telegram webhook: invalid token: %s", token)
sendErrorWithCORS(w, "Invalid webhook token", http.StatusUnauthorized) // Возвращаем 200 OK, но логируем ошибку (не хотим, чтобы Telegram повторял запрос)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Invalid webhook token",
"message": "Token not found",
})
return return
} else if err != nil { } else if err != nil {
log.Printf("Error finding user by webhook token: %v", err) log.Printf("Error finding user by webhook token: %v", err)
sendErrorWithCORS(w, "Internal server error", http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Internal server error",
"message": "Database error",
})
return return
} }
@@ -5119,7 +5197,14 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
var update TelegramUpdate var update TelegramUpdate
if err := json.NewDecoder(r.Body).Decode(&update); err != nil { if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
log.Printf("Error decoding Telegram webhook: %v", err) log.Printf("Error decoding Telegram webhook: %v", err)
sendErrorWithCORS(w, "Invalid request body", http.StatusBadRequest) // Возвращаем 200 OK, чтобы Telegram не повторял запрос
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": "Invalid request body",
"message": "Failed to decode webhook",
})
return return
} }
@@ -5134,7 +5219,9 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
} else { } else {
log.Printf("Telegram webhook received: update_id=%d, but no message or edited_message found", update.UpdateID) log.Printf("Telegram webhook received: update_id=%d, but no message or edited_message found", update.UpdateID)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{ w.WriteHeader(http.StatusOK) // Возвращаем 200 OK для Telegram
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": true,
"message": "No message found in update", "message": "No message found in update",
}) })
return return
@@ -5173,7 +5260,9 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
if message.Text == "" { if message.Text == "" {
log.Printf("Telegram webhook: no text in message") log.Printf("Telegram webhook: no text in message")
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{ w.WriteHeader(http.StatusOK) // Возвращаем 200 OK для Telegram
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": true,
"message": "No text in message, ignored", "message": "No text in message, ignored",
}) })
return return
@@ -5191,7 +5280,13 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
response, err := a.processTelegramMessage(fullText, entities, userIDPtr) response, err := a.processTelegramMessage(fullText, entities, userIDPtr)
if err != nil { if err != nil {
log.Printf("Error processing Telegram message: %v", err) log.Printf("Error processing Telegram message: %v", err)
sendErrorWithCORS(w, err.Error(), http.StatusInternalServerError) // Возвращаем 200 OK, чтобы Telegram не повторял запрос
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": false,
"error": err.Error(),
"message": "Error processing message",
})
return return
} }
@@ -5199,6 +5294,7 @@ func (a *App) telegramWebhookHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"ok": true,
"message": "Message processed successfully", "message": "Message processed successfully",
"result": response, "result": response,
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "play-life-web", "name": "play-life-web",
"version": "2.0.3", "version": "2.0.4",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -7,6 +7,7 @@ function TodoistIntegration({ onBack }) {
const [webhookURL, setWebhookURL] = useState('') const [webhookURL, setWebhookURL] = useState('')
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [error, setError] = useState('')
useEffect(() => { useEffect(() => {
fetchWebhookURL() fetchWebhookURL()
@@ -15,14 +16,21 @@ function TodoistIntegration({ onBack }) {
const fetchWebhookURL = async () => { const fetchWebhookURL = async () => {
try { try {
setLoading(true) setLoading(true)
setError('')
const response = await authFetch('/api/integrations/todoist/webhook-url') const response = await authFetch('/api/integrations/todoist/webhook-url')
if (!response.ok) { if (!response.ok) {
throw new Error('Ошибка при загрузке URL webhook') const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.error || 'Ошибка при загрузке URL webhook')
} }
const data = await response.json() const data = await response.json()
setWebhookURL(data.webhook_url) if (data.webhook_url) {
setWebhookURL(data.webhook_url)
} else {
throw new Error('Webhook URL не найден в ответе')
}
} catch (error) { } catch (error) {
console.error('Error fetching webhook URL:', error) console.error('Error fetching webhook URL:', error)
setError(error.message || 'Не удалось загрузить webhook URL')
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -50,6 +58,16 @@ function TodoistIntegration({ onBack }) {
<h2 className="text-lg font-semibold mb-4">Webhook URL</h2> <h2 className="text-lg font-semibold mb-4">Webhook URL</h2>
{loading ? ( {loading ? (
<div className="text-gray-500">Загрузка...</div> <div className="text-gray-500">Загрузка...</div>
) : error ? (
<div className="text-red-600 mb-4">
<p className="mb-2">{error}</p>
<button
onClick={fetchWebhookURL}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
Попробовать снова
</button>
</div>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<input <input

View File

@@ -1,6 +1,7 @@
[supervisord] [supervisord]
nodaemon=true nodaemon=true
logfile=/var/log/supervisor/supervisord.log logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid pidfile=/var/run/supervisord.pid
user=root user=root
@@ -17,8 +18,11 @@ command=/app/backend/main
directory=/app/backend directory=/app/backend
autostart=true autostart=true
autorestart=true autorestart=true
stderr_logfile=/var/log/supervisor/backend.err.log # Логи идут в stdout/stderr контейнера для docker logs
stdout_logfile=/var/log/supervisor/backend.out.log stderr_logfile=/dev/stderr
stdout_logfile=/dev/stdout
stderr_logfile_maxbytes=0
stdout_logfile_maxbytes=0
priority=20 priority=20
# Переменные окружения будут переданы из docker run --env-file # Переменные окружения будут переданы из docker run --env-file
# PORT по умолчанию 8080 внутри контейнера # PORT по умолчанию 8080 внутри контейнера