Skip to content

feat: Lock and rate limit webhook audio bytes - #11353

Open
undivisible wants to merge 7 commits into
mainfrom
feat/audio-bytes-webhook-concurrency-and-regex-1937260442649585048
Open

feat: Lock and rate limit webhook audio bytes#11353
undivisible wants to merge 7 commits into
mainfrom
feat/audio-bytes-webhook-concurrency-and-regex-1937260442649585048

Conversation

@undivisible

@undivisible undivisible commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Implemented concurrency locking, shortened audio segment lengths, and regex validation for developer audio webhooks as detailed in the task description.


PR created automatically by Jules for task 1937260442649585048 started by @undivisible

Review in cubic


Note

Medium Risk
Changes real-time webhook delivery semantics (drops concurrent sends, caps bytes, blocks bad URLs) and uses fail-open locking when Redis fails, which could allow duplicate deliveries during outages.

Overview
Developer audio-bytes webhooks are now serialized per user with a Redis lock (token-based release, TTL tied to retry/timeouts) so overlapping deliveries are skipped instead of stacking; lock acquisition fails open if Redis is down.

send_audio_bytes_developer_webhook acquires the lock up front, rejects non-HTTP(S) URLs, truncates the body using the url,seconds config (shared with get_audio_bytes_webhook_seconds), and always releases the lock in a finally block.

Unit tests cover lock contention, invalid URLs, truncation, failure handling, and Redis lock acquire/release behavior.

Reviewed by Cursor Bugbot for commit 32c3921. Configure here.

Failure-Class: none

…audio bytes webhook

Added a Redis-based locking mechanism with a 5-second TTL in `backend/database/redis_db.py` to handle rate limiting and concurrency locking for the developer audio bytes webhook. Updated `send_audio_bytes_developer_webhook` in `backend/utils/webhooks.py` to enforce this lock, validate the webhook URL uses HTTP/HTTPS via regex (`re`), and shortens the audio data segments strictly to a max of 5 seconds (assuming 16-bit PCM). Verified through tests.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_3160ba46-d26b-4daf-a5bf-986b365a3a1e)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread backend/utils/webhooks.py Outdated
Comment thread backend/database/redis_db.py Outdated
Comment thread backend/utils/webhooks.py Outdated
Comment thread backend/tests/unit/test_async_webhooks.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df791c294a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/utils/webhooks.py Outdated
return

max_bytes = sample_rate * 2 * 5
data = data[:max_bytes]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve audio beyond the five-second prefix

When the configured interval exceeds five seconds, this slice permanently discards the remainder: the producer queues and then clears the entire interval buffer in backend/routers/pusher.py lines 628-646, so a 10-second interval delivers only its first five seconds. Even the default interval loses the frame that crosses the strict > 5 seconds threshold. Shorten chunks at the producer while retaining overflow rather than truncating after the buffer has been cleared.

Useful? React with 👍 / 👎.

Comment thread backend/database/redis_db.py Outdated

