docs(resources): fix four static-verb examples that fail silently on Harper 5 - #658
Conversation
Four v5 reference examples taught patterns that fail silently on Harper 5.
Overriding a static verb replaces the transactional() wrapper, so the record
or body arrives as an unresolved promise and every field reads undefined
instead of raising (harper resources/DESIGN.md -> Conventions).
- database/schema.md blob error handling: `super.get(target)` was not awaited,
so `record.data` was undefined and `blob.on('error', ...)` threw on every
read. The equivalent example in database/api.md already awaits it.
- database/api.md base64-in-JSON: the `if (record.data)` branch never ran, so
`super.post` stored the raw base64 string. Await the body once and forward it.
- resources/overview.md extending a table: `this.create(...)` was neither
awaited nor returned, so POST responded before the commit and a rejected
create became an unhandled rejection.
- resources/resource-api.md sign-in/sign-out and getCurrentUser: instance verbs
written with the static argument order. An unflagged instance `post` is
dispatched as `post(data, query)` (harper resources/Resource.ts), so
`data.username` was always undefined. Converted to the static form the rest
of the page prescribes, taking the context as a parameter — `getContext()`
and `getCurrentUser()` are instance methods with no static counterpart, so
the context arrives as `(target, context)` / `(target, data, context)`,
matching core's own login.ts and security/jwt-authentication.md.
Refs HarperFast/skills#79
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019drMJUhfEQfeNVDcU22oNe
Independent pre-push review found a second defect in the same block.
`request.session` carries only `update` (harper security/auth.ts:279); there
is no runtime `delete`, and core's own logout ends a session with
`session.update({ user: null })` (security/auth.ts:444). The guard was also
dead: `request.session` is set to `{}` rather than left undefined when
sessions are enabled and no cookie is present (security/auth.ts:126), so
`!context.session` never fired and an unauthenticated POST reached the
nonexistent `delete` for a 500 instead of the documented 401.
Also narrows the static-context note: a direct server-side call supplies a
context argument only if the caller passes one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019drMJUhfEQfeNVDcU22oNe
There was a problem hiding this comment.
Code Review
This pull request updates documentation and code examples across several markdown files to correctly handle asynchronous operations, static resource methods, and request contexts. Specifically, it clarifies how to await request bodies, handle static verbs, and manage user sessions. The review feedback highlights several opportunities to make the example code snippets more robust by using optional chaining and fallback values to prevent potential runtime TypeErrors when parameters like body, record, or context are missing or undefined.
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-658 This preview will update automatically when you push new commits. |
|
Reviewed 8db0609 and found no blocking issues. No confirmed blocking findings remain on the changed lines. The fixes correctly await asynchronous values and use the documented static resource context and session APIs. — |
… examples
Review feedback on the five examples this PR already touches. Each guard covers
a state the documented dispatch path can actually reach — `server/REST.ts:215`
assigns `request.data` only when the request carries `content-length` or
`transfer-encoding`, so a body-less POST really does await to `undefined`.
- api.md answers a missing body with a 400 rather than optional-chaining past
it. Forwarding a nullish body to `super.post(target, body)` would land in
`transactional()`'s single-argument branch (`resources/Resource.ts:589-603`)
and store the `RequestTarget` as the record — the same silent failure this PR
exists to remove.
- schema.md guards the missing record and returns it early. Optional chaining
alone would answer a missing record with a 200 and an empty body; a nullish
GET return is what REST turns into a 404 (`server/REST.ts:283`). The sibling
"Serving Binary from a Resource" example in api.md already reads this way.
- `(await data) ?? {}` (resource-api.md) is verbatim what core's own static
sign-in does (`resources/login.ts`), and without it an empty POST to the
sign-in endpoint is a destructuring TypeError instead of a 403.
- `context?.` in the `getCurrentUser` and `SignOut` examples — a static verb
called from server-side code gets a context only if the caller passes one,
as the `getContext()` entry above them already explains. Both guards were
already returning 401 for "no authenticated user", which is the honest
answer when there is no context either.
Co-Authored-By: Claude Opus <noreply@anthropic.com>
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-658 This preview will update automatically when you push new commits. |
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-658 This preview will update automatically when you push new commits. |
🧹 Preview CleanupThe preview deployment for this PR has been removed. |
Four v5 reference examples taught patterns that fail silently on Harper 5 — no throw, no type error, just wrong behavior. Each is also a
mode: generatesource for aharper-best-practicesrule, so the broken code was being shipped to agents.The shared root cause is documented in harper's own
resources/DESIGN.md-> Conventions: overriding a static verb replaces thetransactional()wrapper, including thewhen(data, ...)that resolves the body. A promise has no own enumerable properties, so every field readsundefinedrather than raising.reference/database/schema.md(Blob Usage)super.get(target)not awaitedrecord.dataisundefined, soblob.on('error', ...)throws aTypeErroron every blob readreference/database/api.md(Accepting Binary in JSON Requests)if (record.data)branch never runs andsuper.poststores the raw base64 stringreference/resources/overview.md(Extending a Table)this.create(...)neither awaited nor returned--unhandled-rejections=throwkills the worker threadreference/resources/resource-api.md(Resource Static Methods)postis dispatched aspost(data, query), sodata.username/data.passwordare alwaysundefinedandcontext.login()fails every timeWhat changed
Defects 1-3 are one-line corrections. Defect 4 converts
SignIn/SignOut/getCurrentUserto the static form the rest of that page prescribes ("For new code, prefer static methods and omit the flag"), matching the already-correctstatic async post(_target, data)examples inreference/security/jwt-authentication.md.The context now arrives as a parameter, not from
this.getContext().getContext()andgetCurrentUser()are declared onResource.prototypeonly — there is no static counterpart — sothis.getContext()inside a static verb isMyClass.getContext, i.e.undefined.server/REST.tsdispatchesresource.get(target, request)andresource.post(target, request.data, request)against the class, so a static verb reads the context from its trailing argument. This is exactly how core's ownresources/login.tsimplements the pre-authentication login endpoint. ThegetContext()andgetCurrentUser()entries, both listed under Resource Static Methods, now say so.A second defect the pre-push review found
The sign-out example called
context.session.delete(context.session.id), and the prose recommended it. That method does not exist at runtime:security/auth.ts:279attaches onlyupdatetorequest.session, and core's own logout path ends a session withsession.update({ user: null })(security/auth.ts:444). The guard was dead too —security/auth.ts:126setsrequest.sessionto{}rather than leaving itundefinedwhen sessions are enabled and no cookie is present, soif (!context.session)never fired and an unauthenticated POST reached the nonexistentdeletefor a 500 instead of the documented 401. Both the example and the prose now usecontext?.session?.userandcontext.session.update({ user: null }).Verification
npm ci && npm run build— clean (406 documents processed, no broken links), re-run after the guard commit. The two new anchors were checked directly in the build output:#getcontext-context-1andhttp/api#propertiesboth resolve to realid=attributes.npm run format:check— clean.HarperFast/harpercheckout:resources/DESIGN.md-> Conventions (theMaybePromiseobligation),resources/Resource.ts:255(loadAsInstance === false ? resource.post(query, data) : resource.post(data, query)),resources/Resource.ts:516,524(getContext/getCurrentUserare prototype-only),resources/login.ts(the static sign-in shape),server/REST.ts:242,244(verb dispatch against the class),security/auth.ts:126,279,444(session shape and the real logout call).reference/database/,reference/resources/andreference/security/forsuper.get(/super.post(/super.put(/super.patch(: 12 call sites, every one now awaited or returned. The only unawaited-and-unreturned one wasschema.md:869, fixed here. Every static verb example that touches a request body now awaits it first —api.md:244,overview.md:59,resource-api.md:115,136,155,755,jwt-authentication.md:156,171.resource-api.md's Resource Static Methods section other than the caching-source examples undersourcedFrom()(class MySource extends Resource { async get() ... }, the write-throughput/delete,async post(data)under "Loading from source in write methods",async *subscribe()) — instance methods are the correct and intended form for source resources, so those were deliberately left alone.reference_versioned_docs/**untouched; no heading renamed.Review feedback
gemini-code-assistopened five nullish-guard threads on the examples above. Every one is reachable on the documented dispatch path —server/REST.ts:215assignsrequest.dataonly when the request carriescontent-lengthortransfer-encoding, so a body-less POST really does await toundefined— so all five are addressed. Two of them differ from the literal suggestion:api.mdanswers a missing body with a 400 rather than optional-chaining past it.if (body?.data)on its own forwards the nullish body tosuper.post(target, body), which lands intransactional()'s single-argument branch (resources/Resource.ts:589-603) and stores theRequestTargetas the record — the same silent failure this PR exists to remove. The pre-push review caught that on the first attempt at this thread.schema.mdguardsrecord?.dataand returns the record early. Optional chaining alone would answer a missing record with a200and an empty body; a nullish GET return is what REST turns into a 404 (server/REST.ts:283), and the sibling Serving Binary from a Resource example inapi.mdalready reads exactly this way.The other three take the suggestion:
(await data) ?? {}in the sign-in example — verbatim what core's own static sign-in does (resources/login.ts). Without it an empty POST to a sign-in endpoint is a destructuringTypeError(500) instead of a 403.context?.userandcontext?.session?.user— a static verb called from server-side code receives a context argument only if the caller passes one, which is what thegetContext()entry above them now says. Both guards already returned 401 for "no authenticated user", which is the honest answer when there is no context either.Downstream
HarperFast/skills#79reported these against the publishedharper-best-practicesskill. The four affected rules —using-blob-datatype,handling-binary-data,extending-tables,checking-authentication— aremode: generateinrules.manifest.yaml, generated from these exact files and sections. The chain is: this PR merges ->auto/docs-syncregeneratesharper-best-practices-> first-party apps refresh their vendored copy withnpx skills update --project. Patching the skills repo or the vendored copies directly would be reverted by the next regeneration, which is why the fix belongs here.HarperFast/skills#79can be closed once the regenerated rules land.For the human reviewer
static loadAsInstance = false;. Converting won because the page itself says "For new code, prefer static methods and omit the flag", the siblingjwt-authentication.mdexamples are already static, and the rest of the skill corpus prescribes static.getCurrentUser()'s example no longer callsgetCurrentUser(). It cannot: the section sits under Resource Static Methods, and the method is prototype-only. The example is now a static verb readingcontext.user, with a sentence sayinggetCurrentUser()is the instance-side accessor. If you would rather the section keep demonstrating its own method, the alternative is to move it — but headings feed the skills manifest'ssection:selectors, so a move is not free.overview.md:59— astatic postoverride means noallowCreatepredicate runs (resources/DESIGN.md: "the override makes the access-control decision"), and the extending-a-table example does not say so. True before this PR too, and probably the most valuable follow-up here.resource-api.md:157—this.create(target, data.content)mis-ordersResource.create's arguments. WithloadAsInstanceunset,Resource.ts:214-225takes the two-argument form and shifts, so theRequestTargetis stored as the record anddata.contentbecomes the context. Same silent-failure class, different mechanism. I traced this one; it is real.resource-api.md:1152—static async function get(target)is invalid class syntax.reference/components/javascript-environment.md:89— still recommendsthis.getCurrentUser()in a Resource method without the static-dispatch caveat this PR adds.resource-api.md:69's non-asyncstatic get(target) { ... return super.get(target); }can resolve to a pending promise. It cannot:server/REST.tsawaits the dispatch result (await transaction(request, ...), andtransaction()forwards a thenable), andsuper.getis aMaybePromiseby design. Left as is.overview.md:58narration. It states the consequence of returning the promise, which is the defect being fixed and feeds the generatedextending-tablesrule, so it stays.authon every round in this worktree (agynot authenticated). Cross-model coverage was codex + cursor-composer on rounds 1 and 4, codex alone on the round-2 and round-3 deltas. The round-4 domain-adjudication leg timed out, so the findings below are unadjudicated. Round 4 confirmed the 400 guard resolves the finding it raised in round 3 and produced no new ones; cursor-composer reported no blockers, concerns, or suggestions.Refs HarperFast/skills#79
🤖 Generated with Claude Code
https://claude.ai/code/session_019drMJUhfEQfeNVDcU22oNe
Complexity: easy
Review-Coverage: authored=claude; ran=codex,cursor-composer; blocked=gemini(auth),domain(timeout); declined=cursor-grok; rounds=4 @ a15776b
Human-Review-Need: 4 @ a15776b