feat: the transport takes a signal, a fetch of your own, and stops taking a refused credential for a result - #446
Open
MrRefactoring wants to merge 1 commit into
Open
feat: the transport takes a signal, a fetch of your own, and stops taking a refused credential for a result#446MrRefactoring wants to merge 1 commit into
MrRefactoring wants to merge 1 commit into
Conversation
This was referenced Aug 24, 2026
Open
MrRefactoring
force-pushed
the
feat/transport
branch
from
August 25, 2026 15:12
be3e6e0 to
aea1022
Compare
…king a refused credential for a result Three requests older than 6.0, all of them the same shape: the client had no seam. `fetch` was reached as a global, a request could not be cancelled, and a response's headers were read for one thing and thrown away. None of it was visible from outside. Every call takes an `AbortSignal` as an optional last argument — an operation with no parameters takes it in their place — and the signal reaches `fetch` as well as cutting short a retry back-off that until now had no bound on total wall time. The abort reason is rethrown untouched rather than wrapped, so `error.name === 'AbortError'` and `error === signal.reason` both hold, and a `TimeoutError` from `AbortSignal.timeout()` stays one. Fixes #406. The `fetch` the client calls is yours to replace. It receives the URL and the `RequestInit` the client built, headers included, and returns a `Response` — logging, tracing, a corporate proxy, fixture recording. The OAuth 2.0 token and cloud-id calls go through it too, so a proxy covers the whole flow rather than working until the first refresh. Its type is `(url: string, init: RequestInit) => Promise<Response>` rather than `typeof globalThis.fetch`, so undici's fetch fits without a cast. Fixes #404. An expired API token is an error rather than an empty result, and this changes behaviour. Around a quarter of Jira's operations can be reached anonymously, and there a dead token does not fail the request: Jira answers as the anonymous user and reports the refusal only in `X-Seraph-LoginReason`. Measured against a live site, `GET /rest/api/3/project/search` with a dead token answers 200 and `{"total":0,"isLast":true,"values":[]}`. The client reads that header now and throws `AuthError` whatever the status, recording the status that actually arrived. Fixes #418. Two smaller ones ride along. A header set to `undefined` is left out rather than sent, and falls through to the client-wide value instead of erasing it. And two authentication strategies for a self-hosted instance arrive with the core rather than with the surface they serve — `src/core` is generated whole and `createClient` branches on the strategy, so separating them would mean hand-writing a core the generator does not produce. Riding along with the regeneration is specification drift, and one part of it breaks callers. Four operations declared their body as `Record<string, any>` — the generator's fallback for a request body that is not an object — and now declare what the endpoint reads: `addWatcher` and `setPreference` take a `string`, `updateEntityPropertiesValue` an `EntityPropertyDetails[]`, and Service Management's `attachTemporaryFile` a `MultipartFile[]`. None of the four could be called correctly through the old declaration; the live suite carried two helpers whose only job was to cast a string past it, and both are deleted here. That cast is the break. `updateEntityPropertiesValue` and `attachTemporaryFile` had no coverage at all, and a live run cannot give them much: one is addressed with a Connect app's JWT, the other needs an agent licence this tenant does not hold. Both gain a live test pinning the typed refusal, and all four gain unit coverage asserting what leaves the client — the neighbouring `updateIssueFields` wraps its body under a key, so a bare array is a distinction worth holding. Four generated doc comments are restored rather than regressed. The generator's HTML stripper could not tell a tag from a placeholder — `<project ID or key>` reads as an element named `project` — and turned a format string into "The format is `,,`" while emptying the API token out of three curl examples. Fixed upstream in apis-code-gen; regenerating with the fix changes exactly these four files, all of them restorations to what master already says.
MrRefactoring
force-pushed
the
feat/transport
branch
from
August 25, 2026 15:57
aea1022 to
ede5791
Compare
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.
Second of eight, splitting #443. Base is
chore/foundation(#445) — GitHub retargets this tomasteronce thatmerges, and the diff shown here is the increment alone.
Closes #406, closes #404, closes #418.
Three requests older than 6.0
The client had no seam.
fetchwas reached as a global, a request could not be cancelled, and a response's headerswere read for one thing and thrown away.
AbortSignalas an optional last argument — an operation with no parameters takes it intheir place. It reaches
fetchand cuts short a retry back-off that had no bound on total wall time. The abortreason is rethrown untouched, so
error.name === 'AbortError'anderror === signal.reasonboth hold. Nothing thatcompiled before stops compiling: the argument is optional and last. Support AbortSignal #406
fetchthe client calls is yours to replace. The OAuth 2.0 token and cloud-id calls go through it too, so aproxy covers the whole flow rather than working until the first refresh an hour later — and a wrapper that logs
bodies will see
client_secret. [Feature Request] Add Axios Interceptor Configuration Support #404operations can be reached anonymously, and there a dead token does not fail: Jira answers as the anonymous user and
reports the refusal only in
X-Seraph-LoginReason. Measured on a live site,GET /rest/api/3/project/searchwith adead token answers
200and{"total":0,"isLast":true,"values":[]}. Code that treated that as normal now throws —it was reading anonymous data and calling it yours. [Feature request] Throw error on expired/invalid API token #418
Four request body types change, and that is a break
The regeneration carries specification drift that has nothing to do with the transport, and one part of it is
breaking. Four operations declared their body as
Record<string, any>— the generator's fallback for a request bodythat is not an object — and now declare what the endpoint actually reads:
issueWatchers.addWatcherRecord<string, any>stringmyself.setPreferenceRecord<string, any>stringappMigration.updateEntityPropertiesValueRecord<string, any>EntityPropertyDetails[]servicedesk.attachTemporaryFile(Service Desk)Record<string, any>MultipartFile[]None of the four could be called correctly through the old declaration. The proof is in this repository: the live
suite carried two helpers whose only purpose was to cast a
stringinto aRecord<string, unknown>, each with acomment saying the declaration was wrong and the fix belonged upstream. It landed upstream, and this pull request
deletes both helpers. That is also the break — a caller who was calling these correctly was casting, and the cast is
what stops compiling.
The two array bodies break more quietly, since an array satisfies
Record<string, any>: a caller already passing theright thing is untouched. Documented in the changelog under a new Types heading, matching how 6.2.0 recorded the
same class of change.
updateEntityPropertiesValueandattachTemporaryFilehad no coverage of any kind, and a live run cannot give themmuch — the first is addressed with a Connect app's JWT, the second needs an agent licence this tenant does not hold.
Both now have a live test pinning the typed refusal, and all four have unit coverage in
tests/unit/nonObjectBodies.test.tsasserting what leaves the client. That last one earns its keep: the neighbouringupdateIssueFieldsin the same generated file does wrap its body under a key, so "bare array, not wrapped" is adistinction a regression could plausibly erase.
A documentation regression this branch was carrying, fixed in the generator
The same regeneration was quietly destroying doc comments.
htmlDescriptionsToMarkdownstrips leftover HTML fromdescriptions, and its catch-all was
/<\/?[a-z][^>]*>/gi— which cannot tell a tag from a placeholder, since<project ID or key>reads as an element namedprojectcarrying three boolean attributes.Master is correct; this branch was not. It turned
IssueBulkMovePayload's format string into "The format is,," andemptied the API token out of three curl examples, leaving
-u 'email@example.com:'. Measured across the sixspecification documents, the catch-all removed 85 substrings of prose to remove 4 of markup.
Fixed in
apis-code-gen(fix: a description is prose, and prose is full of angle brackets) by two conditions, sinceneither is sufficient alone: only names of real HTML elements are recognised, and no replacement reaches inside a code
span. On the current documents that is exact — 4 removals, all markup, nothing else touched — and six new tests pin
it. Regenerating with the fix changes exactly four files in this tree, all of them restorations, so the branch no
longer regresses master.
Two things worth flagging in review
Data Center authentication ships here, ahead of its surface.
basicgains ausername/passwordbranch andoauth2Serverarrives withsrc/core/oauthServer/. They belong to the Data Center pull request further up thisstack, and they are here anyway:
src/coreis generated whole, andcreateClientbranches on the auth strategy, solifting them out would mean hand-writing a core state the generator does not produce — which CONTRIBUTING says
disappears on the next regeneration. Taking a subset of the generated tree is safe; inventing an intermediate one is
not. Nothing in this pull request answers to either strategy yet, and the changelog says so.
getTenantContextwas lifted out, because there it was a clean subset: one file, one export line, no otherreference in
src/core. It arrives with Teams, which is what needs it.Verified
typecheck,lint,test(236 passing, up from 203),build,check:browser,check:consumersall pass locally.docs:apinow builds with no warnings at all — the pre-existing one flagged in #445, a Service Managementdescription linking
./#api-…, is corrected by this regeneration as predicted. 4377 pages.Live coverage for the new behaviour:
tests/live/cloud/abort.test.tsandtests/live/cloud/auth.test.ts, plus 29 newunit tests across
createClientand the server OAuth 2.0 manager.