(chunk: Buffer)
| 1160 | ); |
| 1161 | }; |
| 1162 | const onUpstreamData = (chunk: Buffer) => { |
| 1163 | totalBytes += chunk.length; |
| 1164 | // Trip truncation on EITHER bytes OR frame count. The byte cap alone |
| 1165 | // never bounds `frameTimestamps` (count-indexed, not byte-sized) nor a |
| 1166 | // never-completing parse buffer, so a long-lived / never-ending stream |
| 1167 | // would otherwise grow per-frame state forever. `frameTimestamps.length` |
| 1168 | // is the running complete-frame count. `>=` (not `>`) so we never |
| 1169 | // retain MORE than `maxBufferFrames` frames — see the "maximum N |
| 1170 | // frames retained" contract on DEFAULT_MAX_PROXY_BUFFER_FRAMES. |
| 1171 | if (!bufferTruncated && bufferedBytes + chunk.length > maxBufferBytes) { |
| 1172 | tripTruncation("byte"); |
| 1173 | } else if (!bufferTruncated && frameTimestamps.length >= maxBufferFrames) { |
| 1174 | tripTruncation("frame"); |
| 1175 | } |
| 1176 | // Buffer the raw bytes. Under cap: always. Over cap on a progressive |
| 1177 | // stream: never (bytes are already teed live; chunks was freed). Over |
| 1178 | // cap on a NON-progressive response: keep buffering for the relay — |
| 1179 | // it has no live tee, so `chunks` is the only copy the client can get |
| 1180 | // — but hard-cap it at HARD_CEILING (well under V8's max string |
| 1181 | // length) so the eventual rawBuffer.toString() relay can never throw |
| 1182 | // RangeError: Invalid string length. |
| 1183 | if (!bufferTruncated) { |
| 1184 | chunks.push(chunk); |
| 1185 | bufferedBytes += chunk.length; |
| 1186 | } else if (!isProgressiveStream) { |
| 1187 | if (bufferedBytes + chunk.length <= hardCeiling) { |
| 1188 | chunks.push(chunk); |
| 1189 | bufferedBytes += chunk.length; |
| 1190 | } else { |
| 1191 | // The non-progressive relay copy would exceed the hard ceiling. |
| 1192 | // We CANNOT relay the full body (it can't be buffered safely) and |
| 1193 | // MUST NOT relay the partial buffer as a success — flag it so the |
| 1194 | // caller fails loud (502) instead of presenting a truncated 2xx. |
| 1195 | hardCeilingExceeded = true; |
| 1196 | } |
| 1197 | } |
| 1198 | |
| 1199 | // Capture per-frame timestamps for SSE/NDJSON streams. Gated on |
| 1200 | // !bufferTruncated so per-frame parse/timing state stops growing once |
| 1201 | // the cap trips (the byte/frame guard above already freed it). |
| 1202 | // TCP data events don't align with SSE frames — buffer and |
| 1203 | // split on the protocol delimiter to timestamp each complete frame. |
| 1204 | if (!bufferTruncated && (isSSE || isNDJSON)) { |
| 1205 | frameBuffer += frameDecoder.write(chunk); |
| 1206 | // Split on the protocol delimiter, tolerating CRLF line endings. |
| 1207 | // The SSE spec permits CRLF, and some upstreams/proxies emit |
| 1208 | // \r\n\r\n (SSE) or \r\n (NDJSON) frame boundaries. An LF-only |
| 1209 | // split would see the whole CRLF stream as a single frame and |
| 1210 | // lose per-frame timing. The last split element (a partial frame |
| 1211 | // tail) stays buffered, exactly as with a string delimiter. |
| 1212 | const delimiter = isNDJSON ? /\r?\n/ : /\r?\n\r?\n/; |
| 1213 | const parts = frameBuffer.split(delimiter); |
| 1214 | // All complete frames (everything except the last part which |
| 1215 | // may be incomplete). Enforce the frame cap PER-FRAME: a single |
| 1216 | // coalesced chunk can carry many complete frames, so checking only |
| 1217 | // at the top of the callback would let one event push them all and |
| 1218 | // overshoot the cap unbounded. Trip + bail mid-loop instead. |
| 1219 | for (let fi = 0; fi < parts.length - 1; fi++) { |
nothing calls this directly
no test coverage detected
searching dependent graphs…