Repository navigation
Fix the issues from the 20 October code review - #3
Merged
Merged
Conversation
Real hostname fetches failed: asking for a streamed response made Guzzle 8 route the request to its PHP-stream handler, which rejects the CURLOPT_RESOLVE pin (Guzzle 7 would have ignored it silently). The tests replaced the transport with Http::fake() and could not see it. SafeHttpClient now always uses Guzzle's cURL handler and enforces the size cap with a progress callback that aborts the transfer. A new RealTransportTest goes through the real transport: a made-up hostname is pointed at a local PHP server, which is only reachable through the pin. Oversized responses throw ResponseTooLargeException. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
- The web installer asks for a one-time setup code that can only be
read on the server, so a stranger who finds a fresh deployment can't
claim it.
- Installations take a non-blocking lock and re-check ownership inside
it; createOwner() refuses once any account exists, whatever its email.
- isInstalled() fails closed: an installed marker keeps /install shut,
and a database error other than a missing SQLite file answers 503
instead of reopening the installer. .env is only written once the
account exists.
- The seeder never adds a user when one exists and no longer falls back
to a published password.
Installer state lives in config('docket.install.state_path'); tests get
their own directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
- Queue retry_after defaults to 360s, above the fetch job's 300s timeout, and the job takes a per-item lock so a retry can't run alongside a fetch still in progress. - Image downloads share one per-article budget for inline and lead images: attempts (not just successes), total bytes and time. The lead image obeys the same on/off switch and reuses an inline copy. - A failed image write keeps the remote src instead of pointing the article at a missing file. - A failed refresh no longer hides the copy already saved: the reader shows it with a note, and a refresh that finds no article only fills in missing details rather than overwriting them. - Lists, search, bulk actions, the CSV export and refetch select every column but the article bodies, bulk actions run in batches, and exports and refetch page by ID, so a shrinking result set can't make refetch skip items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Part of the same review: the list, bulk and refetch code that uses the new withoutBodies() scope and ID paging, and an export option that prefixes any cell a spreadsheet would run as a formula with an apostrophe. The plain CSV stays lossless for re-importing. HardeningTest covers this and the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
A tag name such as "work/personal" produced a URL that did not match its route. Tags are now addressed by ID; the slug remains the case-insensitive identity used to match names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
The backup's database is an SQL dump to import with sqlite3, not a database file to move into place. The steps now import, check and swap it in with the site and worker stopped, keeping the old files. Also documents the installer's setup code and the queue timeout relationship. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
SafeHttpClient reads the fetch timeout through the Settings service, which queries the settings table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DPvSBmPyHN2QVmXsQQVKe
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Fixes all 14 findings in the external review (
reviews/2026-10-20-code-review.md, on thereviews/code-review-2026-10-20branch). I checked each against the code and none turned out to be a false positive.Fixes
stream => true, Guzzle 8 picked its PHP-stream handler, which rejects theCURLOPT_RESOLVEpin. Guzzle 7 would have ignored the pin silently.SafeHttpClientalways uses Guzzle's cURL handler. The size cap is enforced by a progress callback that aborts the transfer. NewRealTransportTestgoes through the real transport: a made-up hostname is pointed at a local PHP server, which can only be reached through the pin. It covers a plain fetch, a redirect, oversized bodies (declared and undeclared) and a blocked address.storage/app/install/setup-code). A non-blocking install lock with an ownership re-check inside it.createOwner()refuses once any account exists. The console installer shares the lock and the one-owner rule./installshut. Unexpected database errors return a 503..envis written only after the account exists.db:seedcould add an account with a published passwordDOCKET_USER_PASSWORD; there's no fallback password any more.retry_after(90s) was shorter than the job timeout (300s)retry_afteris now 360s, and aWithoutOverlappingper-item lock on the job stops two fetches of the same item running at once.withoutBodies()scope (search keeps it too). Bulk actions run in batches of 100. Exports and refetch page through items by ID.ImageDownloadsgives each article one budget shared by inline and lead images. It counts attempts, total bytes and time. The lead image follows the same on/off switch and reuses an inline copy if there is one.srcis kept and a warning logged.hasArticle()depends only on stored content. The reader shows the saved copy with a "Could not refresh" note. A refresh that finds no article only fills in details that were missing.lazyById().work/personalled to 404s/export/csv?for=spreadsheet). The plain CSV stays lossless for re-importing.sqlite3, checks it, and swaps it in with the site and worker stopped. I ran the steps with the realsqlite3CLI: the FTS5 table and its triggers survive.Tests
RealTransportTestHardeningTest: one or more tests each for #5–#10, #12 and #14DatabaseSeederTestWebInstallerTest: the setup code, the marker, a concurrent install refused with 409, a database error answered with 503InstallCommandTest:--forcewon't add a second ownerOrganisingTest: tag names containing URL charactersGenerated by Claude Code