You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[#630] Fetch LeetCode daily from GraphQL with retry - #631
The daily LeetCode post didn't go out on 10/28. The noon cron run logged Error: Leetcode API -> 429. We were getting the problem from alfa-leetcode-api.onrender.com, a free public wrapper that everyone shares and that rate
limits by IP. The job didn't retry, so one 429 skipped the whole day.
All changes are in apps/cron. There are no env var, dependency, schema, tk, or Discord
message changes.
leetcode.ts now queries LeetCode's GraphQL endpoint (https://leetcode.com/graphql, activeDailyCodingChallengeQuestion) directly. This is the same endpoint alfa was calling
for us. The response is mapped into the same object as before, so the embed, thread, and
messages don't change. The relative problem link is turned into a full URL. The request
sends a User-Agent that identifies the cron, to make Cloudflare bot checks less likely to
flag it.
The fetch is now retried up to 3 times, waiting 30s and then 2min, on network errors,
timeouts, 429s, and 5xx responses. Each attempt gives up after 15s, so a stalled connection
can't hold up the retry. Other 4xx responses, a body that isn't JSON (like a Cloudflare page
served with a 200), and any GraphQL errors fail right away with the reason in the log,
because retrying won't fix them. Only the fetch is retried. The webhook posts and
thread creation aren't, so a run can't post twice. In the worst case, a temporary failure
makes the post about 2.5 minutes late.
The retry lives in leetcode.ts because this is the only job that needs it. Feature notes
are in .forge/features/leetcode-daily-reliability/.
CodeRabbit review
CodeRabbit's first review flagged three things, all fixed in bb10549:
Fetch attempts had no timeout, so a stalled connection would wait for Node's ~5 minute
default before the retry kicked in. Each attempt now uses AbortSignal.timeout(15_000),
and a timeout is retried.
A 200 with an HTML body made res.json() throw, which was retried like a network error and
logged as Unexpected token '<'. It now fails once with Leetcode API -> invalid JSON (content-type: text/html).
GraphQL errors were only checked when the daily question was missing. A partial result
(data present, some fields null) could reach the embed and crash on null. Any errors
now fail the run before anything is posted.
Limitations
LeetCode's GraphQL endpoint isn't a documented public API. If they change the query,
the cron logs a clear Leetcode API -> 400 error instead of failing silently.
I couldn't test from the production Worker VM, so I haven't confirmed that Cloudflare lets
our Azure IP through. Requests from a local machine work. The first noon run after deploy
is the check. If it logs a 403 or HTML, revert this PR.
The missed 10/28 post isn't reposted.
Test Plan
Added apps/cron/src/tests/leetcode.test.ts, with Discord and the fetch mocked:
a normal run posts the embed (full link, date, spoilered topics), creates the thread,
and sends the spoiler reminder
a 429 or network error followed by success posts exactly once
three 503s stop after 3 attempts and post nothing
a timeout followed by success posts
a GraphQL 400, a 200 with no daily question, a 200 with GraphQL errors, and a 200
with an HTML body all fail without retrying or posting
Ran the old code (main, alfa) and the new code against the live APIs with Discord
stubbed. All 22 output fields matched, apart from the random daily message.
Ran the new job end to end against the dev server's testing channel: a real fetch,
webhook post, bot-created thread, and reminder. The post looked the same as the current
daily. After the CodeRabbit fixes, a live run against LeetCode still builds the embed
correctly.
pnpm --filter=@forge/cron test (62/62), pnpm lint, and pnpm typecheck pass.
Prettier passes on the changed files. Locally, pnpm format only fails because of the
Windows CRLF checkout.
Checklist
Database: No schema changes, OR I ran pnpm db:generate and committed the generated files in packages/db/drizzle/
Environment Variables: No environment variables changed, OR I have contacted the Development Lead to modify them on Coolify BEFORE merging.
The cron now retrieves the daily challenge from LeetCode GraphQL and maps its data into the existing Discord post. It retries eligible fetch failures after 30 and 120 seconds. Webhook delivery and thread creation remain outside the retry flow. Tests cover successful posts, retryable failures, and failures that stop without posting.
Priority: Normal
Severity of issue fixed: Medium
Merge Risk: Low · up to bb105
Failed daily posts may be harder to diagnose when LeetCode returns HTTP 400. This is a bounded operational risk.
Pre-merge checks: 7 · 1
Failed checks: 1 warning
Check name
Status
Explanation
Resolution
Docstring Coverage
Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks: 7 passed
Check name
Status
Explanation
Title check
The title starts with [#630], concisely describes the GraphQL fetch and retry change, and is 51 characters long.
Description check
The description clearly explains the LeetCode GraphQL migration, retry behavior, scope, limitations, and test results.
Linked Issues check
Issue #630 requires direct GraphQL retrieval, retries for network errors, 429, and 5xx responses, and no webhook retry. The reviewed changes implement these rules in apps/cron/src/crons/leetcode.ts.…
Out of Scope Changes check
The changes are limited to the LeetCode cron, its tests, and feature documentation for the reliability change. The documentation records the same retry behavior and scope exclusions. No unrelated sche…
No Hardcoded Secrets
No hardcoded API keys, passwords, tokens, or secrets appear in the six changed files. The new literals are public LeetCode URLs, a User-Agent, GraphQL data, test fixtures, and a non-secret webhook pla…
Validated Env Access
No changed file adds direct process.env access. apps/cron/src/crons/leetcode.ts uses the validated env import, and the new test mocks ../env.
No Typescript Escape Hatches
No TypeScript escape hatches were added. The changed TypeScript files contain no any type, @ts-ignore, @ts-expect-error, or non-null assertion (!.). The any matches are ordinary English comm…
Full details: Linked Issues check
Explanation
Issue #630 requires direct GraphQL retrieval, retries for network errors, 429, and 5xx responses, and no webhook retry. The reviewed changes implement these rules in apps/cron/src/crons/leetcode.ts. The tests cover success, retry, timeout, persistent failure, GraphQL errors, missing data, and invalid JSON. The existing embed and posting flow remain unchanged.
Full details: Out of Scope Changes check
Explanation
The changes are limited to the LeetCode cron, its tests, and feature documentation for the reliability change. The documentation records the same retry behavior and scope exclusions. No unrelated schedule, webhook, message, schema, dependency, or environment changes are reported.
Full details: No Hardcoded Secrets
Explanation
No hardcoded API keys, passwords, tokens, or secrets appear in the six changed files. The new literals are public LeetCode URLs, a User-Agent, GraphQL data, test fixtures, and a non-secret webhook placeholder; the production webhook remains read from env.DISCORD_WEBHOOK_LEETCODE.
Full details: No Typescript Escape Hatches
Explanation
No TypeScript escape hatches were added. The changed TypeScript files contain no any type, @ts-ignore, @ts-expect-error, or non-null assertion (!.). The any matches are ordinary English comments only.
Full details: Docstring Coverage
Explanation
Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 2 files. (3 skipped: 3 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
Finishing Touches: 💡 1
📝 Generate docstrings: 💡
Commit to this branch
Create a new PR
🧪 Generate unit tests (beta)
Commit to this branch
Create a new PR
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Warning
Some tools did not complete. Review the errors below.
ESLint
If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
apps/cron/src/crons/leetcode.ts
ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.
apps/cron/src/tests/leetcode.test.ts
ESLint skipped: the matched ESLint configuration already failed (missing-dependency).
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
Other comments: 2
apps/cron/src/crons/leetcode.ts: 2
188-190: Minor · Functional Correctness · Quick win
Do not retry response-parsing errors as network failures.
If a successful HTTP response contains HTML, res.json() rejects. This condition retries twice, waits 150 seconds, and ultimately logs a parsing error instead of identifying the invalid response. Retry fetch failures only; convert invalid response bodies into a non-retryable LeetcodeFetchError. (fetch.spec.whatwg.org)
223-225: Minor · Data Integrity & Integration · Quick win
Reject GraphQL errors before mapping the daily question.
A GraphQL response can contain both data and errors. When the daily question exists, this code ignores errors and continues toward posting, contrary to the specified fail-fast behavior. Check body.errors before mapping the question and throw a non-retryable LeetcodeFetchError. (graphql.org)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
When LeetCode returns HTTP 400 with a GraphQL errors body, this branch discards the body. CronBuilder logs only Leetcode API -> 400, not the query error. Read a bounded error body and include its message in LeetcodeFetchError. Keep the response non-retryable. As per path instructions, “Proper logging for debugging” applies to apps/cron/**. (raw.githubusercontent.com)
runDaily() rethrows executor errors. The current test injects an immediate fetch rejection, so it does not verify that AbortSignal.timeout aborts the request. Make the first mocked request remain pending until its signal aborts, control that signal with a test-owned AbortController, advance the retry delay, and assert the executor result.
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The daily LeetCode post didn't go out on 10/28. The noon cron run logged
Error: Leetcode API -> 429. We were getting the problem fromalfa-leetcode-api.onrender.com, a free public wrapper that everyone shares and that ratelimits by IP. The job didn't retry, so one 429 skipped the whole day.
What
Closes: #630
All changes are in
apps/cron. There are no env var, dependency, schema, tk, or Discordmessage changes.
leetcode.tsnow queries LeetCode's GraphQL endpoint (https://leetcode.com/graphql,activeDailyCodingChallengeQuestion) directly. This is the same endpoint alfa was callingfor us. The response is mapped into the same object as before, so the embed, thread, and
messages don't change. The relative problem link is turned into a full URL. The request
sends a
User-Agentthat identifies the cron, to make Cloudflare bot checks less likely toflag it.
The fetch is now retried up to 3 times, waiting 30s and then 2min, on network errors,
timeouts, 429s, and 5xx responses. Each attempt gives up after 15s, so a stalled connection
can't hold up the retry. Other 4xx responses, a body that isn't JSON (like a Cloudflare page
served with a 200), and any GraphQL
errorsfail right away with the reason in the log,because retrying won't fix them. Only the fetch is retried. The webhook posts and
thread creation aren't, so a run can't post twice. In the worst case, a temporary failure
makes the post about 2.5 minutes late.
The retry lives in
leetcode.tsbecause this is the only job that needs it. Feature notesare in
.forge/features/leetcode-daily-reliability/.CodeRabbit review
CodeRabbit's first review flagged three things, all fixed in bb10549:
default before the retry kicked in. Each attempt now uses
AbortSignal.timeout(15_000),and a timeout is retried.
res.json()throw, which was retried like a network error andlogged as
Unexpected token '<'. It now fails once withLeetcode API -> invalid JSON (content-type: text/html).errorswere only checked when the daily question was missing. A partial result(data present, some fields null) could reach the embed and crash on
null. Anyerrorsnow fail the run before anything is posted.
Limitations
the cron logs a clear
Leetcode API -> 400error instead of failing silently.our Azure IP through. Requests from a local machine work. The first noon run after deploy
is the check. If it logs a 403 or HTML, revert this PR.
Test Plan
apps/cron/src/tests/leetcode.test.ts, with Discord and the fetch mocked:and sends the spoiler reminder
errors, and a 200with an HTML body all fail without retrying or posting
main, alfa) and the new code against the live APIs with Discordstubbed. All 22 output fields matched, apart from the random daily message.
webhook post, bot-created thread, and reminder. The post looked the same as the current
daily. After the CodeRabbit fixes, a live run against LeetCode still builds the embed
correctly.
pnpm --filter=@forge/cron test(62/62),pnpm lint, andpnpm typecheckpass.Prettier passes on the changed files. Locally,
pnpm formatonly fails because of theWindows CRLF checkout.
Checklist
pnpm db:generateand committed the generated files inpackages/db/drizzle/