Skip to content

[#630] Fetch LeetCode daily from GraphQL with retry - #631

Merged
DVidal1205 merged 2 commits into
mainfrom
cron/leetcode-graphql-daily
Oct 29, 2026
Merged

DVidal1205 merged 2 commits into
mainfrom
cron/leetcode-graphql-daily

Conversation

@Kevinli7673

@Kevinli7673 Kevinli7673 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

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 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.

What

Closes: #630

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.
Screenshot 2026-10-29 022126

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.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kevinli7673 Kevinli7673 self-assigned this Oct 29, 2026
@Kevinli7673 Kevinli7673 added Bug Something isn't working Minor Small change - 1 reviewer required CRON Change modifies code in CRON app labels Oct 29, 2026
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

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: Passed 7 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning 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 Passed checks: 7 passed
Check name Status Explanation
Title check Passed The title starts with [#630], concisely describes the GraphQL fetch and retry change, and is 51 characters long.
Description check Passed The description clearly explains the LeetCode GraphQL migration, retry behavior, scope, limitations, and test results.
Linked Issues check Passed 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 Passed 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 Passed 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 Passed 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 Passed 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

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)


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between cd4fb96 and cb991fe.

Files selected for processing: 6
  • .forge/features/leetcode-daily-reliability/spec.md
  • .forge/features/leetcode-daily-reliability/srd.md
  • .forge/features/leetcode-daily-reliability/status.md
  • .forge/features/leetcode-daily-reliability/test-cases.md
  • apps/cron/src/crons/leetcode.ts
  • apps/cron/src/tests/leetcode.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread apps/cron/src/crons/leetcode.ts Resolved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

Minor · Log the GraphQL error detail for HTTP 400. · leetcode.ts:218-223

apps/cron/src/crons/leetcode.ts:218-223
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Log the GraphQL error detail for HTTP 400.

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)

Source: Path instructions

Nitpick comments: 1
apps/cron/src/tests/leetcode.test.ts: 1

195-206: Trivial · Functional Correctness · Quick win

Exercise the abort-driven timeout path.

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.


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between cb991fe and bb10549.

Files selected for processing: 5
  • .forge/features/leetcode-daily-reliability/srd.md
  • .forge/features/leetcode-daily-reliability/status.md
  • .forge/features/leetcode-daily-reliability/test-cases.md
  • apps/cron/src/crons/leetcode.ts
  • apps/cron/src/tests/leetcode.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@DVidal1205 DVidal1205 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.

yo this guys cooking

@DVidal1205
DVidal1205 added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 0fb0dc3 Oct 29, 2026
14 checks passed
@DVidal1205
DVidal1205 deleted the cron/leetcode-graphql-daily branch October 29, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working CRON Change modifies code in CRON app Minor Small change - 1 reviewer required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LeetCode daily cron skips days when alfa-leetcode-api rate limits

2 participants