Repository navigation
feat(fonts): record resolver stage diagnostics on fail-closed font errors - #5189
Conversation
…esolved error Attach a stage record to the per-compile fail-closed Unresolved FontFetchError so callers can log why a family did not resolve. The record is an ordered list of families, each with required/resolved flags and its fetch attempts (path, authored/default URL, text param, CSS cache fresh/hit, real CSS HTTP status, block and regex-match counts, asset totals and non-ok HTTP statuses). It holds enums, counts and HTTP statuses only: no family names, URLs, CSS or page text. The error class, name, code, message, familyName and unresolvedFamilies are unchanged, and shared rejected errors are never mutated. - Jerrai
Fallow audit reportFound 5 findings. Dead code (3)
Duplication (2)
Generated by fallow. |
Edit accuracy: accurate 2059 (base branch 2059), smooth 1526 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
…0072026 Conflict in packages/core/src/fonts/deterministicFonts.ts: kept #5186's authored-zero-faces default fallback and its stylesheet result/status shapes unchanged; threaded the diagnostics attempt through every stylesheet fetch (authored and default fallback each get their own). - Jerrai
…e cssStatus comment Add generic tests (invented family names) for an authored stylesheet that is rejected or yields zero faces and falls back to the default request, an authored success with no fallback attempt, an unchanged transient UNAVAILABLE, and a failed case that records both attempts with the error contract unchanged. cssStatus doc now reads: no HTTP response was recorded for that attempt. - Jerrai
…Unresolved error's url and cause A second compile of the same failing authored-plus-default page must label both attempts as cache hits while the first compile's are fresh. The Unresolved error keeps url "" and no cause, as at main. - Jerrai
An optional family can join an in-flight CSS lookup that then rejects as UNAVAILABLE. The rejection is swallowed, but the attempt kept its default cssCache "fresh" because the label was written only after the await. The label is now recorded on the caller's own attempt before the await. The shared result and error objects are not touched; retry, fallback and selection are unchanged. - Jerrai
The second compile now passes its own AbortSignal; a lookup that awaits a shared promise registers a listener on it, so the test waits (bounded) until the joiner has reached the optional family's lookup and asserts the optional URL still has exactly one request before releasing the deferred 503. The first compile's optional attempt must be fresh and the joiner's a hit, in that order. - Jerrai
jrusso1020
left a comment
There was a problem hiding this comment.
Review at 82e204e5. Approving. The diagnostics are useful, contain only enums, counts and HTTP statuses, and don't change resolver behaviour.
Usefulness.
- Each failed lookup records which URL it tried:
urlSourceisauthoredordefault(deterministicFonts.ts:1563-1582). - An authored link that returns 400 and then a 400 from the weight-list fallback reads as
[{urlSource:"authored",cssStatus:400},{urlSource:"default",cssStatus:400}]underrequired:true, resolved:false. - An authored link with zero faces, followed by a fallback whose asset 404s, reads as
blocksTotal:1, regexMatches:0on the first attempt andnonOk:{"404":1}on the fallback. - So the record says which stage failed, without naming the font.
Privacy. FontDiagnostics contains only enum literals, booleans, counts, and Response.status values (fontDiagnostics.ts:1-33). The nonOk keys are String(status). I fed in a hostile family name (containing a URL, text=…, quotes and @font-face), an authored link with a text= parameter, and a family name planted as a secret. None of them appeared in the serialized diagnostics.
Behaviour unchanged. I swapped in main's deterministicFonts.ts and ran the same six scenarios on both versions:
- authored 400 → default 400;
- authored 400 → default OK;
- zero faces → woff2 404;
- CSS 503;
- woff2 503;
- open mode.
Fetch call sequences, error names, codes and messages were identical in all six, so the PR adds no fetches. The only new work is bookkeeping plus one regex count over CSS text the resolver already has.
Tests.
deterministicFonts-diagnostics.test.tspasses 30/30.- Mutants, all caught (4/4):
| Mutant | Tests failed |
|---|---|
| cache label set after the await | 1 (the join test) |
fallback labelled authored |
4 |
nonOk not counted |
2 |
cssStatus dropped |
16 |
Non-blocking.
cssCache: "hit"covers two cases: a result that was already cached, and a joined request that was still in flight. Non-retryable statuses like 400 stay cached for the whole process, sohit+ 400 reads as "a sticky 400 from an earlier lookup", which isn't always what happened. Consider calling itshared, or documenting it atfontDiagnostics.ts:14.- No structured record is written when the fallback succeeds. Only the existing warn line covers that case. Lookups made through the declared-alias second pass are filed as
direct_googleattempts under the same family, so you can't tell them apart. cssStatus: nullhas several meanings: a swallowed error in open mode, a joined lookup that rejected, or a swallowed optional unavailable. A smalloutcomeenum would separate them.snapshot()returns the live array (fontDiagnostics.ts:67-69). Returning a copy would be safer.- The Fallow "unused class member" findings are false positives.
beginFamily,endFamilyandstartAttemptare called through the interface-typedoptions.diagatdeterministicFonts.ts:1029,:1047and:1567.
CI. All required checks are green at this head. In the second same-head run, Render on windows-latest was still in progress when I checked; the first run's passed.
— Rames
What
Attach optional structured diagnostics to the per-compile unresolved-font
FontFetchError(FONT_FETCH_FAILED), so callers can identify the resolver stage that failed without parsing the error message. Existing error names, codes, messages,familyNameandunresolvedFamiliesremain unchanged.This change adds observability only; it does not introduce a new font-selection, fallback or retry policy.
Why
A fail-closed font error identifies the required families that remain unresolved, but not whether the resolver encountered an HTTP rejection, a cached CSS response, no accepted font-face blocks, or a font-asset download failure.
Related work
The branch incorporates the authored-link fallback from #5186, preserving its control flow, warning and result shape. Both authored and default requests are recorded separately. No linked issue.
How
packages/core/src/fonts/fontDiagnostics.ts.deterministicFonts.tsand attach the snapshot only to a newly constructed unresolved-font error. Shared, cached and retry-exhausted errors are not mutated.cssStatus: nullmeans no HTTP response was recorded for that attempt.unresolvedFamilies. Diagnostic records use resolver order, while unresolved names use sorted order; individual records cannot be associated with names when multiple families fail.FONT_FETCH_UNAVAILABLEand shared/cached errors without attached diagnostics. A swallowed optional-family failure can still appear in a later per-compile unresolved-font snapshot.Test plan
packages/core/src/fonts/deterministicFonts-diagnostics.test.ts: 30/30 tests passed locally at82e204e59, including authored/default fallback, unchanged error fields, repeated cache hits and a synchronized in-flight rejection case.Validation at the published commit
At
82e204e59, all 11 required checks passed, including Build, Typecheck, runtime contract, Windows tests/render, regression and the edit-accuracy gate. The runtime test job actually executed the CLI/core/engine tests; the aggregate Test check is a gate, not a separate test run. Local typecheck was not run.The main CI run 37711021045 completed with 41 successful jobs, two failed jobs and eight skipped jobs. Skipped jobs were not exercised. The two failures are non-required:
The merge conflict is resolved and the combined revision received fresh review and tests. This PR is ready for review. The results above describe the completed push-triggered checks; subsequent automatic checks must be assessed separately. This validation is not a live failure reproduction or a claim that a production incident is repaired.
— Jerrai