Skip to content

fix: crawlproof-affiliate cron posted to a NULL url and never ran - #305

Merged
ralyodio merged 1 commit into
masterfrom
fix/affiliate-cron-url
Sep 24, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/affiliate-cron-url

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

crawlproof-affiliate has never run once since it was created on 2026-09-13.

20260913120000_openaffiliate.sql scheduled it with current_setting('app.site_url', true) — the exact pattern 20260515090000_cron_config.sql had removed four months earlier. That migration's own header explains why it fails:

Managed Supabase only allows custom app.* GUCs via supabase_admin (superuser) — the postgres role we run migrations as cannot ALTER DATABASE/ROLE SET them. The migrations succeeded but the GUCs were never populated, so net.http_post() was called with url=NULL and pg_cron silently failed every hour.

So NULL || '/api/cron/affiliate' is NULL and pg_net rejects it with null value in column "url" ... violates not-null constraint.

It hides well. It shows up only in cron.job_run_details, nothing alerts on it, and the other nine jobs are green — 54/54 pg_net responses are 200 with real payloads. I only found it auditing the ten jobs after the move to dev2.

Not caused by the migration to self-hosting. The Supabase cloud project had been failing this same job hourly on the same error; every failure in its 41,744-run history is this one job. Self-hosting reproduced it faithfully.

Fixed by reading public.cron_config like the other nine — deliberately not by setting the GUCs, since a database-level setting is invisible to pg_dump and would be lost by the next migration, which is how this came back the second time. Adds a guard that fails the migration loudly if either config value is missing, rather than scheduling another job that dies hourly in silence.

Applied to dev2: resolves to https://crawlproof.com/api/cron/affiliate, route reachable and secret-gated (401 without the secret), 10/10 jobs active. Next scheduled run 23:23 UTC.

20260913120000_openaffiliate.sql scheduled the job with
current_setting('app.site_url', true), which is the exact pattern
20260515090000_cron_config.sql had removed four months earlier. Its header
even explains why: the app.* GUCs can only be set by supabase_admin,
migrations run as `postgres`, so they are never populated. NULL || '/api/...'
is NULL and pg_net rejects the row with "null value in column url of relation
http_request_queue violates not-null constraint".

The job has therefore never run once since 2026-09-13. It hides well: it is
visible only in cron.job_run_details, nothing alerts on it, and the other nine
jobs are green. Found while auditing the ten jobs after the move to dev2 — the
Supabase cloud project had been failing it hourly too, so self-hosting
reproduced the bug rather than causing it.

Fixed by reading public.cron_config like the other nine, NOT by setting the
GUCs: a database-level setting is invisible to pg_dump, so it would be lost by
the next migration and this would return a third time. Adds a guard that fails
the migration loudly if either config value is missing, rather than scheduling
another job that dies hourly in silence.

Applied to dev2; the job now resolves to https://crawlproof.com/api/cron/affiliate
and the route is reachable and secret-gated (401 without it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

48 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM js-dynamic-code-execution lib/crawl-limits.ts:67
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:14
MEDIUM js-dynamic-code-execution scripts/test-crawl-limits.mjs:24
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:20
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:24
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:25
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:26
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:31
LOW js-dynamic-code-execution tests/contract/ads-click-cooldown-redis.test.ts:35
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 540071c into master Sep 24, 2026
10 checks passed
@ralyodio
ralyodio deleted the fix/affiliate-cron-url branch September 24, 2026 22:32
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.

1 participant