Skip to content

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
masterfrom
feat/transport
Open

feat: the transport takes a signal, a fetch of your own, and stops taking a refused credential for a result#446
MrRefactoring wants to merge 1 commit into
masterfrom
feat/transport

Conversation

@MrRefactoring

@MrRefactoring MrRefactoring commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Second of eight, splitting #443. Base is chore/foundation (#445) — GitHub retargets this to master once that
merges, 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. 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.

  • Every call takes an AbortSignal as an optional last argument — an operation with no parameters takes it in
    their place. It reaches fetch and cuts short a retry back-off that had no bound on total wall time. The abort
    reason is rethrown untouched, so error.name === 'AbortError' and error === signal.reason both hold. Nothing that
    compiled before stops compiling: the argument is optional and last. Support AbortSignal #406
  • The fetch the client calls is yours to replace. 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 an hour later — and a wrapper that logs
    bodies will see client_secret. [Feature Request] Add Axios Interceptor Configuration Support #404
  • An expired API token is an error rather than an empty result. This changes behaviour. A quarter of Jira's
    operations 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/search with a
    dead token answers 200 and {"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 body
that is not an object — and now declare what the endpoint actually reads:

Operation Body was Body is
issueWatchers.addWatcher Record<string, any> string
myself.setPreference Record<string, any> string
appMigration.updateEntityPropertiesValue Record<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 string into a Record<string, unknown>, each with a
comment 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 the
right thing is untouched. Documented in the changelog under a new Types heading, matching how 6.2.0 recorded the
same class of change.

updateEntityPropertiesValue and attachTemporaryFile had no coverage of any kind, and a live run cannot give them
much — 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.ts asserting what leaves the client. That last one earns its keep: the neighbouring
updateIssueFields in the same generated file does wrap its body under a key, so "bare array, not wrapped" is a
distinction a regression could plausibly erase.

A documentation regression this branch was carrying, fixed in the generator

The same regeneration was quietly destroying doc comments. htmlDescriptionsToMarkdown strips leftover HTML from
descriptions, 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 named project carrying three boolean attributes.

Master is correct; this branch was not. It turned IssueBulkMovePayload's format string into "The format is ,," and
emptied the API token out of three curl examples, leaving -u 'email@example.com:'. Measured across the six
specification 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, since
neither 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. basic gains a username/password branch and
oauth2Server arrives with src/core/oauthServer/. They belong to the Data Center pull request further up this
stack, and they are here anyway: src/core is generated whole, and createClient branches on the auth strategy, so
lifting 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.

getTenantContext was lifted out, because there it was a clean subset: one file, one export line, no other
reference 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:consumers all pass locally.

docs:api now builds with no warnings at all — the pre-existing one flagged in #445, a Service Management
description linking ./#api-…, is corrected by this regeneration as predicted. 4377 pages.

Live coverage for the new behaviour: tests/live/cloud/abort.test.ts and tests/live/cloud/auth.test.ts, plus 29 new
unit tests across createClient and the server OAuth 2.0 manager.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature request] Throw error on expired/invalid API token Support AbortSignal [Feature Request] Add Axios Interceptor Configuration Support

1 participant