Skip to content

Commit 5b31a56

Browse files
committed
RDBC-1083: a few remarks addressed
1 parent ca2fc10 commit 5b31a56

3 files changed

Lines changed: 74 additions & 42 deletions

File tree

src/Http/RavenCommand.ts

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -162,64 +162,81 @@ export abstract class RavenCommand<TResult> {
162162

163163
let optionsToUse: RequestInit | BunFetchRequestInit;
164164

165-
if (RuntimeUtil.isBun()) {
166-
optionsToUse = { body: bodyToUse, ...restOptions } as BunFetchRequestInit;
167-
} else if (RuntimeUtil.isWorkerd()) {
168-
// Cloudflare Workers' fetch has no undici `dispatcher` concept; mTLS is
169-
// handled by the custom fetch (an mtls_certificate binding). Passing a
170-
// dispatcher here is meaningless and can confuse the runtime.
171-
optionsToUse = { body: bodyToUse, ...restOptions } as RequestInit;
172-
} else {
165+
if (RuntimeUtil.supportsUndiciDispatcher()) {
173166
if (dispatcher) { // support for fiddler
174167
agent = dispatcher;
175168
}
176169
optionsToUse = { body: bodyToUse, ...restOptions, dispatcher: agent } as RequestInit;
170+
} else {
171+
// Bun and Cloudflare Workers (workerd) have no undici `dispatcher` concept, so
172+
// one must never leak into their fetch init. On workerd mTLS is handled by the
173+
// custom fetch (an mtls_certificate binding); Bun manages TLS itself.
174+
optionsToUse = { body: bodyToUse, ...restOptions } as RequestInit;
177175
}
178176

179-
let passthrough: PassThrough;
180-
try {
181-
passthrough = new PassThrough();
182-
} catch (err) {
183-
throw RavenCommand._maybeNodeStreamUnavailableError(err as Error);
184-
}
185-
passthrough.pause();
177+
// `new PassThrough()`, `Readable.fromWeb`, `new Stream()` and `.pipe` are all
178+
// `node:stream` APIs; an incomplete edge polyfill (Nitro/unenv) can throw from any
179+
// of them. `_guardNodeStream` turns those into an actionable error. PassThrough is
180+
// built BEFORE the fetch on purpose: on a broken polyfill we fail fast without
181+
// issuing the (possibly state-mutating) request.
182+
const passthrough = RavenCommand._guardNodeStream(() => {
183+
const pt = new PassThrough();
184+
pt.pause();
185+
return pt;
186+
});
186187

187188
const fetchFn = fetcher ?? fetch; // support for custom fetcher
188189
const response = await fetchFn(uri, optionsToUse);
189190

190-
// `Readable.fromWeb` / `new Stream()` / `.pipe` are all `node:stream` APIs,
191-
// so an incomplete edge polyfill (Nitro/unenv) can throw here just like
192-
// `new PassThrough()` above. Guard them so the actionable error is raised.
193-
try {
191+
RavenCommand._guardNodeStream(() => {
194192
const effectiveStream: Stream =
195193
response.body
196194
? Readable.fromWeb(response.body)
197195
: new Stream();
198196

199197
effectiveStream
200198
.pipe(passthrough);
201-
} catch (err) {
202-
throw RavenCommand._maybeNodeStreamUnavailableError(err as Error);
203-
}
199+
});
204200

205201
return {
206202
response,
207203
bodyStream: passthrough
208204
};
209205
}
210206

207+
// Runs a `node:stream` operation, translating an incomplete-polyfill failure into an
208+
// actionable error. Shared by every node:stream call site in send() so none can be
209+
// added without the guard, and the translation logic lives in one place.
210+
private static _guardNodeStream<T>(fn: () => T): T {
211+
try {
212+
return fn();
213+
} catch (err) {
214+
throw RavenCommand._maybeNodeStreamUnavailableError(err as Error);
215+
}
216+
}
217+
211218
private static _maybeNodeStreamUnavailableError(err: Error): Error {
212-
// Some edge bundlers ship an incomplete `node:stream` where PassThrough throws
213-
// "not implemented" (e.g. Nitro/unenv, used by TanStack Start & Nuxt). Turn that
214-
// cryptic stub error into an actionable one pointing at the real fix.
215-
if (RuntimeUtil.isWorkerd()) {
219+
// The failure is signalled by the polyfill itself, not by the runtime: unenv
220+
// (used by Nitro-based presets -- TanStack Start, Nuxt -- and edge targets such as
221+
// Cloudflare Workers, Vercel Edge, Deno Deploy) ships `node:stream` stubs that
222+
// throw e.g. "[unenv] PassThrough is not implemented yet!". We match that signature
223+
// rather than the runtime so (a) every affected runtime gets the guidance, not just
224+
// workerd, and (b) an unrelated node:stream error on a correctly configured runtime
225+
// is NOT mislabeled as a polyfill problem. The original error is kept as the cause.
226+
const message = err?.message ?? "";
227+
const isIncompletePolyfill = message.includes("[unenv]")
228+
|| /\bnot implemented\b/i.test(message);
229+
230+
if (isIncompletePolyfill) {
216231
return getError(
217232
"InvalidOperationException",
218-
"The RavenDB client requires a working node:stream on Cloudflare Workers, but this "
219-
+ "runtime provided an incomplete polyfill. Enable Node.js compatibility by adding "
220-
+ `compatibility_flags = ["nodejs_compat"] (and a recent compatibility_date) to your `
221-
+ "wrangler configuration. For framework bundlers such as Nitro / TanStack Start / Nuxt "
222-
+ "this makes node: built-ins resolve to the workerd runtime instead of unenv stubs.",
233+
"The RavenDB client requires a working node:stream, but this runtime provided an "
234+
+ "incomplete polyfill (unenv). On Cloudflare Workers, enable Node.js compatibility by "
235+
+ `adding compatibility_flags = ["nodejs_compat"] (and a recent compatibility_date) to `
236+
+ "your wrangler configuration; for framework bundlers such as Nitro / TanStack Start / "
237+
+ "Nuxt this makes node: built-ins resolve to the runtime instead of unenv stubs. On "
238+
+ "other edge runtimes (e.g. Vercel Edge, Deno Deploy) enable the equivalent Node.js "
239+
+ "compatibility so node:stream is backed by a real implementation.",
223240
err);
224241
}
225242
return err;

src/Http/RequestExecutor.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -340,17 +340,14 @@ export class RequestExecutor implements IDisposable {
340340
return null;
341341
}
342342

343-
if (RuntimeUtil.isBun()) {
344-
return null;
345-
}
346-
347-
if (RuntimeUtil.isWorkerd()) {
348-
// Cloudflare Workers has no undici Agent / Node https agent.
349-
// mTLS is provided via conventions.customFetch (an mtls_certificate binding).
350-
// If a certificate was configured (the Node way) but no customFetch was set
351-
// (checked above), the cert can never be presented -- fail fast with an
352-
// actionable message instead of silently issuing an uncertified request.
353-
if (this._certificate) {
343+
// Runtimes whose fetch has no undici dispatcher (Bun, Cloudflare Workers) can't use
344+
// an http agent -- there is nothing to build.
345+
if (!RuntimeUtil.supportsUndiciDispatcher()) {
346+
// On workerd a client certificate configured the Node way can never be presented
347+
// (no Node https agent) unless mTLS is wired through conventions.customFetch
348+
// (checked above). Fail fast with an actionable message instead of silently
349+
// issuing an uncertified request.
350+
if (RuntimeUtil.isWorkerd() && this._certificate) {
354351
throwError("InvalidOperationException",
355352
"A client certificate was configured via authOptions, but Cloudflare Workers has no Node "
356353
+ "https agent to present it. On workerd, X.509 mTLS must be provided through a Cloudflare "

src/Utility/RuntimeUtil.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export class RuntimeUtil {
1010
*/
1111
public static isBun(): boolean {
1212
if (this._isBun === null) {
13+
// `process` is absent on some edge runtimes (e.g. Cloudflare Workers/workerd),
14+
// where a bare reference would throw a ReferenceError at module-load time and
15+
// crash the very load path this detection protects. Keep the `typeof` guard --
16+
// do not "simplify" it away. See isWorkerd().
1317
this._isBun = typeof process !== "undefined" && !!process.versions?.bun;
1418
}
1519
return this._isBun;
@@ -32,4 +36,18 @@ export class RuntimeUtil {
3236
}
3337
return this._isWorkerd;
3438
}
39+
40+
/**
41+
* Whether the current runtime can accept an undici `Dispatcher` (Node's undici
42+
* Agent) in a `fetch` init. Node can; Bun and Cloudflare Workers (workerd) cannot --
43+
* their `fetch` has no dispatcher concept, so building or passing one is meaningless.
44+
*
45+
* Centralized here so the two consumers -- request send (RavenCommand.send) and http
46+
* agent creation (RequestExecutor.getHttpAgent) -- stay in lockstep: if they drift
47+
* (an agent is built but the dispatcher is dropped on send, or vice versa) the
48+
* original workerd failure returns. A future dispatcher-less runtime is added once.
49+
*/
50+
public static supportsUndiciDispatcher(): boolean {
51+
return !this.isBun() && !this.isWorkerd();
52+
}
3553
}

0 commit comments

Comments
 (0)