@webex/http-core 3.12.0-next.8 → 3.12.0-next.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.repo-context.json +590 -0
- package/.sdd/manifest.json +362 -0
- package/AGENTS.md +219 -0
- package/docs/adr/0001-fork-xhr-for-frozen-object-environments.md +111 -0
- package/docs/adr/index.md +26 -0
- package/docs/architecture.md +335 -0
- package/docs/getting-started.md +108 -0
- package/docs/index.md +46 -0
- package/docs/specs/README.md +79 -0
- package/package.json +8 -8
- package/schemas/repo-context.schema.json +796 -0
- package/src/docs/README.md +485 -0
- package/src/request/docs/README.md +481 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Module Spec
|
|
3
|
+
title: 'request pipeline specification'
|
|
4
|
+
description: Responsibilities, boundaries, design, invariants, and verification for the http-core request pipeline and its Node and browser transports.
|
|
5
|
+
tags: [module, specification]
|
|
6
|
+
---
|
|
7
|
+
<!-- sdd-generated-metadata
|
|
8
|
+
doc_kind: module-spec
|
|
9
|
+
generated_from: module-spec@0.3.0
|
|
10
|
+
generated_by: claude-code
|
|
11
|
+
approved_by: pending
|
|
12
|
+
updated_at: 2026-09-23T06:29:48Z
|
|
13
|
+
validation_status: pass-with-warnings
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
# request pipeline
|
|
17
|
+
|
|
18
|
+
This source-local document at `src/request/docs/README.md` owns the stable specification for the
|
|
19
|
+
**request pipeline**: applying the interceptor chain around a platform-specific transport, and the
|
|
20
|
+
two transports themselves.
|
|
21
|
+
|
|
22
|
+
The export surface, error taxonomy, and interceptor base class belong to the parent module and are
|
|
23
|
+
specified in [`src/docs/README.md`](../../docs/README.md).
|
|
24
|
+
|
|
25
|
+
Related context: [repository architecture](../../../docs/architecture.md) ·
|
|
26
|
+
[documentation index](../../../docs/index.md) · [agent instructions](../../../AGENTS.md) ·
|
|
27
|
+
[specification registry](../../../docs/specs/README.md)
|
|
28
|
+
|
|
29
|
+
## Metadata
|
|
30
|
+
|
|
31
|
+
| Field | Value |
|
|
32
|
+
| ----------------- | ------------------------------------------- |
|
|
33
|
+
| Owner | Cisco Webex for Developers |
|
|
34
|
+
| Source path | `src/request` |
|
|
35
|
+
| Resource kind | Capability module |
|
|
36
|
+
| Status | Active |
|
|
37
|
+
| Last verified | 2026-09-23 |
|
|
38
|
+
| Module id | `src/request` |
|
|
39
|
+
| Parent spec | [`src/docs/README.md`](../../docs/README.md) |
|
|
40
|
+
| Doc kind | Module spec |
|
|
41
|
+
| Coverage score | 93.3% assessed 2026-09-23; 14 of 15 mandatory fields present — a characterization baseline is the one outstanding gap |
|
|
42
|
+
| Validation status | pass-with-warnings; validator `codex` assessed 2026-09-23; prior B1 `options.sync` and B2 transport-loading findings resolved; current result has 0 Blocking, 1 Important, 1 Medium at repository scope; characterization baseline still required |
|
|
43
|
+
|
|
44
|
+
## Applicability
|
|
45
|
+
|
|
46
|
+
| Condition ID | Status | Evidence or reason | Owned section |
|
|
47
|
+
| ------------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
48
|
+
| `module.has_tiers` | N/A | The repository assigns no operational or review tiers | Tier |
|
|
49
|
+
| `module.has_ui` | N/A | No components or rendering | UI use-case flow |
|
|
50
|
+
| `module.crosses_service_boundaries` | Applicable | This module performs the network I/O, in `src/request/request.js` and `src/request/request.shim.js` | Cross-boundary use-case flow |
|
|
51
|
+
| `module.holds_client_state` | N/A | Emitters and options are per-request and discarded with the request | Client state model |
|
|
52
|
+
| `module.enforces_domain_rules` | N/A | Payloads are opaque; no domain entities or invariants | Business rules and invariants |
|
|
53
|
+
| `module.is_concurrent_async` | Applicable | Promise-based dispatch with an interceptor fold in `src/request/utils.ts` and streaming progress events | Concurrency and reactive flow |
|
|
54
|
+
| `module.owns_persistence` | N/A | No store, schema, or migration | Data, schema, and migration |
|
|
55
|
+
| `module.stateful_transitions` | N/A | A request is a single dispatch, not a lifecycle with states | State machine |
|
|
56
|
+
| `module.exposes_wire_protocol` | N/A | Speaks standard HTTP; defines no protocol or binary format of its own | Protocol and wire format |
|
|
57
|
+
| `module.ui_multi_screen` | N/A | Gated by module.has_ui, which is N/A | UI flow |
|
|
58
|
+
| `module.large_data_model` | N/A | Gated by module.owns_persistence, which is N/A | Data model |
|
|
59
|
+
| `module.returns_caller_errors` | Applicable | Both transports normalize failures into a statusCode: 0 response the parent converts into an error | Caller-visible failure modes |
|
|
60
|
+
| `module.module_specific_conventions` | Applicable | The browser field in `package.json` swaps transports at build time, so changes must be mirrored | Module-specific rules |
|
|
61
|
+
| `module.published_package` | N/A | Not published independently; it ships inside the parent package | Export stability |
|
|
62
|
+
| `module.embedded_in_host` | N/A | Not mounted into a host application | Host integration and theming |
|
|
63
|
+
| `module.has_design_tradeoff` | Applicable | Failure-as-response normalization and the build-time transport swap are both non-obvious | Key design trade-off |
|
|
64
|
+
| `module.has_submodules` | N/A | No child modules; computed from the manifest module tree | Sub-modules |
|
|
65
|
+
|
|
66
|
+
## Evidence register
|
|
67
|
+
|
|
68
|
+
| Evidence | What it establishes |
|
|
69
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| `src/request/index.js` | Option normalization, emitter creation, the two interceptor passes, and custom-transport dispatch |
|
|
71
|
+
| `src/request/utils.ts` | The interceptor fold, its error routing, and fetch-option preparation |
|
|
72
|
+
| `src/request/request.js` | The Node transport: option preparation, failure normalization, and download progress |
|
|
73
|
+
| `src/request/request.shim.js` | The browser transport: option translation onto XHR, auth, payloads, and logging |
|
|
74
|
+
| `package.json` | The build-time transport swap and every dependency the transports use |
|
|
75
|
+
| `test/unit/spec/request/index.js` | Option normalization, emitter creation, both interceptor passes, and custom-transport dispatch |
|
|
76
|
+
| `test/unit/spec/request/utils.js` | Interceptor folding and three `prepareFetchOptions` header and body cases |
|
|
77
|
+
| `test/unit/spec/request/request.shim.js` | Basic auth header construction |
|
|
78
|
+
| `test/integration/spec/request.js` | End-to-end behavior across methods, payload forms, auth, cookies, progress, and network failure |
|
|
79
|
+
|
|
80
|
+
The `Configuration surface` subsection under `Public surface` is a repo-specific extension. It is
|
|
81
|
+
retained because this module's entire configuration surface is per-request option keys rather than
|
|
82
|
+
environment variables or a config file, and those keys behave differently on each platform — a fact
|
|
83
|
+
scattered across `src/request/request.js`, `src/request/request.shim.js`, and `src/lib/xhr.js` with
|
|
84
|
+
no canonical home in the source template. It adds no claim not already evidenced by those three
|
|
85
|
+
files and duplicates no other section's content.
|
|
86
|
+
|
|
87
|
+
## Purpose and boundary
|
|
88
|
+
|
|
89
|
+
- **Responsibility:** turn a prepared options object into a response. That means folding the
|
|
90
|
+
interceptor chain over it in both directions, choosing and invoking a transport, and normalizing
|
|
91
|
+
whatever comes back into a uniform response shape.
|
|
92
|
+
- **In scope:** interceptor application order and error routing; per-request emitter creation;
|
|
93
|
+
`options.url` to `options.uri` normalization; the Node transport over the `request` library; the
|
|
94
|
+
browser transport over the vendored XHR fork; the option-vocabulary translation the browser
|
|
95
|
+
transport performs; failure normalization to `statusCode: 0`; progress emission; fetch-option
|
|
96
|
+
preparation for the metrics path.
|
|
97
|
+
- **Out of scope:** the public export surface, default option values, the `HttpError` taxonomy, the
|
|
98
|
+
`Interceptor` base class, and status-to-error conversion — all owned by the parent module. The
|
|
99
|
+
vendored XHR fork itself is owned by the parent as `http-core-xhr-fork`; this module consumes it.
|
|
100
|
+
- **Consumers:** the parent module only. Nothing outside the package imports `src/request` directly.
|
|
101
|
+
|
|
102
|
+
## Structure and key files
|
|
103
|
+
|
|
104
|
+
| Path | Responsibility |
|
|
105
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `src/request/index.js` | The pipeline. Normalizes options, creates the progress emitters, runs request interceptors, dispatches, runs response interceptors in reverse |
|
|
107
|
+
| `src/request/utils.ts` | `intercept()`, the fold that applies one interceptor phase; and `prepareFetchOptions()`, which shapes options for `fetch` without sending |
|
|
108
|
+
| `src/request/request.js` | Node transport. Delegates to the `request` library and normalizes its callback into a resolved promise |
|
|
109
|
+
| `src/request/request.shim.js` | Browser transport. Reimplements the same option vocabulary over `XMLHttpRequest` via the vendored fork |
|
|
110
|
+
|
|
111
|
+
`src/request/utils.ts` is the module's only TypeScript file; everything else is JavaScript.
|
|
112
|
+
|
|
113
|
+
## Public surface
|
|
114
|
+
|
|
115
|
+
| Surface | Contract | Consumer | Compatibility commitment | Source |
|
|
116
|
+
| --- | --- | --- | --- | --- |
|
|
117
|
+
| `default` (the pipeline) | `http-core-request-transport` | Parent module only | Internal. Takes an options object, returns a promise for a response | `src/request/index.js` |
|
|
118
|
+
| `intercept` | `http-core-request-transport` | Parent module, own transports | Internal. `(options, interceptors, key, res?)` folds one interceptor phase | `src/request/utils.ts` |
|
|
119
|
+
| `prepareFetchOptions` | `http-core-request-transport` | Parent module | Internal. Returns the same mutated options object, fetch-ready | `src/request/utils.ts` |
|
|
120
|
+
| `default` (transports) | `http-core-request-transport` | The pipeline only | Internal. Both transports must resolve — never reject — and must accept the same option vocabulary | `src/request/request.js`, `src/request/request.shim.js` |
|
|
121
|
+
| `options.request` hook | `http-core-request-transport` | Any caller | **Caller-visible.** Supplying it replaces the built-in transport entirely while the interceptor chain still runs | `src/request/index.js` |
|
|
122
|
+
|
|
123
|
+
Everything here is internal except the `options.request` hook, which is reachable by any caller of
|
|
124
|
+
the parent's `request` export and is therefore a published behavior. Every row routes to
|
|
125
|
+
`http-core-request-transport` in `contract_catalog.definitions`, the single contract this module
|
|
126
|
+
provides. It requires `http-core-sdk`, `http-core-progress-events`, `http-core-xhr-fork`,
|
|
127
|
+
`webex-common-sdk`, and `nodejs-request-library`, all recorded in the Dependencies table below. The
|
|
128
|
+
repository-wide index of the same ids is
|
|
129
|
+
[Public and consumer surfaces](../../../docs/architecture.md#public-and-consumer-surfaces).
|
|
130
|
+
|
|
131
|
+
### Configuration surface
|
|
132
|
+
|
|
133
|
+
The module reads no environment variables and owns no config file. Its entire configuration surface
|
|
134
|
+
is per-request option keys, and there are no rollout or feature flags gating any of its behavior.
|
|
135
|
+
|
|
136
|
+
| Option | Platform | Effect | Default |
|
|
137
|
+
| --- | --- | --- | --- |
|
|
138
|
+
| `options.timeout` | Both | Node passes it to the underlying client; the browser starts a timer that aborts the request and raises `ETIMEDOUT` | `0` in the browser, meaning no timeout |
|
|
139
|
+
| `options.withCredentials` | Both | Becomes a cookie jar in Node and the XHR credentials flag in the browser | `false`, set explicitly so it is not inferred from CORS |
|
|
140
|
+
| `options.jar` | Both | The alias callers actually use for the same cookie behavior | unset |
|
|
141
|
+
| `options.cors` | Browser | Enables cross-origin mode on the XHR wrapper | `true` |
|
|
142
|
+
| `options.responseType` | Both | `buffer` and `blob` switch the response to binary handling | unset |
|
|
143
|
+
| `options.request` | Both | Replaces the transport entirely | unset |
|
|
144
|
+
| `options.interceptors` | Both | The chain folded around the dispatch | supplied by the parent module |
|
|
145
|
+
|
|
146
|
+
## Dependencies
|
|
147
|
+
|
|
148
|
+
| Dependency | Why it is required | Failure behavior |
|
|
149
|
+
| ---------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
150
|
+
| `request` (npm) | The Node transport | Its callback error is caught and normalized into a `statusCode: 0` response |
|
|
151
|
+
| `src/lib/xhr.js` (`http-core-xhr-fork`) | The browser transport's XHR wrapper | Its error callback produces a `statusCode: 0` response |
|
|
152
|
+
| `src/lib/interceptor.js` (`http-core-interceptor-extension`) | Type and `logOptions` contract for the fold in `utils.ts` | Compile-time dependency in TypeScript; `logOptions` is called per interceptor |
|
|
153
|
+
| `src/progress-event.js` (`http-core-progress-events`) | The payload emitted for download progress | Emitted on the caller's emitter; no effect on the response promise |
|
|
154
|
+
| `src/lib/detect.js` | MIME detection for buffer and blob request bodies. Awaited before dispatch by the Node transport only; the browser call is not awaited, see `MOD-024` | Falls back to `application/octet-stream` rather than failing |
|
|
155
|
+
| `@webex/common` (`isBuffer`) | Buffer detection for request and response bodies in the Node transport | Load-time dependency |
|
|
156
|
+
| `qs` | Query-string and urlencoded-form serialization in the browser | Load-time dependency |
|
|
157
|
+
| `safe-buffer` | Reconstructs buffers the `request` library returns in a degraded form | Load-time dependency |
|
|
158
|
+
| `events` (`EventEmitter`) | The per-request download and upload emitters | Node built-in, polyfilled by bundlers for the browser |
|
|
159
|
+
|
|
160
|
+
## Requirements
|
|
161
|
+
|
|
162
|
+
| ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence |
|
|
163
|
+
| --------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------- | ---------- |
|
|
164
|
+
| `MOD-001` | `options.url` is copied to `options.uri` and then set to `null` | Callers may use either name; the transports read only `uri` | `src/request/index.js` | `test/unit/spec/request/index.js` | none | Present |
|
|
165
|
+
| `MOD-002` | `options.headers` defaults to an empty object | Downstream code assigns into it unconditionally and would throw on `undefined` | `src/request/index.js` | `test/unit/spec/request/index.js` | none | Present |
|
|
166
|
+
| `MOD-003` | A fresh `download` and `upload` `EventEmitter` is created on every request | Progress must be scoped to one request; a shared emitter would cross-report between concurrent requests | `src/request/index.js` | `test/unit/spec/request/index.js` | none | Present |
|
|
167
|
+
| `MOD-004` | Request interceptors run in array order before dispatch; response interceptors run in reverse order after it | Gives interceptors proper nesting, so the outermost sees the request first and the response last | `src/request/index.js` | `test/unit/spec/request/index.js` | none | Present |
|
|
168
|
+
| `MOD-005` | A caller-supplied `options.request` replaces the built-in transport, while the interceptor chain still runs | Lets a consumer route through a channel this package does not know about, such as an iframe `postMessage` | `src/request/index.js` | `test/unit/spec/request/index.js` | none | Present |
|
|
169
|
+
| `MOD-006` | `intercept()` folds a phase over the chain, routing a rejection to the next interceptor's `on<Key>Error` handler | An interceptor can observe and recover from a failure raised by an earlier one | `src/request/utils.ts` | `test/unit/spec/request/utils.js` | Gap: the rejection-routing branch itself is not directly asserted | Present |
|
|
170
|
+
| `MOD-007` | `intercept()` calls `logOptions` on every interceptor in both phases | Verbose network logging must show the options as each interceptor sees them | `src/request/utils.ts` | `test/integration/spec/interceptor.js` | none | Present |
|
|
171
|
+
| `MOD-008` | `prepareFetchOptions` sets `accept` and `content-type` to `application/json` without overriding headers the caller set | Caller intent wins; the default only fills a gap | `src/request/utils.ts` | `test/unit/spec/request/utils.js` | none | Present |
|
|
172
|
+
| `MOD-009` | `prepareFetchOptions` skips the `content-type` header and body serialization for `GET` and `HEAD` | Those methods carry no body, and sending the header would misrepresent the request | `src/request/utils.ts` | none found | Gap: the existing tests pass no `method`, so this branch is never taken | Present |
|
|
173
|
+
| `MOD-010` | `prepareFetchOptions` sets `keepalive: true` and creates the two emitters | Metrics submissions must survive page unload, which `keepalive` enables | `src/request/utils.ts` | `test/unit/spec/request/utils.js` | none | Present |
|
|
174
|
+
| `MOD-011` | The Node transport sets `encoding: null` when `responseType` is `buffer` or `blob` | The `request` library otherwise returns a decoded string and corrupts binary payloads | `src/request/request.js` | `test/integration/spec/request.js` | none | Present |
|
|
175
|
+
| `MOD-012` | The Node transport maps `withCredentials` to the library's `jar` option | Keeps one cookie option name across both platforms | `src/request/request.js` | `test/integration/spec/request.js` | none | Present |
|
|
176
|
+
| `MOD-013` | The Node transport detects and sets `content-type` for a buffer request body before sending | A buffer carries no type, and sending the wrong one causes the service to reject the upload | `src/request/request.js` | `test/integration/spec/request.js` | none | Present |
|
|
177
|
+
| `MOD-014` | Both transports **resolve** a synthetic `{statusCode: 0, options, headers, method, url, body: error}` on transport failure | Keeps rejection in one place — the parent's status interceptor — so every failure flows the same path | `src/request/request.js`, `src/request/request.shim.js` | `test/integration/spec/request.js` | Gap: the browser branch is untested and the test file says so explicitly | Present |
|
|
178
|
+
| `MOD-015` | The Node transport emits `progress` on `options.download` per response chunk, sized from `content-length` | Callers downloading large payloads need incremental progress | `src/request/request.js` | `test/integration/spec/request.js` | none | Present |
|
|
179
|
+
| `MOD-016` | The browser transport builds `Authorization: Bearer …` from `options.auth.bearer`, or `Basic …` via `btoa` from user and pass | The `request` library does this natively in Node, so the browser must reproduce it to keep one API | `src/request/request.shim.js` | `test/unit/spec/request/request.shim.js`, `test/integration/spec/request.js` | none | Present |
|
|
180
|
+
| `MOD-017` | The browser transport defaults `cors: true`, `withCredentials: false`, and `timeout: 0` | The XHR wrapper would otherwise imply credentials from `cors`, sending cookies the caller never requested | `src/request/request.shim.js` | none found | Gap: the defaulting logic is untested despite its security relevance | Present |
|
|
181
|
+
| `MOD-018` | The browser transport maps `responseType: 'buffer'` to `'arraybuffer'` | `buffer` is this package's cross-platform name; the browser only knows `arraybuffer` | `src/request/request.shim.js` | `test/integration/spec/request.js` | none | Present |
|
|
182
|
+
| `MOD-019` | The browser transport serializes `qs` onto the URI, `form` as urlencoded, and `formData` as `FormData`, converting `ArrayBuffer` entries to `Blob` | Reproduces the `request` library's payload vocabulary on top of XHR | `src/request/request.shim.js` | `test/integration/spec/request.js` | none | Present |
|
|
183
|
+
| `MOD-020` | The browser transport binds upload progress only for `PATCH`, `POST`, and `PUT` | Only those methods carry a body worth reporting progress for | `src/request/request.shim.js` | none found | Gap: neither the bound nor the unbound case is tested | Present |
|
|
184
|
+
| `MOD-021` | The browser transport attempts a JSON parse of the response body when `params.json` was not set | The XHR wrapper will not deserialize without that flag, and services reply with JSON regardless | `src/request/request.shim.js` | none found | Gap: untested, including the swallowed parse failure | Present |
|
|
185
|
+
| `MOD-022` | The browser transport logs the request at `debug`, a `>= 400` result with its body at `warn`, and a success at `debug` | Gives operators a failure signal without logging every successful body | `src/request/request.shim.js` | none found | Gap: log levels are unasserted | Present |
|
|
186
|
+
| `MOD-023` | The transport file is selected at build time by the `browser` field, not by a runtime check | Bundlers exclude the Node transport and the `request` library from browser builds entirely | `package.json` | none found | Gap: no test asserts the mapping stays correct | Present |
|
|
187
|
+
| `MOD-024` | The browser transport's `setContentType` is `async` but is called without `await`, so for a `Blob` or `ArrayBuffer` body only the synchronous `json: false` assignment lands before dispatch; the detected `content-type` header is written after `xhr()` has already been invoked | Not deliberate. The Node transport awaits the same detection before dispatch (`MOD-013`), so the platforms disagree; recorded as current behavior rather than as intent | `src/request/request.shim.js` | none found | Defect: a buffer or blob upload from the browser can leave without the detected `content-type`. Neither branch is tested | Present |
|
|
188
|
+
|
|
189
|
+
## Design overview
|
|
190
|
+
|
|
191
|
+
The pipeline is deliberately thin. `src/request/index.js` is about thirty lines and does four
|
|
192
|
+
things: normalize two option aliases, attach the emitters, and run the interceptor chain on each
|
|
193
|
+
side of a single dispatch call. Everything genuinely difficult lives either in `utils.ts` or in the
|
|
194
|
+
transports.
|
|
195
|
+
|
|
196
|
+
**The fold is the whole interceptor mechanism.** `intercept()` in `src/request/utils.ts` reduces the
|
|
197
|
+
interceptor array into a promise chain, attaching both a success and a failure handler at each step.
|
|
198
|
+
That single structure produces the behavior callers rely on: handlers run in sequence, each one sees
|
|
199
|
+
the previous one's output, and a rejection anywhere skips forward to the next `on<Key>Error` handler
|
|
200
|
+
rather than abandoning the chain. Reverse order for the response phase is achieved not in the fold
|
|
201
|
+
but at the call site, with `options.interceptors.slice().reverse()` — the copy matters, because
|
|
202
|
+
reversing in place would corrupt the caller's array for the next request.
|
|
203
|
+
|
|
204
|
+
**The two transports are the same contract implemented twice.** Both accept the option vocabulary of
|
|
205
|
+
the `request` library and both must resolve rather than reject. In Node that is nearly free, since
|
|
206
|
+
the library already speaks that vocabulary — the transport mostly translates a callback into a
|
|
207
|
+
promise and patches up buffers the library returns in a degraded form. In the browser none of it is
|
|
208
|
+
free: `setAuth`, `setCookies`, `setQs`, `setPayload`, `setResponseType`, and `setDefaults` in
|
|
209
|
+
`src/request/request.shim.js` exist purely to rebuild that vocabulary on top of XHR. This asymmetry
|
|
210
|
+
is the module's main maintenance burden and the reason parity is its central constraint.
|
|
211
|
+
|
|
212
|
+
**`prepareFetchOptions` is a third, partial path.** It shares the request-interceptor phase and the
|
|
213
|
+
emitter setup with the main pipeline, then stops short of sending. It exists for metrics submission,
|
|
214
|
+
where options are built at one moment and dispatched at another. It notably does not share the
|
|
215
|
+
response phase, so nothing on that path converts a status into an error.
|
|
216
|
+
|
|
217
|
+
## Data flow and sequence coverage
|
|
218
|
+
|
|
219
|
+
The transport is HTTP. Three operation groups differ in actors, ordering, and outcome, so each is
|
|
220
|
+
diagrammed separately: the standard dispatch, a failure dispatch, and the prepare-only path.
|
|
221
|
+
|
|
222
|
+
| Operation group | Entry and outcome | Diagram or evidence | Failure and recovery coverage |
|
|
223
|
+
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
224
|
+
| Standard dispatch | `request(options)` → resolved response after both interceptor phases | Diagram below | Interceptor rejection routed to `onRequestError`/`onResponseError` |
|
|
225
|
+
| Failure dispatch | Transport error → resolved `statusCode: 0` response, not a rejection | Diagram below | Covers Node and browser transport errors, plus the browser timeout path |
|
|
226
|
+
| Prepared fetch options | `prepareFetchOptions(options)` → mutated options, nothing sent | `src/request/utils.ts`, `test/unit/spec/request/utils.js` | Request-phase rejection propagates; no response phase exists on this path |
|
|
227
|
+
|
|
228
|
+
**Standard dispatch**
|
|
229
|
+
|
|
230
|
+
```mermaid
|
|
231
|
+
sequenceDiagram
|
|
232
|
+
participant P as Parent module
|
|
233
|
+
participant X as src/request/index.js
|
|
234
|
+
participant U as src/request/utils.ts
|
|
235
|
+
participant T as transport
|
|
236
|
+
P->>X: _request(options)
|
|
237
|
+
X->>X: url to uri, default headers, create emitters
|
|
238
|
+
X->>U: intercept(options, interceptors, "Request")
|
|
239
|
+
loop each interceptor, forward order
|
|
240
|
+
U->>U: logOptions then onRequest
|
|
241
|
+
end
|
|
242
|
+
U-->>X: options
|
|
243
|
+
alt options.request supplied
|
|
244
|
+
X->>P: options.request(options)
|
|
245
|
+
else built-in transport
|
|
246
|
+
X->>T: _request(options)
|
|
247
|
+
T-->>X: response
|
|
248
|
+
end
|
|
249
|
+
X->>U: intercept(options, reversed, "Response", response)
|
|
250
|
+
loop each interceptor, reverse order
|
|
251
|
+
U->>U: logOptions then onResponse
|
|
252
|
+
end
|
|
253
|
+
U-->>P: response
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Failure dispatch**
|
|
257
|
+
|
|
258
|
+
```mermaid
|
|
259
|
+
sequenceDiagram
|
|
260
|
+
participant X as src/request/index.js
|
|
261
|
+
participant T as transport
|
|
262
|
+
participant N as network
|
|
263
|
+
participant U as src/request/utils.ts
|
|
264
|
+
X->>T: _request(options)
|
|
265
|
+
T->>N: send
|
|
266
|
+
alt network or CORS failure
|
|
267
|
+
N-->>T: error
|
|
268
|
+
T->>T: log warn
|
|
269
|
+
T-->>X: resolve {statusCode: 0, body: error}
|
|
270
|
+
else browser timeout elapsed
|
|
271
|
+
T->>T: abort, build ETIMEDOUT error
|
|
272
|
+
T-->>X: resolve {statusCode: 0, body: error}
|
|
273
|
+
else success
|
|
274
|
+
N-->>T: response
|
|
275
|
+
T-->>X: resolve response
|
|
276
|
+
end
|
|
277
|
+
X->>U: response interceptor phase (runs either way)
|
|
278
|
+
U-->>X: parent converts statusCode 0 to NetworkOrCORSError
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The second diagram is the one that explains the module's most surprising behavior: the response
|
|
282
|
+
interceptor phase runs identically whether the request succeeded or the network was unreachable.
|
|
283
|
+
|
|
284
|
+
## Class and component relationships
|
|
285
|
+
|
|
286
|
+
```mermaid
|
|
287
|
+
classDiagram
|
|
288
|
+
class Pipeline["src/request/index.js"] {
|
|
289
|
+
+default(options) Promise
|
|
290
|
+
}
|
|
291
|
+
class Utils["src/request/utils.ts"] {
|
|
292
|
+
+intercept(options, interceptors, key, res) Promise
|
|
293
|
+
+prepareFetchOptions(options) Promise
|
|
294
|
+
}
|
|
295
|
+
class NodeTransport["src/request/request.js"] {
|
|
296
|
+
-prepareOptions(options)
|
|
297
|
+
-doRequest(options)
|
|
298
|
+
+default(options) Promise
|
|
299
|
+
}
|
|
300
|
+
class BrowserTransport["src/request/request.shim.js"] {
|
|
301
|
+
-setAuth(params, o)
|
|
302
|
+
-setCookies(params, o)
|
|
303
|
+
-setQs(params, o)
|
|
304
|
+
-setPayload(params, o)
|
|
305
|
+
-setDefaults(params, o)
|
|
306
|
+
-processResponseJson(response, params)
|
|
307
|
+
+default(options) Promise
|
|
308
|
+
}
|
|
309
|
+
class Interceptor
|
|
310
|
+
class ProgressEvent
|
|
311
|
+
Pipeline --> Utils : folds interceptor phases
|
|
312
|
+
Pipeline --> NodeTransport : node build
|
|
313
|
+
Pipeline --> BrowserTransport : browser build
|
|
314
|
+
Utils ..> Interceptor : invokes hooks
|
|
315
|
+
NodeTransport --> ProgressEvent : emits download progress
|
|
316
|
+
BrowserTransport ..> ProgressEvent : forwards XHR progress
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`NodeTransport` and `BrowserTransport` are never both present in one bundle — the `browser` field in
|
|
320
|
+
`package.json` substitutes one for the other at build time.
|
|
321
|
+
|
|
322
|
+
## Use cases and flows
|
|
323
|
+
|
|
324
|
+
| Use case | Actor or caller | Primary steps and outcome | Failure or boundary behavior | Evidence |
|
|
325
|
+
| -------- | --------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
326
|
+
| `UC-001` | Parent module | Dispatch a JSON `GET`; both interceptor phases run and a parsed response is returned | A `>= 400` status still returns here; the parent converts it | `src/request/index.js`, `test/integration/spec/request.js` |
|
|
327
|
+
| `UC-002` | Caller uploading | `POST` a file via `formData`; `ArrayBuffer` entries become `Blob`s and upload progress is emitted | Progress binds only for `PATCH`, `POST`, `PUT` | `src/request/request.shim.js`, `test/integration/spec/request.js` |
|
|
328
|
+
| `UC-003` | Caller downloading | `GET` with `responseType: 'buffer'`; the body arrives as a buffer and download progress is emitted per chunk | Progress is unmeasurable when `content-length` is absent — `lengthComputable` is `false` | `src/request/request.js`, `test/integration/spec/request.js` |
|
|
329
|
+
| `UC-004` | Any caller | The host is unreachable; a `statusCode: 0` response is resolved and the parent rejects with `NetworkOrCORSError` | No rejection originates in this module | `src/request/request.js`, `test/integration/spec/request.js` |
|
|
330
|
+
| `UC-005` | Caller with own transport | Supply `options.request`; interceptors run and the custom function performs the exchange | The built-in transport is still imported and loaded by `src/request/index.js`; it is simply never invoked | `src/request/index.js`, `test/unit/spec/request/index.js` |
|
|
331
|
+
| `UC-006` | Metrics submission | Call `prepareFetchOptions` to build options now, send them later | No response phase, so a `4xx` is never converted to an error on this path | `src/request/utils.ts`, `test/unit/spec/request/utils.js` |
|
|
332
|
+
| `UC-007` | Authenticated caller | Pass `options.auth.bearer`; the transport builds the `Authorization` header | With user and pass instead, a base64 `Basic` header is built | `src/request/request.shim.js`, `test/unit/spec/request/request.shim.js` |
|
|
333
|
+
|
|
334
|
+
### Cross-boundary use-case flow
|
|
335
|
+
|
|
336
|
+
| Boundary | Transport | Ordering | Compatibility | Timeout and retry | Recovery |
|
|
337
|
+
| ---------------------------------- | -------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
338
|
+
| Node process → HTTP service | `request` library over Node HTTP | One dispatch per call; progress events interleave with the response | Option vocabulary is the library's own | `options.timeout` passes through; no retry | Errors become a `statusCode: 0` response after a `warn` log |
|
|
339
|
+
| Browser → HTTP service | `XMLHttpRequest` via the vendored fork | Same, plus an upload-progress stream for body-carrying methods | Vocabulary is reimplemented by hand and must track the Node side | `options.timeout` starts a timer that aborts and raises `ETIMEDOUT`; default is `0` (none) | Errors and timeouts both become a `statusCode: 0` response |
|
|
340
|
+
| Either → caller-supplied transport | Whatever `options.request` implements | Interceptor phases still bracket the call | The caller owns the contract; this module makes no guarantees about its result | Entirely caller-owned | Entirely caller-owned |
|
|
341
|
+
|
|
342
|
+
Cookie handling is the sharpest compatibility edge: `options.jar` means a cookie jar in Node and
|
|
343
|
+
`withCredentials` in the browser. They are not equivalent — the browser flag is subject to CORS
|
|
344
|
+
rules the Node jar is not.
|
|
345
|
+
|
|
346
|
+
## Concurrency and reactive flow
|
|
347
|
+
|
|
348
|
+
- **Execution model:** promise-based, on the host event loop. The Node transport wraps a callback
|
|
349
|
+
API in a `Promise`; the browser transport wraps XHR event handlers the same way. Progress arrives
|
|
350
|
+
as `EventEmitter` events, independent of the response promise.
|
|
351
|
+
- **Ordering guarantees:** interceptors are strictly sequential within a phase — forward for the
|
|
352
|
+
request, reverse for the response. Progress events carry no ordering guarantee relative to the
|
|
353
|
+
response promise and may still be emitted as it settles. Across requests there is no ordering.
|
|
354
|
+
- **Idempotency and retry:** neither is implemented. Every call performs exactly one dispatch. The
|
|
355
|
+
browser transport's `cbOnce` guard ensures the callback fires once even if both `onload` and
|
|
356
|
+
`onreadystatechange` trigger, which is a single-delivery guarantee rather than deduplication.
|
|
357
|
+
- **Shared-state protection:** none is needed; each request owns its options and emitters. The one
|
|
358
|
+
hazard is the interceptor array, which belongs to the caller — hence the defensive `.slice()`
|
|
359
|
+
before reversing in `src/request/index.js`.
|
|
360
|
+
- **Blocking restrictions:** interceptor hooks are awaited in sequence, so a slow hook delays the
|
|
361
|
+
request. Browser requests are always asynchronous: the vendored fork supports a synchronous mode
|
|
362
|
+
and guards its timeout path behind that flag, but this transport never forwards the option, so the
|
|
363
|
+
timeout path is always reachable. See Pitfalls and constraints.
|
|
364
|
+
|
|
365
|
+
## Caller-visible failure modes
|
|
366
|
+
|
|
367
|
+
| Condition | Signal or result | Caller behavior | Retry or recovery | Evidence |
|
|
368
|
+
| ------------------------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------- |
|
|
369
|
+
| Network unreachable, DNS failure, or CORS | Resolved `{statusCode: 0, body: <error>}`; parent rejects `NetworkOrCORSError` | Treat as unreachable, not as a server answer | Caller-owned | `src/request/request.js`, `test/integration/spec/request.js` |
|
|
370
|
+
| Browser timeout elapsed | Request aborted; `ETIMEDOUT` error becomes `statusCode: 0` | Same handling as any transport failure | Caller-owned | `src/lib/xhr.js`, `src/request/request.shim.js` |
|
|
371
|
+
| Request interceptor rejects | Rejection routed to the next `onRequestError`; unhandled ones propagate | Handle in a later interceptor or at the call site | Interceptor-owned | `src/request/utils.ts` |
|
|
372
|
+
| Response interceptor rejects | Rejection routed to the next `onResponseError` in reverse order | Same | Interceptor-owned | `src/request/utils.ts` |
|
|
373
|
+
| Custom `options.request` throws or rejects | Propagates; the response phase does not run | Caller owns the contract entirely | Caller-owned | `src/request/index.js` |
|
|
374
|
+
| Response body is not valid JSON (browser) | The parse failure is swallowed and the raw body is kept | Inspect the body type before assuming an object | None needed | `src/request/request.shim.js` |
|
|
375
|
+
| `content-length` absent on a download | Progress still emits, with `lengthComputable: false` | Show indeterminate progress rather than a percentage | None needed | `src/request/request.js`, `src/progress-event.js` |
|
|
376
|
+
|
|
377
|
+
## Pitfalls and constraints
|
|
378
|
+
|
|
379
|
+
- **The two transports must be changed together.** The `browser` field in `package.json` swaps them
|
|
380
|
+
at build time, so a change to one is invisible on the other platform until a user hits it. This is
|
|
381
|
+
the single most likely source of defects in this module.
|
|
382
|
+
- **Neither transport may reject.** Resolving a `statusCode: 0` response is load-bearing: the
|
|
383
|
+
parent's status interceptor is the only place rejections originate. A transport that rejects
|
|
384
|
+
bypasses every `onResponseError` handler in the chain.
|
|
385
|
+
- **`options.interceptors` belongs to the caller.** `src/request/index.js` copies before reversing.
|
|
386
|
+
Removing the `.slice()` would permanently reverse the caller's array.
|
|
387
|
+
- **The options object is mutated throughout.** `url` is nulled, `headers` filled, emitters attached.
|
|
388
|
+
Reusing one options object across requests carries state between them.
|
|
389
|
+
- **`jar` is not the same thing on both platforms.** A cookie jar in Node, `withCredentials` in the
|
|
390
|
+
browser — and the browser form is constrained by CORS in ways the jar is not.
|
|
391
|
+
- **CORS defaults on but credentials default off.** `setDefaults` in `src/request/request.shim.js`
|
|
392
|
+
sets `withCredentials: false` explicitly because the underlying wrapper would otherwise infer it
|
|
393
|
+
from `cors: true` and send cookies the caller never asked to send.
|
|
394
|
+
- **The prepared-fetch path has no response phase.** Nothing converts a status into an error there,
|
|
395
|
+
so a `4xx` submitted via `setTimingsAndFetch` resolves normally.
|
|
396
|
+
- **The vendored fork's synchronous mode is unreachable from here.** `src/lib/xhr.js` implements a
|
|
397
|
+
`sync` option and disables its timeout timer when that option is set, but the browser transport's
|
|
398
|
+
`pick()` in `src/request/request.shim.js` does not copy `sync` into the params it passes, and
|
|
399
|
+
`setDefaults` does not supply it either. A caller setting `options.sync` gets an ordinary
|
|
400
|
+
asynchronous request. Treat that code path in the fork as dead unless the transport is
|
|
401
|
+
deliberately changed to forward the option.
|
|
402
|
+
- **The Node transport rebuilds degraded buffers.** The `request` library sometimes returns objects
|
|
403
|
+
shaped like buffers that are not buffers; the transport reconstructs them. Do not remove that
|
|
404
|
+
branch without confirming the library's current behavior.
|
|
405
|
+
|
|
406
|
+
## Module-specific rules
|
|
407
|
+
|
|
408
|
+
- **Do:** mirror every behavioral change across `src/request/request.js` and
|
|
409
|
+
`src/request/request.shim.js`, and add or extend tests on both sides. Parity is the module's
|
|
410
|
+
primary constraint.
|
|
411
|
+
- **Do:** keep the option vocabulary aligned with the `request` library's names. New options should
|
|
412
|
+
follow its conventions so the two transports can keep one surface.
|
|
413
|
+
- **Do:** resolve a `statusCode: 0` response for any transport-level failure, including new ones.
|
|
414
|
+
- **Do:** copy `options.interceptors` before reordering it.
|
|
415
|
+
- **Do not:** reject from a transport, or convert a status into an error here. That belongs to the
|
|
416
|
+
parent's `HttpStatusInterceptor`.
|
|
417
|
+
- **Do not:** add retry, backoff, or circuit-breaking to this module. It performs exactly one
|
|
418
|
+
dispatch per call by design.
|
|
419
|
+
- **Do not:** import from the parent's entry point. The dependency runs one way, parent to child.
|
|
420
|
+
|
|
421
|
+
## Key design trade-off
|
|
422
|
+
|
|
423
|
+
| Chosen trade-off | Preserved invariant or benefit | Cost or limitation | Decision evidence |
|
|
424
|
+
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
425
|
+
| Transports resolve a synthetic failure response instead of rejecting | Rejection originates in exactly one place, so every failure reaches `onResponseError` handlers uniformly | A resolved promise may carry a failure, so no code may treat "resolved" as "succeeded" | `src/request/request.js`, `src/request/request.shim.js` |
|
|
426
|
+
| Select the transport at build time via the `browser` field rather than at runtime | Browser bundles exclude the Node transport and the `request` library entirely, keeping the shipped bundle smaller | The two implementations can diverge without any test failing on the platform being developed on | `package.json` |
|
|
427
|
+
| Reimplement the `request` option vocabulary on top of XHR | One public API across platforms; callers migrating from that library need no changes | An entire translation layer must be maintained by hand and kept in sync with an unmaintained library's semantics | `src/request/request.shim.js` |
|
|
428
|
+
| Expose `options.request` as a transport escape hatch | A consumer can route over a channel this package does not know about, such as `postMessage` to a parent frame | The result is unvalidated — it bypasses both transports while still flowing through the response interceptor phase | `src/request/index.js` |
|
|
429
|
+
| Reverse the response phase with a copy of the interceptor array | Proper nesting: the outermost interceptor sees the request first and the response last | An array copy per request, and a subtle bug waiting for anyone who removes the `.slice()` | `src/request/index.js` |
|
|
430
|
+
| Share the request phase but not the response phase with `prepareFetchOptions` | Metrics can build a request now and send it later while still getting interceptor-applied headers | Two paths with different failure semantics; the prepared path never produces an `HttpError` | `src/request/utils.ts` |
|
|
431
|
+
|
|
432
|
+
## Verification
|
|
433
|
+
|
|
434
|
+
| Requirement or invariant | Test level | Positive evidence | Negative or boundary evidence | Gap |
|
|
435
|
+
| ------------------------ | ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
436
|
+
| `MOD-001` | Unit + Integration | `test/unit/spec/request/index.js` | `test/integration/spec/request.js` | none |
|
|
437
|
+
| `MOD-002` | Unit | `test/unit/spec/request/index.js` | none found | Pre-supplied headers are not asserted to survive |
|
|
438
|
+
| `MOD-003` | Unit | `test/unit/spec/request/index.js` | none found | none |
|
|
439
|
+
| `MOD-004` | Unit | `test/unit/spec/request/index.js` | none found | Order is inferred from a two-call count, not asserted directly |
|
|
440
|
+
| `MOD-005` | Unit | `test/unit/spec/request/index.js` | `test/unit/spec/request/index.js` (built-in transport not called) | none |
|
|
441
|
+
| `MOD-006` | Unit | `test/unit/spec/request/utils.js` | none found | The error-routing branch is uncovered |
|
|
442
|
+
| `MOD-007` | Integration | `test/integration/spec/interceptor.js` | none found | none |
|
|
443
|
+
| `MOD-008` | Unit | `test/unit/spec/request/utils.js` | `test/unit/spec/request/utils.js` (caller headers preserved) | none |
|
|
444
|
+
| `MOD-009` | — | none found | none found | No test passes a `method`, so the `GET`/`HEAD` skip never executes |
|
|
445
|
+
| `MOD-010` | Unit | `test/unit/spec/request/utils.js` | none found | none |
|
|
446
|
+
| `MOD-011` | Integration | `test/integration/spec/request.js` | none found | Node only; no browser equivalent |
|
|
447
|
+
| `MOD-012` | Integration | `test/integration/spec/request.js` | none found | none |
|
|
448
|
+
| `MOD-013` | Integration | `test/integration/spec/request.js` | none found | The detection failure path is uncovered |
|
|
449
|
+
| `MOD-014` | Integration | `test/integration/spec/request.js` (node only) | none found | The browser branch is untested, as the test file notes in a comment |
|
|
450
|
+
| `MOD-015` | Integration | `test/integration/spec/request.js` | none found | The missing-`content-length` case is uncovered |
|
|
451
|
+
| `MOD-016` | Unit + Integration | `test/unit/spec/request/request.shim.js` (basic), `test/integration/spec/request.js` (bearer and basic) | none found | none |
|
|
452
|
+
| `MOD-017` | — | none found | none found | The explicit `withCredentials: false` default is unasserted despite its security role |
|
|
453
|
+
| `MOD-018` | Integration | `test/integration/spec/request.js` | none found | none |
|
|
454
|
+
| `MOD-019` | Integration | `test/integration/spec/request.js` (qs, form, formData) | none found | `ArrayBuffer`-to-`Blob` conversion is not directly asserted |
|
|
455
|
+
| `MOD-020` | — | none found | none found | Neither the bound nor the unbound method set is tested |
|
|
456
|
+
| `MOD-021` | — | none found | none found | Including the deliberately swallowed parse failure |
|
|
457
|
+
| `MOD-022` | — | none found | none found | Log levels are unasserted |
|
|
458
|
+
| `MOD-023` | — | none found | none found | No test would catch a broken `browser` mapping in `package.json` |
|
|
459
|
+
| `MOD-024` | — | none found | none found | The unawaited call is unasserted, and no test observes the outgoing `content-type` in the browser |
|
|
460
|
+
|
|
461
|
+
**Coverage gaps worth acting on.** The concentration is one-sided: the Node transport is reachable
|
|
462
|
+
by the Jest unit tier and the Mocha integration tier, while most browser-transport behavior is
|
|
463
|
+
verified only by the Karma tier, which runs the same integration specs. Eight requirements have no
|
|
464
|
+
test at all — `MOD-009`, `MOD-017`, and `MOD-020` through `MOD-024` — and all but one are
|
|
465
|
+
browser-side.
|
|
466
|
+
|
|
467
|
+
Three deserve attention beyond the count. `MOD-017` governs whether cookies are sent on a
|
|
468
|
+
cross-origin request, and it is entirely unasserted. `MOD-014`'s browser half is knowingly untested;
|
|
469
|
+
`test/integration/spec/request.js` says so in a comment and suggests moving the error-reformatting
|
|
470
|
+
logic out of the platform-specific implementations to make it testable — a refactor that would
|
|
471
|
+
reduce exactly the parity risk this module's constraints are built around. `MOD-024` is not a
|
|
472
|
+
coverage gap but a defect the absence of coverage hides: `setContentType` in
|
|
473
|
+
`src/request/request.shim.js` is `async` and is called without `await`, so the detected
|
|
474
|
+
`content-type` is assigned after the request has been dispatched. This specification records the
|
|
475
|
+
behavior as it is; correcting it is a transport change and belongs with a browser-tier test.
|
|
476
|
+
|
|
477
|
+
Generator-side field measurement is complete for this module and every critical field is present.
|
|
478
|
+
What remains is not measurement: independent semantic validation by `codex` ran on 2026-09-23 and
|
|
479
|
+
returned `pass-with-warnings`; the prior B1 and B2 findings were resolved and no Blocking finding
|
|
480
|
+
remains. Promotion from `Partial` to `Specced` remains held because a characterization baseline
|
|
481
|
+
should be recorded before any risky modification.
|