@@ -8,6 +8,7 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a crash when undici emits an internal client error while terminating a mid-stream HTTP response ([#6133](https://github.com/earendil-works/pi/issues/6133)).
|
||||
- Fixed the compaction event regression test to cover status indicator cleanup and keep CI passing.
|
||||
- Fixed interactive status indicators so ending work, retry, compaction, or branch-summary indicators no longer shrink the TUI when clear-on-shrink is enabled ([#6026](https://github.com/earendil-works/pi/pull/6026)).
|
||||
- Fixed `--session` and `SessionManager.open()` to reject non-empty invalid session files without overwriting them ([#6002](https://github.com/earendil-works/pi/issues/6002)).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import * as undici from "undici";
|
||||
|
||||
export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000;
|
||||
@@ -46,18 +47,50 @@ export function applyHttpProxySettings(httpProxy: string | undefined): void {
|
||||
process.env.HTTPS_PROXY ??= proxy;
|
||||
}
|
||||
|
||||
const ignoreUndiciDispatcherError = (_error: unknown): void => {};
|
||||
|
||||
// Undici can emit an internal Client "error" while terminating a mid-stream
|
||||
// fetch body. The body stream still rejects through reader.read(); this listener
|
||||
// only prevents EventEmitter's unhandled "error" special case from crashing pi.
|
||||
function withUndiciErrorListener<T extends undici.Dispatcher>(dispatcher: T): T {
|
||||
if (dispatcher instanceof EventEmitter) {
|
||||
EventEmitter.prototype.on.call(dispatcher, "error", ignoreUndiciDispatcherError);
|
||||
}
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
function createUndiciClient(origin: string | URL, options: object): undici.Dispatcher {
|
||||
return withUndiciErrorListener(new undici.Client(origin, options as undici.Client.Options));
|
||||
}
|
||||
|
||||
function createUndiciOriginDispatcher(origin: string | URL, options: object): undici.Dispatcher {
|
||||
const dispatcherOptions = options as undici.Pool.Options;
|
||||
if (dispatcherOptions.connections === 1) {
|
||||
return createUndiciClient(origin, dispatcherOptions);
|
||||
}
|
||||
return withUndiciErrorListener(
|
||||
new undici.Pool(origin, {
|
||||
...dispatcherOptions,
|
||||
factory: createUndiciClient,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void {
|
||||
const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs);
|
||||
if (normalizedTimeoutMs === undefined) {
|
||||
throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`);
|
||||
}
|
||||
undici.setGlobalDispatcher(
|
||||
const dispatcher = withUndiciErrorListener(
|
||||
new undici.EnvHttpProxyAgent({
|
||||
allowH2: false,
|
||||
bodyTimeout: normalizedTimeoutMs,
|
||||
headersTimeout: normalizedTimeoutMs,
|
||||
clientFactory: createUndiciClient,
|
||||
factory: createUndiciOriginDispatcher,
|
||||
}),
|
||||
);
|
||||
undici.setGlobalDispatcher(dispatcher);
|
||||
// Keep fetch and the dispatcher on the same undici implementation. Node 26.0's
|
||||
// bundled fetch can otherwise consume compressed responses through npm undici's
|
||||
// dispatcher without decompressing them, causing response.json() failures.
|
||||
|
||||
Reference in New Issue
Block a user