Repository navigation
Sdk harness update - #54
Merged
Merged
Conversation
…rors A refusal's header fields are how a peer says what it wants next: a Web Bot Auth verifier names the signature it requires in Accept-Signature, a draining gateway sets Retry-After, and a delivery edge's 403 carries the discovery headers. The Go client kept them only on connect-go's Error.Meta(), which is filled for a decoded JSON envelope and never for a plain-text gateway answer, and the content leg dropped them. CallError gains Header, and resolvers.FetchError gains the same field. Both carry the header fields of an error answer, 400 or above, and nothing for any other answer: a redirect's Location names a URL the peer chose, so a refused redirect must not hand its target back through the fields. One function, failure.RefusalHeader, owns that rule for both legs. The answer recorder now keeps the status and the copied fields of the response that arrived, so a gateway's answer carries them as an envelope does. CallError.Status is now set on every RPC refusal, as its doc already said; the RPC path left it at 0. A strictly refused envelope keeps its status and fields beside its Connect code. The connect-error and transport-failure replays now assert the status too.
Every RPC leg refuses to follow a 3xx, and the refusal's own message redacts the target to its scheme, host and path. net/http wraps that refusal in a *url.Error carrying the raw Location the peer sent, credentials included, and the Go client keeps that Location out of its errors only because connect-go discards the wrapper before the error surfaces. A test now pins the result on every RPC face, the offer-derived leg included: a redirect whose Location carries userinfo, a query token and a fragment yields an error that names none of them. A dependency that stopped discarding the wrapper fails here rather than in a log.
CallError gains headers: the header fields of the peer's error answer, 400 or above, on the RPC legs and the delivery fetch, on both faces. A Web Bot Auth refusal's Accept-Signature, a gateway's Retry-After and an edge's discovery headers are now read through the SDK instead of over raw HTTP; parse_discovery_hint reads them with the error's status. Every other failure carries none: a local one, a transport one, a 2xx the client could not read, and a refused redirect, whose Location names a URL the peer chose. The CallError constructor owns that rule, so each site that builds a failure from an answer hands it the answer's fields and the constructor keeps them only for an error status. They are a copy, read case-insensitively, a field sent on several lines joined with ", ", and never rendered into the message. Each face's send now returns the answer's status, body and header fields as one value, which the verbs' decode step reads. decode and decode_with_raw keep their positional arguments and take the fields as a keyword.
…allError ForaCallError gains headers, a Fetch Headers holding the header fields of the peer's error answer, 400 or above, on the RPC legs and the delivery fetch. A Web Bot Auth refusal's Accept-Signature, a gateway's Retry-After and an edge's discovery headers are now read through the SDK; parseDiscoveryHint reads them with the error's status. Every other failure carries none: a local one, a transport one, a 2xx the client could not read, and a refused redirect, whose Location names a URL the peer chose. The ForaCallError constructor owns that rule, so each site that builds a failure from an answer passes the answer's fields and the constructor keeps a copy only for an error status. A field sent on several lines reads as one value joined with ", ", and the fields are never rendered into the message. undici's raw response headers are read one way for both tiers: the resolvers' existing lookup and the client's Headers come from one function. A value Headers cannot hold, a character above U+00FF, is left out rather than allowed to turn the description of a failure into a second failure. The default send returns the fields as UnaryResponse.headers, which is optional, so an injected send still compiles and yields refusals carrying none.
…play The three clients read a refusal's header fields through three HTTP libraries: net/http canonicalizes names and keeps each line of a repeated field, undici lowercases names, and httpx keeps them as sent. refusal-header-vectors.json pins what a caller sees regardless: which failures carry fields (an answer of status 400 or above, on the RPC legs and the delivery fetch, and never a redirect or a 2xx the client could not read), the value each field reads as with a repeated field joined by ", ", and the discovery hint parsed from the failure's own status and fields. The rows are captured from the real Go client, record their own field lines only, and cover a verifier's Accept-Signature, a strictly refused envelope, a gateway's Retry-After, a field sent on two lines, an edge's proof refusal and its 403 discovery headers, and a redirect on each leg. Go, Python on both faces and TypeScript over real sockets replay every row, and the parity matrix lists the corpus.
…changelogs The Unreleased changelog, mirrored on the website, gains a "Refusal headers" entry among the harness capabilities: the field in each language, which failures carry fields and why a redirect never does, how a field sent on several lines reads, the optional UnaryResponse.headers, and the new corpus. The breaking list for SDK users notes that the Go CallError and FetchError no longer compare with == and that an RPC CallError now carries and renders its status. The design history records the rule and its reasons, how the three HTTP libraries are read alike, why Go records the answer itself rather than reading connect-go's Error.Meta(), the test that pins a refused redirect's target out of Go's RPC errors, and the differences left as they are. The agent SDK overview's harness table and rules, the fetch flow's Go error listings and the three SDK READMEs describe the new fields.
… internal Go package The canonical signing form, JCS over proto-JSON under the pinned option set, and the checks around the acceptances now live in internal/canonicalsign. helpers renders every offer and acceptance through it, with its exported functions, their refusals and their order unchanged. The acceptance functions take the requester check as a parameter and run it at the exact position the protocol's signer does, with every other check their own: a nil or unsigned offer, a missing requester, no items, more than 256 items, an item with no signature or exchange. helpers passes the check fora.proto requires of a signer. A test signer that must produce what a verifier refuses can then reuse the one renderer and every other check instead of copying them. A new test pins the exact text, the sentinel and the order of every refusal the acceptance canonicalizers and signers make, combined faults included, and passes unchanged before and after the move. helpers.ErrUnknownFields is the same value as before, and the shared vectors regenerate byte for byte.
…ty requester fora.proto requires a signer to refuse acceptance bytes that name an empty requester, and a verifier to refuse them even when the signature over them verifies. The signers in helpers are that signer, so no SDK function could produce the one input a verifier's refusal is tested with; a harness wrote the canonical bytes and the Ed25519 signature by hand. helperstest holds four twins of the helpers acceptance functions: CanonicalAcceptanceBytesAnyRequester, SignOfferAcceptanceAnyRequester, CanonicalRequestAcceptanceBytesAnyRequester and SignRequestAcceptanceAnyRequester. Each skips only the empty-requester refusal and keeps every other one, in the same text and order, by passing the shared renderer a check that admits any requester. That check is unexported, so nothing outside the package can name it. The package is test support, like net/http/httptest, and a test fails if any non-test file imports it. The twins reproduce the refused rows of acceptance-vectors.json and request-acceptance-vectors.json byte for byte, signatures included, match the helpers signers on every named requester, and produce acceptances the helpers verifiers still refuse.
fora_sdk.testing holds the Python twins of the Go helperstest signers: jcs_acceptance_payload_any_requester, sign_offer_acceptance_any_requester, jcs_request_acceptance_payload_any_requester and sign_request_acceptance_any_requester. Each is its fora_sdk.core namesake with only the empty-requester refusal skipped, so a test can send an acceptance that is valid in every respect but its requester and attribute a verifier's refusal to that alone. core renders each acceptance through one private renderer that takes the requester check as a parameter, and its public functions pass the check the protocol requires; the testing module passes one that admits any requester. The texts and the order of every refusal are unchanged, pinned by a new test that passes before and after the change. The twins reproduce the refused rows of acceptance-vectors.json and request-acceptance-vectors.json byte for byte, and the recorded request-acceptance signature is now checked against its bytes, which no Python test did. The module is not re-exported from fora_sdk, and a test fails if any module of the SDK imports it.
…n empty requester @fora-protocol/sdk/testing holds the TypeScript twins of the Go helperstest signers: acceptancePayloadAnyRequester, signOfferAcceptanceAnyRequester, requestAcceptancePayloadAnyRequester and signRequestAcceptanceAnyRequester. Each is its ./acceptance namesake with only the empty-requester refusal skipped. They use WebCrypto only, like the signers they twin. The acceptance renderer moves into src/acceptance-render.ts, a module the package manifest deliberately does not export. It takes the requester check as a parameter; ./acceptance passes the check the protocol requires, and ./testing passes one that admits any requester. Every refusal keeps its text and order, pinned by a new test that passes before and after the move. The twins reproduce the refused rows of the shared acceptance corpora byte for byte, and the recorded request-acceptance signature is now checked against its bytes. A test fails if any source file imports ./testing, or if anything but the two signers imports the renderer. Both package manifests list the new export path.
…parity matrix The four helperstest functions are mapped to their Python and TypeScript twins exactly as the helpers signers they skip one refusal of are mapped: CanonicalAcceptanceBytesAnyRequester to jcs_acceptance_payload_any_requester and acceptancePayloadAnyRequester, SignOfferAcceptanceAnyRequester to sign_offer_acceptance_any_requester and signOfferAcceptanceAnyRequester, and the request-acceptance pair alike. The API-surface gate now reads helperstest through go doc and fora_sdk.testing through its __all__, so a twin missing in any language fails it, and the generated parity matrix gives the package its own section.
The Unreleased changelog, mirrored on the website, gains a "Test-only acceptance signers" entry among the harness capabilities: why a test needs a signer the protocol's own signers refuse to be, the twelve functions across the three test-support packages, that only the empty-requester refusal is skipped, the shared renderer in each language, and the refused rows they reproduce. The design history records the decision: a test package rather than a flag on the protocol's signer or a client seam, why only the requester check is skipped, how one renderer serves both, and why fora.proto's sentence about a signer stays as it is. The agent SDK overview's harness table and rules, and the three SDK READMEs, list the test signers as test support.
…escribe ClientConfig.strict_requests, off by default, closes the two ways a request can leave the Python client carrying something the contract does not describe. The generated models ignore a member they do not declare, so a plain dict with a misspelled key passed the field-level check, was signed and sent, and the server dropped the member. Under the switch a verb takes only its own generated request model, or a RawBody, which is still sent exactly as given and never checked. A plain dict, or a model of another message, is refused as MALFORMED naming the model to pass, at the one point every message passes through before the verb resolves, reads or signs anything. The message as stamped, with ver, the configured requester and a purchase's reflected offers in it, must then pass the published strict JSON Schema of the request message and the SDK's cross-field rules, through the check strict decoding already uses. An unknown member at any depth is refused and named by its path. The check applies whatever validation says; the mapped Validation type is unchanged. The new field is the last one of ClientConfig, so positional construction is unaffected.
The Unreleased changelog, mirrored on the website, gains a "Strict requests (Python)" entry beside typed request inputs: how a misspelled key reaches the wire without the switch, the two checks, RawBody left as it is, the switch's independence from validation, the keyword a model drops before the SDK sees it, and why Go and TypeScript have no such switch. The design history records where each check sits and why: the type gate at the copy every message passes through before any I/O, the strict check on the stamped message, a field of its own rather than a third value of the mapped Validation type, off by default because purchases reflect offers a newer minor version may extend, and the gap a type checker covers. The agent SDK overview's harness table and rules, the JSON Schemas reference and the Python README describe the switch.
The pinned proto-JSON option set and the unknown-field walk moved from helpers into internal/canonicalsign, and three comments still named the old place: the Python wire canonicalizer cited helpers' canonicalSignJSONOptions, the error-detail vector emitter cited canonicalsign.go, and the walk's note on map entries named offer_test.go without the package that holds it.
CallError.headers is typed Mapping[str, str], but at run time it is the error's own httpx.Headers copy, and a caller can change it. The README and both changelog mirrors called it a read-only mapping, which the code does not enforce. They now describe it as the copy it is, looked up case-insensitively, like the Go http.Header and the TypeScript Headers the other SDKs return.
A strict_requests refusal carries the same text strict decoding gives an answer: the path and the schema's complaint, which quotes a member's name and, for a field that breaks a rule, its value. On a purchase, a reflected offer's member is the Exchange's text. The design history now says why that is kept. A harness has to see which key was refused, a strict client already gets the same text for the discovery answer that carried the offer, and the text is bounded by that answer's read cap and quoted with escapes, so it cannot break a log line.
main is now v1.1.0, and this branch's changes are not part of it. The release
renamed the changelog's Unreleased heading to v1.1.0, so the entries this
branch added under Unreleased auto-merged into the v1.1.0 section without a
conflict, listing a Go breaking change and three SDK capabilities as released
when no release carries them. main also rewrote the publisher guide around
the publisher console.
Resolution ledger:
- textual conflicts:
- docs/design-history.md -> union: main's "A publisher proves its domain
with the manifest it serves" first, then this branch's three sections at
the end, in the order the two land.
- semantic, auto-merged, corrected: proto/CHANGELOG.md and its website mirror
-> this branch's four entries move out of v1.1.0 into a new Unreleased
section above it, so the v1.1.0 section is the release's, byte for byte.
The Go client errors entry goes under "Breaking against v1.1.0", and its
"which v1.0.8 left at 0" now reads "which v1.1.0 left at 0": the release
left an RPC CallError's Status at 0 as well. The refusal headers, test-only
acceptance signers and Python strict requests entries go under "Changes
since v1.1.0", in one entry for the harness capabilities they extend, with
their text unchanged.
- semantic, auto-merged, reviewed: package.json, sdk/ts/package.json and
sdk/python/pyproject.toml -> union (main's 1.1.0 versions and pin, this
branch's ./testing export and _call.py lint waiver; disjoint hunks). The
versions stay 1.1.0: unreleased work does not bump them.
- taken from main unchanged: the release's version bumps and lockfiles, the
publisher guide and console pages, the onboarding modules and tests, and
the org-reference check.
- generated code and the conformance vectors: identical to this branch; main
did not touch them.
…s its last An interceptor passed to WithInterceptors runs inside the client's answer and strict-decoding interceptors, and may call next again to retry a call. Every request it sent wrote into the same per-call record, and nothing cleared it: the answer recorder stored an answer only when one arrived, and the strict envelope capture an error body only when one could be read. A call whose first attempt was refused with a 503 and its Retry-After, and whose second got no answer at all, therefore failed as if the peer had answered the second: Code was marked as the peer's, Status and Header carried the 503 and its Retry-After, and Error() named HTTP 503. Under strict decoding the earlier attempt's error envelope was judged in place of the last request, so a dropped connection could be reported as a malformed answer. The answer recorder now clears both records before every request it sends. It clears them before the request rather than when no answer comes back, because a 2xx, an error body past the read cap and one in a coding the client did not ask for all arrive and store nothing, and none of them may leave an earlier attempt's record in place. The capture is looked up once per round trip and handed to captureErrorBody. The record cannot tell which attempt an error came from, so WithInterceptors and CallError now say what a retrying interceptor gets: the last request's record, which is right when the interceptor returns that attempt's error and sends its attempts one at a time. A black-box test retries through the real client stack over a scripted transport, and a white-box test pins the recorder: no answer, a success, an error answer, and three error answers that store no body each leave only their own record.
…uards The IO-leaf guard and the connect-free guards each scanned one directory: helpers' own files, and core's. Code that moved out of that directory left the guard's reach without a failure. The canonical renderer and the acceptance checks now live in internal/canonicalsign, which helpers calls, and a planted http.Client there passed the IO-leaf guard; internal/hostredact was outside every guard already. A new test-support package, internal/importclosure, walks the in-module packages a package is built from by reading the import specs of their non-test files with go/parser, so the guards stay hermetic and run no go command. Like vectorio it takes no *testing.T and is imported by test files only. It fails rather than returning a smaller closure when an import path maps to no package, and its own tests run it on a module built for the test. The IO-leaf guard now scans every package helpers is built from except the generated wire packages, whose imports the connect-free guard checks. Both connect-free guards check every package of their closure, gen/ included, since they read import specs and cannot trip on comments. Each guard pins the packages it must reach. Core's copy of the helpers scan is gone: core's closure includes helpers, and helpers still guards itself. The guard headers described a go list -deps gate that no CI job runs, and the connect-free guard still said it failed until a refactor that has since landed; both now describe what the scan covers.
The guard that keeps production code from importing helperstest skipped every file in a directory named helperstest. The package's only non-test file is helperstest.go, and a package cannot import itself, so the exemption admitted nothing; it only meant that a file in any other directory of that name could import the package unnoticed. With the exemption gone, a planted connect/helperstest/x.go importing the package fails the guard.
…er guard The guard that keeps production code from importing the test signers, and from importing their shared renderer outside the two signers, matched only `from "…/testing.ts"`. Under the package tsconfig an extensionless or .js specifier, the package's own name (@fora-protocol/sdk-l1/testing) and a dynamic import() all compile, and each passed the guard. The guard now asks the compiler: ts.preProcessFile lists every specifier a source file uses, static, re-exported, side-effect, type-only or a literal import(), and ts.resolveModuleName resolves each one under the package's tsconfig. A file is flagged when an import resolves to the module, which is what the Go and Python guards do with their own parsers. A comment or a string naming the module is not an import. Only an import() of a computed string is beyond a static check, and the test says so. Cases for each form, and for what must not be flagged, run the check itself.
…uards in the docs and changelogs The Unreleased changelog, mirrored on the website, gains a Go SDK fix entry: a call an interceptor retries reports its last attempt. In v1.1.0 a call whose last attempt got no answer could report its Connect code as the peer's, and strict decoding could judge an earlier attempt's error envelope in place of the last attempt's failure; Code, and the new Status and Header, now describe the last request the call sent. The design history records why the record belongs to one request, why it is cleared before the request rather than when no answer arrives, what it cannot attribute and therefore what a retrying interceptor should do, and why the Python and TypeScript clients need nothing. A second entry records that the Go purity guards now follow the code they guard into the internal packages helpers and core are built from. The fetch flow's error types and the Go README say that a retried call's Status, Header and Code describe its last request, and the README says what the IO-leaf and connect-free guards scan.
When strict decoding refuses an error envelope, the failure keeps the peer's Connect code, the HTTP status and the answer's header fields, and carries no detail. Go's strict-decoding doc, Python's ClientConfig.strict and TypeScript's strict option already said so, and the refusal-header corpus pins it in all three SDKs. The agent SDK overview, the three READMEs and a private Python docstring still said only the code was kept. The Unreleased "Refusal headers" entry, mirrored on the website, now says that a strictly refused error envelope keeps the fields as it keeps its code.
The docs told a caller to read an edge's discovery headers off a failure with parse_discovery_hint(err.status, err.headers), or its TypeScript twin. The JSON clients' status is int | None and number | undefined, which is absent when no answer arrived, while the parsers take an int and a number, so the documented call fails mypy --strict and tsc --strict; the SDKs' own tests already pass 0 in its place. The parsers stay as they are, mirroring Go's int status, whose zero value means no answer. The Python README and CallError.headers docstring now show parse_discovery_hint(err.status or 0, err.headers), the TypeScript README and ForaCallError.headers doc parseDiscoveryHint(err.status ?? 0, err.headers), and the agent SDK overview says the parser reads the failure's status, 0 when it has none. Both documented calls were checked under mypy --strict and tsc --strict.
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.
Three SDK capabilities for a strict test harness
The reference implementation's e2e harness drives every request through the Python SDK and reads every answer through its types. In three places it still had to work around the SDK: it read refusal headers over raw HTTP, hand-wrote an acceptance signer, and had no guard against a misspelled request key. This branch closes all three in the SDKs. It also makes a Go call that an interceptor retries report its last attempt, and widens the guards that keep the test signers out of production code and the L1 helpers IO-free and Connect-free. Go is the parity oracle throughout, and nothing in the wire contract changes.
What changes
1. A refusal carries the header fields of the answer that refused (Go, Python, TypeScript)
connect.CallError.Headerandresolvers.FetchError.Header, anhttp.Header;CallError.headers, a mapping looked up case-insensitively;ForaCallError.headers, a FetchHeaders.internal/failure.RefusalHeaderin Go, the error constructor in Python and TypeScript.Locationnames a URL the peer chose and can hold credentials.Accept-Signature, a gateway'sRetry-After, and an edge's discovery headers.parse_discovery_hint(err.status or 0, err.headers)(Python),parseDiscoveryHint(err.status ?? 0, err.headers)(TypeScript) andhelpers.ParseDiscoveryHint(err.Status, err.Header)(Go).statusis absent when no answer arrived, and the parsers take an integer, as Go's zero value does. So the documented calls pass0in its place and type-check undermypy --strictandtsc --strict. The parsers' signatures are unchanged.strings.Join(Header.Values(name), ", ")reads the same value.CallError.Statusis now set on RPC errors. It is the status of the peer's error answer (400 or above). The content leg always set it; the RPC legs left it at 0, though it was documented.connect.Error.Meta().Meta()is empty when the error body is not a Connect envelope, which is exactly the case of a gateway's 503 withRetry-After.WithInterceptorsmay callnextagain to retry a call.Status,HeaderandCodetherefore describe the last request the call sent.Codewas reported as the peer's. UnderWithStrictDecoding, an earlier attempt's error envelope could be judged, and refused, in place of the last attempt's failure.WithInterceptorsandCallErrornow document that a retrying interceptor should return its last attempt's error and send attempts one at a time.rpc_401_strict_envelope_refusedpins this.UnaryResponsegains an optionalheaders, which the default send fills. An injected send that leaves it out yields refusals carrying none. A header value that FetchHeaderscannot hold (a character above U+00FF) is skipped, so building the error never throws.sdk/go/connect/testdata/refusal-header-vectors.json, generated by Go and replayed by all three SDKs. Its nine RPC and content-leg refusals pin which failures carry fields and how they read.*url.Errorthat would carry it. A new test, with a negative control, pins that.2. Test-only acceptance signers that admit an empty requester (Go, Python, TypeScript)
fora.proto requires a signer to refuse an acceptance that names an empty requester, and a verifier to refuse one even when its signature verifies. The SDK's signers are that signer, so a test proving the verifier's refusal had to hand-write the canonical bytes and the Ed25519 signature.
sdk/go/helperstest:CanonicalAcceptanceBytesAnyRequester,SignOfferAcceptanceAnyRequester,CanonicalRequestAcceptanceBytesAnyRequester,SignRequestAcceptanceAnyRequester;fora_sdk.testing, not re-exported fromfora_sdk:jcs_acceptance_payload_any_requester,sign_offer_acceptance_any_requester,jcs_request_acceptance_payload_any_requester,sign_request_acceptance_any_requester;@fora-protocol/sdk/testing:acceptancePayloadAnyRequester,signOfferAcceptanceAnyRequester,requestAcceptancePayloadAnyRequester,signRequestAcceptanceAnyRequester.sdk/go/internal/canonicalsign. The renderer and the acceptance checks moved there fromhelpers, whose exported API is unchanged.fora_sdk.core.sdk/ts/src/acceptance-render.ts, which neither package manifest exports.refusedrows ofacceptance-vectors.jsonandrequest-acceptance-vectors.jsonbyte for byte; the corpora themselves are unchanged.ast.go/parser. It no longer exempts every directory namedhelperstest. That exemption admitted nothing, since a package cannot import itself, but it would have let a file in any other directory of that name through.ts.preProcessFile, thents.resolveModuleNameunder the package tsconfig..jsspecifier, the package's own name (@fora-protocol/sdk-l1/testing) and a literalimport(), all of which compile and all of which a filename regex missed.import()of a computed string is beyond the check.helpers/io_leaf_guard_test.go) and the connect-free guards (helpers/connectrpc_free_test.go,core/library_adoption_guard_test.go) scanned one directory each. Moving the renderer intointernal/canonicalsigntook it out of their reach, andinternal/hostredactwas already outside it.internal/canonicalsignandgen/go/fora/v1for helpers; core and helpers for core.sdk/go/internal/importclosure. It reads import specs withgo/parser, runs no go command, and is imported by_test.gofiles only, likevectorio. It fails rather than returning a smaller closure when an import maps to no package. For helpers it returns exactly the in-module packagesgo list -depsreports.gen/packages are left out of the substring dialing scan but are included in the import checks.go list -depsCI gate that does not exist.sdk/parity/symbol-map.jsonlike the signers they mirror, and the generated parity matrix gains ahelperstestsection.docs/design-history.mdrecords the reasoning.3. Strict requests (Python)
ClientConfig.strict_requests: bool = False, appended as the last field.wire.models, or aRawBody. A plain dict, or a model of another message, raisesCallError(MALFORMED)naming the model to pass. The refusal comes before anything is resolved, read or signed.ver, the configuredrequester, and the offers a purchase reflects.strict_requests refused the request at /requester: Additional properties are not allowed ('domian' was unexpected).validation:validation="off"leaves the switch in force. The mappedValidationtype is unchanged.RawBodyis never checked.extra="ignore") before the SDK sees it. A type checker reports it, because the models are dataclass transforms.Docs
proto/CHANGELOG.mdand its website mirror gain:The Unreleased sections of the two files are identical, and the released sections are untouched.
docs/design-history.mdgains one new section per change. It also records why the Go answer record belongs to one request (and what it cannot attribute), and why the Go purity guards scan the packages their package is built from.Also updated:
fetch-flow.mdx;WithInterceptorsandCallErrorgodoc;CallError.headersand TypeScriptForaCallError.headersdocs.What the docs now state:
Compatibility
.protoorgen/change.connect.CallErrorandresolvers.FetchErrorhold anhttp.Header, so neither compares with==any more.CallError.Error()now names the status, for example(HTTP 401 Unauthorized).Codeis zero; v1.1.0 marked it as the peer's.sdk/go/internal/importclosureis internal test support, outside the parity map.UnaryResponse.headersis optional, so an existing injected send still compiles.strict_requestsis off by default.Checks
scripts/ci-local.shpasses (buf 1.66.1). It covers buf lint and generated drift,go build/vet/test, doc conformance, parity-matrix drift, the site test and build, generated-type parity, andbuf breaking.ci-local.shdoes not run also pass:go test -race ./sdk/go/...;npm test(tsc --stricton both tsconfigs, then vitest).refusal-header-vectors.jsonand thehelperstestentries insdk/parity/symbol-map.jsonand the generated parity matrix. Nothing undergen/changes, and every existing corpus regenerates byte-identical in verify mode.http.Clientininternal/canonicalsign, a planted connectrpc import ininternal/hostredact, and a dialing package imported through canonicalsign each fail the Go purity guards..js, self-name or dynamic import of./testingfails the TypeScript guard.sdk/go/connect/helperstest/x.gofails the Go import guard; it passed the old one.Known divergences, left as they are
text/htmlanswer asrefused; Python and TypeScript report it asmalformed. This predates the branch, and the new corpus does not record the kind.too_large) carries no status, so it carries no header fields either.Statusis 0 for a refused redirect and for a 2xx it could not read.What the harness can now replace
Each item needs the downstream pin bumped past
0b2f33e1.Accept-Signatureand of an edge's discovery headers (tests/e2e/harness/wba_conformance.py,tests/e2e/harness/edge_fetch.py): readCallError.headersinstead.tests/e2e/harness/requester_alteration.py: usefora_sdk.testing.ClientConfig(strict_requests=True).