feat(proxy): add configurable webhook proxy bypass#339
Conversation
Add WUZAPI_WEBHOOK_USE_PROXY and per-user webhook_use_proxy so webhook deliveries can bypass the WhatsApp proxy while keeping backward-compatible default true. Includes DB migration, API handlers, dashboard UI, and tests.
There was a problem hiding this comment.
Code Review
This pull request adds support for routing webhook deliveries through a per-user proxy, introducing a new webhook_use_proxy setting across the database, API, and frontend. The review feedback identifies several key areas for improvement: a bug in SetProxy where omitting the setting overwrites existing configurations with the global default, an N+1 query performance bottleneck in ListUsers, unhandled database errors in GetStatus and startClient, and missing OpenAPI documentation for the new parameter in POST /session/proxy.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| webhookUseProxy := resolveWebhookUseProxy(t.WebhookUseProxy) | ||
| _, err = s.db.Exec( | ||
| "UPDATE users SET proxy_url = $1, webhook_use_proxy = $2 WHERE id = $3", | ||
| t.ProxyURL, | ||
| webhookUseProxy, | ||
| txtid, | ||
| ) |
There was a problem hiding this comment.
Bug: Omitted webhook_use_proxy resets to global default
When calling SetProxy, if the client does not provide the webhook_use_proxy parameter (i.e., t.WebhookUseProxy is nil), resolveWebhookUseProxy(t.WebhookUseProxy) will return the global default (*globalWebhookUseProxy, which is true by default).
This will silently overwrite and destroy any custom webhook_use_proxy configuration previously saved by the user in the database.
Recommended Solution
Only update webhook_use_proxy in the database if it is explicitly provided in the request. If it is omitted, preserve the existing value.
var webhookUseProxy bool
if t.WebhookUseProxy != nil {
webhookUseProxy = *t.WebhookUseProxy
_, err = s.db.Exec(
"UPDATE users SET proxy_url = $1, webhook_use_proxy = $2 WHERE id = $3",
t.ProxyURL,
webhookUseProxy,
txtid,
)
} else {
err = s.db.QueryRow("SELECT COALESCE(webhook_use_proxy, true) FROM users WHERE id = $1", txtid).Scan(&webhookUseProxy)
if err != nil {
webhookUseProxy = *globalWebhookUseProxy
}
_, err = s.db.Exec("UPDATE users SET proxy_url = $1 WHERE id = $2", t.ProxyURL, txtid)
}| err = s.db.QueryRow( | ||
| "SELECT COALESCE(webhook_use_proxy, true) FROM users WHERE id = $1", | ||
| user.Id, | ||
| ).Scan(&webhookUseProxy) | ||
| if err != nil && err != sql.ErrNoRows { | ||
| log.Warn().Err(err).Str("user_id", user.Id).Msg("Failed to query webhook_use_proxy for user") | ||
| webhookUseProxy = *globalWebhookUseProxy | ||
| } |
There was a problem hiding this comment.
Performance Bottleneck: N+1 Query Problem
In ListUsers, executing a separate database query (s.db.QueryRow) for each user to retrieve webhook_use_proxy introduces an N+1 query problem. This will severely degrade performance as the number of users grows.
Recommended Solution
Instead of querying the database inside the loop, add the webhook_use_proxy field to the usersStruct struct and select it directly in the main SELECT queries on lines 5250 and 5254.
For example, update usersStruct:
type usersStruct struct {
// ... existing fields ...
WebhookUseProxy sql.NullBool `db:"webhook_use_proxy"`
}And update the queries to include COALESCE(webhook_use_proxy, true) AS webhook_use_proxy, then read it directly from user.WebhookUseProxy.Bool inside the loop.
| var webhookUseProxy bool | ||
| s.db.QueryRow( | ||
| "SELECT proxy_url, COALESCE(webhook_use_proxy, true) FROM users WHERE id = $1", | ||
| txtid, | ||
| ).Scan(&proxyURL, &webhookUseProxy) |
There was a problem hiding this comment.
Unhandled database error and uninitialized fallback
In GetStatus, s.db.QueryRow(...).Scan(...) is executed without checking for errors. If a database error occurs or the user is not found, webhookUseProxy will remain uninitialized (defaulting to false), which incorrectly signals that the proxy should be bypassed, even though the global default is true.
Recommended Solution
Initialize webhookUseProxy with the global default value before scanning, and log any database errors (excluding sql.ErrNoRows if appropriate).
| var webhookUseProxy bool | |
| s.db.QueryRow( | |
| "SELECT proxy_url, COALESCE(webhook_use_proxy, true) FROM users WHERE id = $1", | |
| txtid, | |
| ).Scan(&proxyURL, &webhookUseProxy) | |
| webhookUseProxy := *globalWebhookUseProxy | |
| err := s.db.QueryRow( | |
| "SELECT proxy_url, COALESCE(webhook_use_proxy, true) FROM users WHERE id = $1", | |
| txtid, | |
| ).Scan(&proxyURL, &webhookUseProxy) | |
| if err != nil && err != sql.ErrNoRows { | |
| log.Warn().Err(err).Str("user_id", txtid).Msg("Failed to query proxy settings for user") | |
| } |
| err = s.db.QueryRow( | ||
| "SELECT proxy_url, COALESCE(webhook_use_proxy, true) FROM users WHERE id=$1", | ||
| userID, | ||
| ).Scan(&proxyURL, &webhookUseProxy) |
There was a problem hiding this comment.
Observability: Unlogged database error in startClient
If the database query or scan fails in startClient, the error is silently ignored, and proxy configuration is skipped. This makes troubleshooting proxy issues extremely difficult if there is a database connection problem or schema mismatch.
Recommended Solution
Log the error if the query or scan fails.
err = s.db.QueryRow(
"SELECT proxy_url, COALESCE(webhook_use_proxy, true) FROM users WHERE id=$1",
userID,
).Scan(&proxyURL, &webhookUseProxy)
if err != nil && err != sql.ErrNoRows {
log.Error().Err(err).Str("user_id", userID).Msg("Failed to query proxy settings from database")
}| Enable bool `json:"enable"` // Whether to enable or disable proxy | ||
| ProxyURL string `json:"proxy_url"` // Format: "socks5://user:pass@host:port" or "http://host:port" | ||
| Enable bool `json:"enable"` // Whether to enable or disable proxy | ||
| WebhookUseProxy *bool `json:"webhook_use_proxy,omitempty"` |
There was a problem hiding this comment.
Documentation: Missing webhook_use_proxy in OpenAPI spec for POST /session/proxy
You have added webhook_use_proxy to the proxyStruct used by SetProxy (which handles POST /session/proxy), but this new parameter is not documented in the OpenAPI specification (static/api/spec.yml) under the /session/proxy path.
Recommended Solution
Update static/api/spec.yml under /session/proxy -> post -> requestBody -> properties to include webhook_use_proxy:
webhook_use_proxy:
type: boolean
description: Whether webhook deliveries use the configured proxy
example: truePreserve webhook_use_proxy when omitted from SetProxy, load the field in ListUsers without N+1 queries, initialize safe defaults on query errors, log proxy lookup failures in startClient, and document webhook_use_proxy in the OpenAPI spec.
|
Addressed the review feedback in c46fd0c:
|
- Unify SetProxy enable/disable into single UPDATE paths, preserving existing webhook_use_proxy when omitted. - Remove applyRestyProxy one-line wrapper. - Combine GetStatus proxy/S3/HMAC queries into one round-trip.
Closes #338
Summary
Since PR #152, when a per-user proxy is configured, the same proxy is applied to both the WhatsApp websocket client and the internal Resty HTTP client used for outgoing webhook deliveries. In some deployments the proxy is a dedicated exit for WhatsApp traffic, while webhooks need to reach the application backend directly.
This PR adds a configurable option so webhook delivery can bypass the configured proxy while the WhatsApp connection still uses it.
Behavior
webhook_use_proxy = true— webhooks continue to use the proxy when one is configuredfalse: the per-user webhookresty.Clientdoes not callSetProxy(...), whileclient.SetProxyAddress/client.SetSOCKSProxyfor the WhatsApp connection remain unchangedConfiguration
WUZAPI_WEBHOOK_USE_PROXY=true|falsetrueproxyConfig.webhookUseProxywebhook_use_proxyinPOST /session/proxyChanges
webhook_use_proxy BOOLEAN DEFAULT TRUEonusersstartClient: conditionally applies proxy to webhook client based on per-user settingresolveWebhookUseProxyRelated
Test plan
go test ./...docker compose up --buildwebhook_use_proxy=false