Skip to content

fix: handle retryable http responses across consumers - #265

Open
pauldambra wants to merge 6 commits into
posthog/fix-socket-retry-statefrom
posthog/fix-consumer-http-retries
Open

pauldambra wants to merge 6 commits into
posthog/fix-socket-retry-statefrom
posthog/fix-consumer-http-retries

Conversation

@pauldambra

@pauldambra pauldambra commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

🤖 Automated PR prepared with PostHog Desktop.

💡 Motivation and Context

Applications that use foreground ForkCurl can lose batches because curl reports success when the server rejects a request. Socket also misses HTTP 408 retries and Retry-After delays.

  • Both consumers now retry transient HTTP responses with stable request bodies and a bounded attempt count.
  • Socket reads complete response headers with a size limit and a total timeout.
  • The consumers share the existing HttpClient retry policy. Background ForkCurl keeps its asynchronous contract.
  • HTTP retry exhaustion keeps LibCurl's existing batch removal behavior.

This layer depends on the Socket state repair. The API snapshot adds one internal trait symbol with no public callable methods.

💚 How did you test it?

Parameterized transport cases failed before the fixes and passed after them. They cover status handling, retry limits, Retry-After delays, stable bodies, gzip, and queue drain.

Malformed-header cases and an unlimited PHP default timeout case also failed before their repairs. The full PHP suite, configured style check, API check, and adapter tests passed.

A foreground ForkCurl smoke test passed with PHP curl functions disabled. Full compliance passes for Socket, LibCurl, and foreground ForkCurl, with 47/47 assertions per profile.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm change to generate a change intent file

Corepack could not load pnpm because certificate validation failed. A patch change intent follows the existing file format.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

PostHog Desktop with GPT-6 and GPT-6.1 Sol prepared the change. A separate GPT-6.1 Sol review found the unbounded header read; the repair adds size and time limits. Skills: pr-shepherd, writing-tests, writing-code-comments, writing-pr-descriptions, paul-pair, and stacking-prs. Human review is required.


Created with PostHog Desktop

Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
@pauldambra pauldambra self-assigned this Oct 16, 2026
@pauldambra
pauldambra added this pull request to stack #266 October 16, 2026 15:57

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed 655d8f4: 2 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

Comment thread lib/Consumer/ForkCurl.php Outdated
Comment thread lib/HttpRetryPolicy.php Outdated
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-10-19T17:33:39.173312+00:00
Duration: 117910ms

✅ All Tests Passed!

47/47 tests passed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 30ms
Format Validation.Event Has Uuid ✅ 524ms
Format Validation.Event Has Lib Properties ✅ 526ms
Format Validation.Distinct Id Is String ✅ 526ms
Format Validation.Token Is Present ✅ 527ms
Format Validation.Custom Properties Preserved ✅ 525ms
Format Validation.Event Has Timestamp ✅ 526ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 527ms
Retry Behavior.Retries On 503 ✅ 5835ms
Retry Behavior.Does Not Retry On 400 ✅ 2527ms
Retry Behavior.Does Not Retry On 401 ✅ 2529ms
Retry Behavior.Respects Retry After Header ✅ 8534ms
Retry Behavior.Implements Backoff ✅ 16250ms
Retry Behavior.Retries On 500 ✅ 5635ms
Retry Behavior.Retries On 502 ✅ 5635ms
Retry Behavior.Retries On 504 ✅ 5634ms
Retry Behavior.Max Retries Respected ✅ 16250ms
Deduplication.Generates Unique Uuids ✅ 534ms
Deduplication.Preserves Uuid On Retry ✅ 5634ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10832ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5147ms
Deduplication.No Duplicate Events In Batch ✅ 532ms
Deduplication.Different Events Have Different Uuids ✅ 528ms
Compression.Sends Gzip When Enabled ✅ 527ms
Batch Format.Uses Proper Batch Structure ✅ 526ms
Batch Format.Flush With No Events Sends Nothing ✅ 521ms
Batch Format.Multiple Events Batched Together ✅ 513ms
Error Handling.Does Not Retry On 403 ✅ 2529ms
Error Handling.Does Not Retry On 413 ✅ 2528ms
Error Handling.Retries On 408 ✅ 5632ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 527ms
Request Payload.Flags Request Uses V2 Query Param ✅ 523ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 524ms
Request Payload.Flags Request Omits Authorization Header ✅ 524ms
Request Payload.Token In Flags Body Matches Init ✅ 523ms
Request Payload.Groups Round Trip ✅ 524ms
Request Payload.Groups Default To Empty Object ✅ 523ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 524ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 523ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 523ms
Request Lifecycle.No Flags Request On Init Alone ✅ 518ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 529ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 525ms
Retry Behavior.Retries Flags On 502 ✅ 627ms
Retry Behavior.Retries Flags On 504 ✅ 627ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 525ms

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-10-19T17:33:49.619750+00:00
Duration: 118405ms