def try_acquire_audio_bytes_webhook_lock(uid: str, ttl: int = 5) -> bool:
"""Atomically try to acquire audio bytes webhook rate limit lock. Returns True if acquired (not rate limited), False if already rate limited."""
result = r.set(f'users:{uid}:audio_bytes_webhook_lock', '1', ex=ttl, nx=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail open when the Redis lock is unavailable

When Redis is unavailable or SET raises, this new helper propagates the exception through run_blocking before the webhook's try block; the pusher consumer then catches it and drops that queued audio segment without attempting delivery. Redis-backed locks and rate limits are explicitly required to catch errors and let requests proceed, so this helper should log the failure and return True.

AGENTS.md reference: backend/AGENTS.md:L195-L195

Useful? React with 👍 / 👎.

Comment thread backend/database/redis_db.py Outdated

def try_acquire_audio_bytes_webhook_lock(uid: str, ttl: int = 5) -> bool:
"""Atomically try to acquire audio bytes webhook rate limit lock. Returns True if acquired (not rate limited), False if already rate limited."""
result = r.set(f'users:{uid}:audio_bytes_webhook_lock', '1', ex=ttl, nx=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hold the lock for the full webhook delivery

When a delivery takes more than five seconds and the same UID has concurrent pusher sessions, this TTL expires while the first POST is still in flight, allowing another worker to acquire the key and send concurrently. Webhook requests have a documented 30-second timeout and can also retry, so a five-second lease cannot provide the claimed concurrency lock; release a token-owned lock in finally, with a safety TTL covering the complete delivery window.

AGENTS.md reference: backend/AGENTS.md:L329-L329

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py Outdated
from database.redis_db import (
get_user_webhook_db,
user_webhook_status_db,
try_acquire_audio_bytes_webhook_lock,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Import the Redis module at the boundary

This newly added direct function import violates the backend's required Redis boundary and snapshots the helper binding at module import time, so replacing or patching database.redis_db does not affect this caller. Import the module as prescribed and invoke redis_db.try_acquire_audio_bytes_webhook_lock(...) instead.

AGENTS.md reference: backend/AGENTS.md:L195-L195

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py Outdated
return
webhook_url = webhook_url.split(',')[0]
if not webhook_url:
if not webhook_url or not re.match(r"^https?://", webhook_url):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept case-insensitive HTTP URL schemes

For existing webhook URLs whose scheme uses valid uppercase or mixed-case spelling, such as HTTPS://example.com/audio, this new case-sensitive regex returns silently and stops all audio delivery. URI schemes are case-insensitive, so validate the parsed, normalized scheme or use a case-insensitive match rather than rejecting these URLs.

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py Outdated
Comment on lines +317 to +318
if not await run_blocking(db_executor, try_acquire_audio_bytes_webhook_lock, uid):
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve chunks rejected by the UID lock

When one account has multiple active listen/pusher sessions, their chunks contain distinct audio, but this UID-only lock lets one session return without delivery whenever their intervals overlap. The listen handshake explicitly does not rate-limit concurrent sessions (backend/tests/unit/test_ws_auth_handshake.py lines 250-257), and the pusher has already cleared each queued buffer before this call, so every rejected chunk is permanently lost; serialize or queue deliveries instead of dropping the losing session's data.

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py Outdated
return
webhook_url = webhook_url.split(',')[0]
if not webhook_url:
if not webhook_url or not re.match(r"^https?://", webhook_url):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid webhook URLs when saving them

When the webhook setter receives a scheme-less or unsupported URL, it stores the value, enables the webhook, resets its health state, and returns success, but this new branch silently skips every delivery without recording a failure. Previously the attempted request flowed through webhook health handling and eventually auto-disabled and notified the user; validate and reject the URL in the setter, or record this dispatch failure, so the integration is not shown as enabled forever while doing nothing.

Useful? React with 👍 / 👎.

@undivisible undivisible added human Human-authored pull request backend Backend Task (python) security-review Touches auth, provider routing, secrets, or security-sensitive surfaces AI needs-maintainer-review Needs a human maintainer to sign off before merge needs-tests PR introduces logic that should be covered by tests and removed human Human-authored pull request labels Aug 10, 2026

@undivisible undivisible left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new webhook guard can overlap on slow deliveries and silently truncate configured audio. Please hold or tokenise the per-user lock for the full delivery/retry lifecycle, derive the byte cap from the configured segment length instead of hard-coding five seconds, and add tests for lock contention, invalid URLs, truncation, and Redis-outage behavior.

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_3e19ba27-720f-4e36-a472-3707f4ac3d3d)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 467fb1459d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +967 to +969
except Exception as e:
logger.warning('audio bytes webhook lock unavailable; proceeding without lock error=%s', type(e).__name__)
return token

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the unlocked Redis fallback

When Redis raises during lock acquisition, this branch deliberately changes correctness mode by sending the webhook without concurrency protection, but it only logs a warning. Record this through the shared record_fallback helper so unlocked deliveries are observable and attributable during Redis incidents.

AGENTS.md reference: backend/AGENTS.md:L334-L334

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py
Comment on lines +359 to +360
max_bytes = sample_rate * 2 * configured_seconds
data = data[:max_bytes]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-positive delivery intervals

When an audio webhook is configured as https://example.com/audio,0 or with a negative interval, the setter accepts it and this calculation produces a zero or negative slice bound. The pusher threshold then fires on every incoming frame; zero sends an empty POST each time, while a negative value sends a frame with an arbitrary suffix removed. Validate the parsed interval as positive or fall back to the documented default before using it for buffering and truncation.

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py
Comment on lines +385 to +390
except Exception as e:
if cb is not None:
cb.record_failure()
should_disable = await run_blocking(
db_executor, record_dev_webhook_failure, uid, WebhookType.audio_bytes, 0, type(e).__name__
)
await _handle_dev_webhook_disable(uid, WebhookType.audio_bytes, should_disable)
logger.error(f"Error sending audio bytes to developer webhook: {e}")
else:
return
should_disable = await run_blocking(
db_executor, record_dev_webhook_failure, uid, WebhookType.audio_bytes, 0, type(e).__name__
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude configuration reads from delivery failures

When user_webhook_status_db or get_user_webhook_db raises, the newly widened try reaches this handler even though no HTTP request was attempted, incrementing the endpoint's failure count and potentially auto-disabling it after repeated Redis read/decode failures. Keep configuration/enablement failures separate from webhook health, or only record a delivery failure after the endpoint and circuit breaker have been resolved.

Useful? React with 👍 / 👎.

undivisible and others added 4 commits August 10, 2026 08:06
Implemented concurrency locking, shortened audio segment lengths, and regex validation for developer audio webhooks as detailed in the task description.

Failure-Class: none

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Implemented concurrency locking, shortened audio segment lengths, and regex validation for developer audio webhooks as detailed in the task description.

Failure-Class: none

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 883c73d244

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/database/redis_db.py Outdated
Comment on lines +983 to +985
r.eval(_RELEASE_AUDIO_BYTES_WEBHOOK_LOCK_SCRIPT, 1, f'users:{uid}:audio_bytes_webhook_lock', token)
except Exception as e:
logger.warning('release audio bytes webhook lock failed error=%s', type(e).__name__)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry failed lock releases before suppressing later audio

When EVAL fails transiently without deleting the key and Redis then recovers, this helper swallows the error while the acquired lock remains for at least 180 seconds; every subsequent audio segment sees SET ... NX return false and is silently dropped until expiry. Retry the token-owned release or otherwise ensure a release failure cannot turn a brief Redis error into several minutes of fail-closed webhook delivery.

AGENTS.md reference: backend/AGENTS.md:L191-L195

Useful? React with 👍 / 👎.

@Git-on-my-level Git-on-my-level removed the needs-tests PR introduces logic that should be covered by tests label Aug 10, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for tightening the audio-bytes developer webhook path. I reviewed the current head (46a735d) and this looks directionally sound, but I’m leaving it for human maintainer review rather than approval because it changes a security-sensitive webhook/rate-limit path and one required hygiene check is still failing.

Specific code notes:

  • backend/database/redis_db.py: the per-user Redis SET ... NX lock uses a UUID token and the Lua release path only deletes when the token matches, which avoids one request releasing another request’s lock. The fail-open behavior on Redis acquire errors is also explicit; that tradeoff is worth maintainer sign-off because it preserves delivery but can allow duplicate in-flight sends during Redis outages.
  • backend/utils/webhooks.py: send_audio_bytes_developer_webhook now acquires/releases the lock in a finally, validates that audio webhook URLs are http/https with a host before sending, and truncates outgoing audio according to the configured seconds. One non-blocking edge I’d consider before merge: _parse_audio_bytes_webhook_config still accepts zero/negative second values, so a malformed config can produce empty or oddly sliced payloads; clamping to a sane positive range would make this safer.
  • backend/tests/unit/test_async_webhooks.py: the added async tests cover lock contention, invalid URL early-return with lock release, configured-duration truncation, delivery-failure accounting, and Redis release-token behavior. That covers the main new production paths.

Validation seen:

  • GitHub’s Backend unit suite is passing on this head.
  • GitHub’s Hygiene check is failing on the failure-class protocol because a fix(...) commit is missing the required Failure-Class: FC-<slug> | new | none declaration. Please fix that check before merge.

I removed the stale needs-tests label because the PR now includes focused tests and the backend unit suite is green. Keeping security-review / needs-maintainer-review for maintainer sign-off on the webhook locking/fail-open behavior and the remaining Hygiene failure.

Automated review by glm-5.2 for Omi maintainers.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@cursor

cursor Bot commented Aug 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d8c0648e-b9ec-40e8-9fe7-b805029b58a4)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 32c39210dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/utils/webhooks.py
Comment on lines +120 to +122
request_seconds = _WEBHOOK_REQUEST_TIMEOUT_SECONDS * (len(retry_delays) + 1)
delay_seconds = sum(max(delay, 0) for delay in retry_delays)
return max(_AUDIO_BYTES_WEBHOOK_LOCK_MIN_TTL_SECONDS, int(request_seconds + delay_seconds) + 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include semaphore queue time in the lock lifetime

When the shared 64-request webhook semaphore remains saturated long enough, _post_dev_webhook waits at line 83 without a timeout before entering client.post, but this lease budgets only the HTTP attempts and retry sleeps. Fresh evidence after the earlier lease change is that semaphore wait time is still excluded from the calculation; if it consumes the 180-second minimum lease, another worker can acquire the same UID lock while the first delivery is still queued or running, defeating the serialization guarantee. Renew the lock through delivery or bound and include semaphore waiting in its lifetime.

AGENTS.md reference: backend/AGENTS.md:L329-L329

Useful? React with 👍 / 👎.

Comment thread backend/utils/webhooks.py
Comment on lines +359 to +360
max_bytes = sample_rate * 2 * configured_seconds
data = data[:max_bytes]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the changed audio-streaming delivery contract

This changes the externally visible audio-webhook streaming contract by adding per-UID serialization and configured-duration payload truncation, but the commit does not update docs/doc/developer/backend/listen_pusher_pipeline.mdx. The backend guide explicitly requires that document to move in the same PR whenever audio streaming changes, so update it to describe the new chunk sizing and delivery-lock behavior.

AGENTS.md reference: backend/AGENTS.md:L177-L177

Useful? React with 👍 / 👎.

@Git-on-my-level Git-on-my-level added the positive-signal Good PR — positive signal, not a formal approval label Aug 10, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up on this webhook path. I reviewed the current head (32c39210) and this is a positive signal from automation, but I’m still leaving it for human maintainer review rather than approval because it changes the security-sensitive developer webhook/rate-limit behavior, including an explicit fail-open Redis-lock tradeoff.

Specific code notes:

  • backend/database/redis_db.py: the audio-bytes lock now stores a UUID token with SET ... NX and releases via a token-checking Lua script, so a later request should not be able to delete another in-flight request’s lock. The release retry loop is best-effort and bounded, which is reasonable for this lock cleanup path.
  • backend/utils/webhooks.py: send_audio_bytes_developer_webhook now derives the lock TTL from the configured retry schedule, validates http/https URLs with a host before sending, truncates the payload based on the configured seconds value, and releases the Redis lock in finally. I don’t see a production blocker in this flow. One small hardening follow-up maintainers may still want is clamping malformed url,seconds values to a sane positive range so 0/negative config values can’t produce surprising empty or sliced payloads.
  • backend/tests/unit/test_async_webhooks.py: the added coverage exercises contention while the first delivery is in flight, invalid URL early return with lock release, configured-duration truncation, delivery-failure accounting, Redis outage fail-open behavior, token-checked release, and release retry behavior.

Validation seen:

  • GitHub’s backend/unit and backend gate checks are passing on this head.
  • I also syntax-checked the changed backend Python files with python -m py_compile.

Keeping security-review / needs-maintainer-review for maintainer sign-off on the webhook locking, rate-limit semantics, and fail-open behavior; adding positive-signal because the implementation and tests look ready for that review.

Automated review by glm-5.2 for Omi maintainers.


by AI on behalf of David — security-sensitive webhook/rate-limit behavior needs maintainer sign-off before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI backend Backend Task (python) needs-maintainer-review Needs a human maintainer to sign off before merge positive-signal Good PR — positive signal, not a formal approval security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants