docs/plans/p5a: 01 kind+fetch limits, 02 pdftext child + runner, 03 dcache PDF tables, 04 pdfpool, 05 pdfdoc render (map/pages/grep), 06 api, 07 digest citations, metrics, CLI, skill, unit, README. Given tests compile against a panic-only skeleton and fail there for the intended reasons; PDFs for tests are built by internal/pdftest (given), plus two qpdf-encrypted fixtures. Owner changes for the run: vendor go-pdfkit/extract v0.3.0 + reader v0.6.0 (pinned via tools.go until task 02); AGENTS.md, tools/run-plan.sh (enforces a Co-Authored-By trailer for the model), docs/implementer-log.md, `make gate`; gofmt (whitespace) on clean.go and guard; TestUnsupportedType uses a zip now that PDFs are supported; requestDeadline 90→210 s and writeTimeout 120→240 s for the PDF path's worst case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.8 KiB
4.8 KiB
P5a task 01: recognise PDFs; give them a larger body cap, a longer deadline, and revalidation
Branch: p5a (git switch -c p5a master if it does not exist, else git switch p5a; git status --short must be empty, otherwise stop)
Commit subject: clean, fetch: recognise PDFs; 100 MB / 120 s for PDFs; FetchIf revalidates
Goal
Today every response that is not HTML, JSON or text is 415 unsupported content type. After this
task clean.Kind names PDFs, and the fetcher lets a PDF be as large as 100 MB and take 120 s
while a web page keeps 5 MB and 20 s. FetchIf sends If-None-Match / If-Modified-Since so a
cached document can be revalidated with a 304. Nothing uses these yet (task 06 does).
Files
- Copy:
internal/clean/kind_test.go,internal/fetch/pdf_test.go - Create:
internal/fetch/pdf.go(the PDF rule andFetchIflive here; keepfetch.go's changes to what the new file needs) - Modify:
internal/clean/clean.go,internal/fetch/fetch.go,docs/implementer-log.md
Interfaces
package clean
// Kind classifies a fetched body: "html", "json", "text", "pdf" or "other".
func Kind(contentType string, body []byte) string
package fetch
const (
DefaultPDFMaxBody = 100 << 20
DefaultPDFTimeout = 120 * time.Second
)
// in Fetcher:
PDFMaxBody int64 // 0 = DefaultPDFMaxBody
PDFTimeout time.Duration // 0 = DefaultPDFTimeout
// FetchIf is Fetch with validators: a non-empty etag is sent as If-None-Match and a non-empty
// lastModified as If-Modified-Since. A 304 answer returns a Resource with Status 304, an empty
// Body and a nil error. With both empty it is exactly Fetch.
func (f *Fetcher) FetchIf(ctx context.Context, rawURL, etag, lastModified string) (*Resource, error)
Rules
Kind(the media type is lower-cased and its parameters dropped, asMediaKinddoes):application/pdforapplication/x-pdf→"pdf", whatever the body.text/html,application/xhtml+xml→"html";application/jsonor*+json→"json";text/plain,text/markdown,text/csv→"text"(as today).application/octet-streamorbinary/octet-stream→"pdf"if%PDF-occurs within the first 1024 bytes of the body, else"other".- empty Content-Type →
"pdf"if%PDF-occurs within the first 1024 bytes, else today'sSniff(html / json / text). - anything else →
"other".MediaKindalso returns"pdf"for the two PDF types; its other answers do not change.
- Fetch limits. A response is a PDF when its Content-Type is a PDF type, or when it is empty
or octet-stream and the body starts (within 1024 bytes) with
%PDF-. For a PDF the body cap isPDFMaxBodyand the deadline isPDFTimeout, both counted from the start of the request; for anything else they stayMaxBodyandTimeout. Over the cap is the existing413 too large (>N MB decompressed)with N from the cap that applied. - How to get the deadline right.
Fetchtoday makes one context withTimeout, and the sharedhttp.ClienthasTimeoutset too — both would cut a PDF off at 20 s. Make the client'sTimeoutat least the PDF deadline (or unset it and rely on contexts), give the request a context with the PDF deadline, and cancel it at the ordinary deadline unless the response has been recognised as a PDF by then (time.AfterFunc+ a flag you set once the headers, or the first bytes of an octet-stream body, say PDF). Read the body throughio.LimitReaderwith the cap that applies (+1 to detect overflow), as today. FetchIfgoes through the same guard, client, limits and error mapping asFetch; only the two request headers differ. Do not copyFetch: factor the shared part.
Facts
- The tests reach an
httptestserver on 127.0.0.1 through aguard.GuardwithAllowPrefixes: 127.0.0.0/8; the guard itself is not yours to change. internal/fetchhas no other tests;internal/apitests exerciseFetchthrough the API and must stay green.
Steps
- 1. Branch as above; copy the two given tests.
- 2. See them fail:
go test -count=1 ./internal/clean/ ./internal/fetch/→Kind,FetchIf,DefaultPDFMaxBodyundefined. - 3.
clean.go. 4.fetch/pdf.goand the smallest changes tofetch.go. - 5.
gofmt -w;go test -race -count=3 ./internal/clean/ ./internal/fetch/ ./internal/api/→ok. - 6.
make gate. 7. Rowp5a/01-kind-fetch; commit.
git add internal/clean internal/fetch docs/implementer-log.md
git commit -F .state/commit-msg.txt
Done when
- The given tests and every earlier test pass under
-race -count=3;make gateprintsgate: ok; given files byte-identical.
Stop and report if
- A given test needs a change to
internal/guard.