✅ All Tests Passed!

47/47 tests passed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 22ms
Format Validation.Event Has Uuid ✅ 515ms
Format Validation.Event Has Lib Properties ✅ 518ms
Format Validation.Distinct Id Is String ✅ 518ms
Format Validation.Token Is Present ✅ 518ms
Format Validation.Custom Properties Preserved ✅ 518ms
Format Validation.Event Has Timestamp ✅ 518ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 518ms
Retry Behavior.Retries On 503 ✅ 5826ms
Retry Behavior.Does Not Retry On 400 ✅ 2520ms
Retry Behavior.Does Not Retry On 401 ✅ 2519ms
Retry Behavior.Respects Retry After Header ✅ 8527ms
Retry Behavior.Implements Backoff ✅ 16239ms
Retry Behavior.Retries On 500 ✅ 5628ms
Retry Behavior.Retries On 502 ✅ 5625ms
Retry Behavior.Retries On 504 ✅ 5625ms
Retry Behavior.Max Retries Respected ✅ 17041ms
Deduplication.Generates Unique Uuids ✅ 222ms
Deduplication.Preserves Uuid On Retry ✅ 5623ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10834ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5628ms
Deduplication.No Duplicate Events In Batch ✅ 520ms
Deduplication.Different Events Have Different Uuids ✅ 517ms
Compression.Sends Gzip When Enabled ✅ 517ms
Batch Format.Uses Proper Batch Structure ✅ 518ms
Batch Format.Flush With No Events Sends Nothing ✅ 515ms
Batch Format.Multiple Events Batched Together ✅ 510ms
Error Handling.Does Not Retry On 403 ✅ 2519ms
Error Handling.Does Not Retry On 413 ✅ 2521ms
Error Handling.Retries On 408 ✅ 5625ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 517ms
Request Payload.Flags Request Uses V2 Query Param ✅ 517ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 516ms
Request Payload.Flags Request Omits Authorization Header ✅ 525ms
Request Payload.Token In Flags Body Matches Init ✅ 513ms
Request Payload.Groups Round Trip ✅ 516ms
Request Payload.Groups Default To Empty Object ✅ 516ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 515ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 517ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 515ms
Request Lifecycle.No Flags Request On Init Alone ✅ 513ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 507ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 520ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 517ms
Retry Behavior.Retries Flags On 502 ✅ 618ms
Retry Behavior.Retries Flags On 504 ✅ 617ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 518ms

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-10-19T17:33:35.970953+00:00
Duration: 118238ms

✅ All Tests Passed!

47/47 tests passed


Capture Tests

✅ 30/30 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 38ms
Format Validation.Event Has Uuid ✅ 529ms
Format Validation.Event Has Lib Properties ✅ 531ms
Format Validation.Distinct Id Is String ✅ 532ms
Format Validation.Token Is Present ✅ 532ms
Format Validation.Custom Properties Preserved ✅ 533ms
Format Validation.Event Has Timestamp ✅ 533ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 532ms
Retry Behavior.Retries On 503 ✅ 5855ms
Retry Behavior.Does Not Retry On 400 ✅ 2533ms
Retry Behavior.Does Not Retry On 401 ✅ 2533ms
Retry Behavior.Respects Retry After Header ✅ 8548ms
Retry Behavior.Implements Backoff ✅ 16279ms
Retry Behavior.Retries On 500 ✅ 5649ms
Retry Behavior.Retries On 502 ✅ 5649ms
Retry Behavior.Retries On 504 ✅ 5646ms
Retry Behavior.Max Retries Respected ✅ 16280ms
Deduplication.Generates Unique Uuids ✅ 539ms
Deduplication.Preserves Uuid On Retry ✅ 5649ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10857ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5157ms
Deduplication.No Duplicate Events In Batch ✅ 541ms
Deduplication.Different Events Have Different Uuids ✅ 534ms
Compression.Sends Gzip When Enabled ✅ 532ms
Batch Format.Uses Proper Batch Structure ✅ 531ms
Batch Format.Flush With No Events Sends Nothing ✅ 520ms
Batch Format.Multiple Events Batched Together ✅ 520ms
Error Handling.Does Not Retry On 403 ✅ 2534ms
Error Handling.Does Not Retry On 413 ✅ 2534ms
Error Handling.Retries On 408 ✅ 5647ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 522ms
Request Payload.Flags Request Uses V2 Query Param ✅ 522ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 523ms
Request Payload.Flags Request Omits Authorization Header ✅ 522ms
Request Payload.Token In Flags Body Matches Init ✅ 522ms
Request Payload.Groups Round Trip ✅ 523ms
Request Payload.Groups Default To Empty Object ✅ 523ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 523ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 523ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 522ms
Request Lifecycle.No Flags Request On Init Alone ✅ 518ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 516ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 526ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 523ms
Retry Behavior.Retries Flags On 502 ✅ 626ms
Retry Behavior.Retries Flags On 504 ✅ 625ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 534ms

