( res: Response, record: RecordConfig | undefined, cap = 0, )
| 121 | * disk guard. |
| 122 | */ |
| 123 | export async function readBodyIdle( |
| 124 | res: Response, |
| 125 | record: RecordConfig | undefined, |
| 126 | cap = 0, |
| 127 | ): Promise<{ overCap: false; buf: Buffer } | { overCap: true; bytesRead: number }> { |
| 128 | const body = res.body; |
| 129 | if (!body) return { overCap: false, buf: Buffer.alloc(0) }; |
| 130 | const idleMs = clampTimeout(record?.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS); |
| 131 | const reader = body.getReader(); |
| 132 | const chunks: Buffer[] = []; |
| 133 | let total = 0; |
| 134 | try { |
| 135 | for (;;) { |
| 136 | let idleTimer: NodeJS.Timeout | undefined; |
| 137 | // A stream error landing AFTER the idle timeout has already won the |
| 138 | // race must never become an unhandledRejection (process crash) — attach |
| 139 | // a no-op rejection handler to the read promise BEFORE racing. |
| 140 | // Promise.race subscribes to its inputs too, so this is deliberate |
| 141 | // defense-in-depth pinning the invariant against a refactor that races |
| 142 | // the read differently; the race below still observes the rejection |
| 143 | // normally when the read loses first. |
| 144 | const readPromise = reader.read(); |
| 145 | readPromise.catch(() => {}); |
| 146 | const result = await Promise.race([ |
| 147 | readPromise, |
| 148 | new Promise<never>((_, reject) => { |
| 149 | idleTimer = setTimeout( |
| 150 | () => reject(new Error(`Upstream response body idle for ${idleMs}ms`)), |
| 151 | idleMs, |
| 152 | ); |
| 153 | }), |
| 154 | ]).finally(() => clearTimeout(idleTimer)); |
| 155 | if (result.done) break; |
| 156 | total += result.value.byteLength; |
| 157 | if (cap > 0 && total > cap) { |
| 158 | return { overCap: true, bytesRead: total }; |
| 159 | } |
| 160 | chunks.push(Buffer.from(result.value)); |
| 161 | } |
| 162 | } finally { |
| 163 | // Idle expiry and the over-cap early return leave the stream open — |
| 164 | // release it. After a normal completion this is a no-op. |
| 165 | void reader.cancel().catch(() => {}); |
| 166 | } |
| 167 | return { overCap: false, buf: Buffer.concat(chunks) }; |
| 168 | } |
| 169 | |
| 170 | /** |
| 171 | * Cap on the small-JSON upstream envelope bodies (submit, status poll, models |
no test coverage detected
searching dependent graphs…