feat: ETag conditional requests for version checks#21
Conversation
Reduces check interval from 24h to 15min by leveraging HTTP ETag headers. When GitHub returns 304 Not Modified, the response doesn't count against the unauthenticated rate limit (60 req/hr), so most checks are effectively free.
Greptile SummaryThis PR adds ETag-based conditional request support to the three GitHub API version-check functions and reduces the polling interval from 24 hours to 15 minutes, leveraging the fact that 304 responses don't count against GitHub's rate limit. The schema and migration changes are clean and backward-compatible. Key findings:
Confidence Score: 3/5
Last reviewed commit: 108e148 |
| const { release, etag, notModified } = await fetchRelease( | ||
| `${GITHUB_API}/repos/${SERVER_REPO}/releases/latest`, | ||
| settings?.latestServerReleaseEtag, | ||
| ); |
There was a problem hiding this comment.
force flag does not bypass ETag — forced checks can still return cached data
When force = true, needsCheck is forced to true, but the stored ETag is still passed to fetchRelease. This means GitHub can still respond with 304 Not Modified, and the function returns the previously-cached version instead of freshly fetched data.
This directly contradicts the expected behaviour described in the PR's own test plan: "Force check — verify it fetches fresh data." A user hitting "Force Check" on the Settings page would silently receive stale data if nothing changed on GitHub's side.
The same problem exists in checkAgentVersion (lines 169–172) and checkDevAgentVersion (lines 246–249).
The fix is to pass null for the ETag when force is true:
| const { release, etag, notModified } = await fetchRelease( | |
| `${GITHUB_API}/repos/${SERVER_REPO}/releases/latest`, | |
| settings?.latestServerReleaseEtag, | |
| ); | |
| const { release, etag, notModified } = await fetchRelease( | |
| `${GITHUB_API}/repos/${SERVER_REPO}/releases/latest`, | |
| force ? null : settings?.latestServerReleaseEtag, | |
| ); |
Apply the same pattern in the other two functions.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/services/version-check.ts
Line: 99-102
Comment:
`force` flag does not bypass ETag — forced checks can still return cached data
When `force = true`, `needsCheck` is forced to `true`, but the stored ETag is still passed to `fetchRelease`. This means GitHub can still respond with `304 Not Modified`, and the function returns the previously-cached version instead of freshly fetched data.
This directly contradicts the expected behaviour described in the PR's own test plan: _"Force check — verify it fetches fresh data."_ A user hitting "Force Check" on the Settings page would silently receive stale data if nothing changed on GitHub's side.
The same problem exists in `checkAgentVersion` (lines 169–172) and `checkDevAgentVersion` (lines 246–249).
The fix is to pass `null` for the ETag when `force` is `true`:
```suggestion
const { release, etag, notModified } = await fetchRelease(
`${GITHUB_API}/repos/${SERVER_REPO}/releases/latest`,
force ? null : settings?.latestServerReleaseEtag,
);
```
Apply the same pattern in the other two functions.
How can I resolve this? If you propose a fix, please make it concise.| `${GITHUB_API}/repos/${SERVER_REPO}/releases/latest`, | ||
| settings?.latestServerReleaseEtag, | ||
| ); | ||
| checkedAt = new Date(); |
There was a problem hiding this comment.
Misleading checkedAt on API failure
checkedAt is unconditionally set to new Date() before the API response is evaluated. If the API returns an error (not 304, not 200), neither the notModified nor the release branch fires, so nothing is written to the DB. The next call will correctly retry (the DB timestamp was never bumped), but the function still returns checkedAt = new Date(), telling the UI "last checked: just now" when the check actually failed silently.
In the old code, checkedAt = new Date() was set only inside the success branches, so on failure the UI would show the last successful check time instead.
The same pattern exists on lines 173 and 250 in checkAgentVersion and checkDevAgentVersion.
Consider only setting checkedAt = new Date() inside the branches that actually persist to the DB, or tracking a separate checkFailed state.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/server/services/version-check.ts
Line: 103
Comment:
Misleading `checkedAt` on API failure
`checkedAt` is unconditionally set to `new Date()` before the API response is evaluated. If the API returns an error (not 304, not 200), neither the `notModified` nor the `release` branch fires, so nothing is written to the DB. The next call will correctly retry (the DB timestamp was never bumped), but the function still *returns* `checkedAt = new Date()`, telling the UI "last checked: just now" when the check actually failed silently.
In the old code, `checkedAt = new Date()` was set only inside the success branches, so on failure the UI would show the last successful check time instead.
The same pattern exists on lines 173 and 250 in `checkAgentVersion` and `checkDevAgentVersion`.
Consider only setting `checkedAt = new Date()` inside the branches that actually persist to the DB, or tracking a separate `checkFailed` state.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Test Plan
latestServerReleaseEtagshould be populated