@pauldambra
pauldambra marked this pull request as ready for review October 16, 2026 16:00
@pauldambra
pauldambra requested a review from a team as a code owner October 16, 2026 16:00
Comment thread lib/HttpRetryPolicy.php Outdated
@veria-ai

veria-ai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up question about retry-queue specification alignment.

Comment thread lib/Consumer/ForkCurl.php

$this->handleError($statusCode, implode("\n", $output));
if (!$this->isRetryableStatus($statusCode) || $backoff >= $this->maximum_backoff_duration) {
return self::FLUSH_BATCH_NON_RETRYABLE_FAILURE;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: Did you notice the difference from the retry-queue spec, and is it intentional? Returning FLUSH_BATCH_NON_RETRYABLE_FAILURE here removes the batch after retryable HTTP failures exhaust the retry budget, whereas the spec’s general transient-failure scenario keeps events queued for a later retry. I see that matching existing LibCurl behavior is deliberate; just checking whether this spec divergence was considered too. Reproduction: reproduced — the reviewed head’s ConsumerForkCurlTest::testHttpDeliveryRetryPolicy “bounded retries” case confirms that two HTTP 503 responses drain the batch and a subsequent flush sends no further request.

Cap each retry wait at the configured maximum backoff and keep
oversized Retry-After values inside the integer range, so a server
cannot block a PHP worker for hours or raise a TypeError on flush.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
@pauldambra

Copy link
Copy Markdown
Member Author

Note

🤖 Automated comment by QA Swarm — not written by a human

Multi-perspective review: router (cheap-first pass) + delegated lenses (qa-team, paul-reviewer, xp-reviewer, security-audit, engineering-systems-thinking as warranted)

Verdict: ✅ APPROVE (round 1 @ eb6d09f)

No new defects found. The Retry-After overflow and unbounded wait are fixed in f1a6651, with tests for Socket, ForkCurl and HttpClient.

Key findings

None this round. The open question about the retry-queue spec (batch dropped after the retry budget runs out) stays with the PR author.

Convergence

Both reviewers independently found the ForkCurl temp files and shell arguments safe, and the Socket read loop bounded by a deadline and a 64 KiB header limit.

Reviewer summaries

Reviewer Assessment
🧭 router (sonnet) MEDIUM danger, medium-high confidence. Traced temp-file cleanup, shell escaping and the Socket timeout logic; full suite green (776 tests). Delegated the shell and socket hunks to security-audit.
🔒 security-audit (opus) No exploitable issue. New shell arguments are escaped, temp files come from tempnam and are removed in finally, the status code cannot be faked by the body, and every loop ends.

Automated by QA Swarm — not a human review

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
maximum_backoff_duration acts as the retry budget, so a small value
cut short Retry-After waits the spec expects to be honoured. Cap the
wait at 10 seconds instead.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
Follow the retry-queue spec: retry-budget exhaustion is not a terminal
disposition, so Socket, ForkCurl and LibCurl now return a retryable
failure and the queue keeps the batch. Automatic flushes wait for the
maximum backoff duration before they try again, so a down server does
not block every capture call.

Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PostHog Review · Standard · reviewed b035c9d: 1 Must fix.

Comment thread lib/QueueConsumer.php
Comment on lines +184 to +185
if ($this->inRetryCooldown()) {
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 · Allow automatic recovery when the cooldown fills the queue

When captures fill max_queue_size during the retry cooldown, enqueue() rejects every subsequent event at its earlier capacity check. Even after the cooldown expires and the server recovers, automatic flushing never runs again, so a long-lived client rejects events until explicitly flushed. Allow an expired cooldown to trigger delivery before rejecting a full queue.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants