File size: 11,156 Bytes
67d18ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
import { connect as connectTcp, type Socket } from "node:net";
import { connect as connectTls, type TLSSocket } from "node:tls";

export type OrderedHeaderPair = [string, string];

export interface OrderedUpstreamRequest {
  url: string;
  method?: string;
  headers: OrderedHeaderPair[];
  body?: string | Uint8Array;
  decompress?: boolean;
  /** Client abort signal — destroys the socket the moment the client aborts. */
  signal?: AbortSignal;
}

type WireSocket = Socket | TLSSocket;

const CRLF = "\r\n";
const HEADER_END = new Uint8Array([13, 10, 13, 10]);
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;

export async function sendOrderedUpstreamRequest(req: OrderedUpstreamRequest): Promise<Response> {
  const url = new URL(req.url);
  const bodyBytes = bodyToBytes(req.body);
  const requestHead = buildRequestHead(url, req.method ?? "POST", req.headers, bodyBytes.byteLength);
  const socket = await openSocket(url);

  return await new Promise<Response>((resolve, reject) => {
    let headerBuffer: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
    let responseStarted = false;
    let postWrite = false;
    let bodyController: ReadableStreamDefaultController<Uint8Array> | null = null;
    let chunkedDecoder: ChunkedDecoder | null = null;
    let remainingContentLength: number | null = null;

    const bodyStream = new ReadableStream<Uint8Array>({
      start(controller) {
        bodyController = controller;
      },
      cancel() {
        socket.destroy();
      },
    });

    function fail(err: unknown): void {
      if (!responseStarted && postWrite) {
        // Review follow-up #2 (PR #34): the full request (head + body) was
        // already written to the wire, so the upstream may have processed it
        // — resending could duplicate the LLM call and consume quota twice.
        // Flag it so the connect-retry loop in handler.ts skips this error.
        try { (err as { postWrite?: boolean }).postWrite = true; } catch {}
      }
      if (responseStarted) {
        // Safe by spec: error() on an already-closed/errored ReadableStream
        // controller is a NO-OP under WHATWG Streams semantics (only close()
        // and enqueue() throw on a closed controller) — verified against both
        // Bun and Node. The unguarded call below is intentional; do not wrap
        // it (audit CL-01: triple-verified non-issue, closed).
        bodyController?.error(err);
      } else {
        reject(err);
      }
      socket.destroy();
    }

    // Abort propagation (CL-04): destroy the socket the moment the client
    // aborts. Without this the upstream LLM call kept running (consuming
    // quota for the whole generation) after the client disappeared during a
    // long-TTFB reasoning request. `fail()` both rejects this promise (a bare
    // destroy() emits "close", not "error"/"end", and would leave it pending
    // forever) and errors the consumer-side body stream when the response has
    // already started. The resulting error carries `postWrite` (the request
    // is fully on the wire by then), so handler's connect-retry ladder skips
    // it — combined with the `clientReq.signal.aborted` pre-check there,
    // client aborts never enter the retry loop.
    if (req.signal) {
      const signal = req.signal;
      const onAbort = (): void => {
        fail(new Error("client aborted during ordered upstream request"));
      };
      if (signal.aborted) {
        onAbort();
        return;
      }
      signal.addEventListener("abort", onAbort, { once: true });
      // De-register when the socket settles so a signal that outlives this
      // request (the handler reuses the client signal across connect
      // attempts) does not accumulate listeners.
      socket.once("close", () => signal.removeEventListener("abort", onAbort));
    }

    function finish(): void {
      if (chunkedDecoder && !chunkedDecoder.done) {
        try { bodyController?.error(new Error("upstream chunked body truncated")); } catch {}
        socket.destroy();
        return;
      }
      try { bodyController?.close(); } catch {}
    }

    function pushBody(bytes: Uint8Array): void {
      if (!bodyController || bytes.byteLength === 0) return;
      if (chunkedDecoder) {
        chunkedDecoder.push(bytes, bodyController);
        if (chunkedDecoder.done) finish();
        return;
      }
      if (remainingContentLength !== null) {
        const next = bytes.slice(0, remainingContentLength);
        remainingContentLength -= next.byteLength;
        if (next.byteLength > 0) bodyController.enqueue(next);
        if (remainingContentLength === 0) finish();
        return;
      }
      bodyController.enqueue(bytes);
    }

    socket.on("data", (chunk: Buffer) => {
      try {
        const bytes = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
        if (!responseStarted) {
          headerBuffer = concatBytes(headerBuffer, bytes);
          const headerEnd = indexOfBytes(headerBuffer, HEADER_END);
          if (headerEnd < 0) return;

          const headerBytes = headerBuffer.slice(0, headerEnd);
          const rest = headerBuffer.slice(headerEnd + HEADER_END.byteLength);
          const parsed = parseResponseHeaders(headerBytes);
          responseStarted = true;

          const transferEncoding = parsed.headers.get("transfer-encoding")?.toLowerCase() ?? "";
          if (transferEncoding.split(",").map((s) => s.trim()).includes("chunked")) {
            parsed.headers.delete("transfer-encoding");
            chunkedDecoder = new ChunkedDecoder();
          } else {
            const contentLength = parsed.headers.get("content-length");
            remainingContentLength = contentLength ? Number.parseInt(contentLength, 10) : null;
            if (!Number.isFinite(remainingContentLength as number)) remainingContentLength = null;
          }

          let responseBody: ReadableStream<Uint8Array> = bodyStream;
          if (req.decompress && parsed.headers.get("content-encoding")?.toLowerCase() === "gzip") {
            parsed.headers.delete("content-encoding");
            parsed.headers.delete("content-length");
            const gzip = new DecompressionStream("gzip") as unknown as ReadableWritablePair<Uint8Array, Uint8Array>;
            responseBody = bodyStream.pipeThrough(gzip);
          }

          resolve(new Response(responseBody, {
            status: parsed.status,
            statusText: parsed.statusText,
            headers: parsed.headers,
          }));
          pushBody(rest);
          return;
        }
        pushBody(bytes);
      } catch (err) {
        fail(err);
      }
    });

    socket.once("error", fail);
    socket.once("end", () => {
      if (!responseStarted) {
        reject(new Error("upstream closed before sending response headers"));
        return;
      }
      finish();
    });

    socket.write(requestHead);
    if (bodyBytes.byteLength > 0) socket.write(bodyBytes);
    postWrite = true;
  });
}

function openSocket(url: URL): Promise<WireSocket> {
  const isHttps = url.protocol === "https:";
  if (!isHttps && url.protocol !== "http:") {
    return Promise.reject(new Error(`Unsupported upstream protocol: ${url.protocol}`));
  }
  const port = Number(url.port || (isHttps ? 443 : 80));

  return new Promise((resolve, reject) => {
    const onConnect = () => {
      socket.off("error", reject);
      resolve(socket);
    };
    const socket: WireSocket = isHttps
      ? connectTls({ host: url.hostname, port, servername: url.hostname }, onConnect)
      : connectTcp({ host: url.hostname, port }, onConnect);
    socket.once("error", reject);
  });
}

function buildRequestHead(url: URL, method: string, headers: OrderedHeaderPair[], contentLength: number): string {
  const path = `${url.pathname || "/"}${url.search}`;
  const lines = [
    `${method} ${path} HTTP/1.1`,
    `Host: ${url.host}`,
    ...headers.map(headerLine),
    `Content-Length: ${contentLength}`,
    "Connection: close",
    "",
    "",
  ];
  return lines.join(CRLF);
}

function headerLine([name, value]: OrderedHeaderPair): string {
  if (!HEADER_NAME.test(name)) throw new Error(`Invalid upstream header name: ${name}`);
  if (/[\r\n]/.test(value)) throw new Error(`Invalid upstream header value for ${name}`);
  return `${name}: ${value}`;
}

function bodyToBytes(body: string | Uint8Array | undefined): Uint8Array {
  if (body === undefined) return new Uint8Array(0);
  if (typeof body === "string") return new TextEncoder().encode(body);
  return body;
}

function parseResponseHeaders(bytes: Uint8Array): { status: number; statusText: string; headers: Headers } {
  const text = new TextDecoder("latin1").decode(bytes);
  const lines = text.split(CRLF);
  const statusLine = lines.shift() ?? "";
  const match = /^HTTP\/\d(?:\.\d)?\s+(\d{3})(?:\s+(.*))?$/.exec(statusLine);
  if (!match) throw new Error(`Invalid upstream status line: ${statusLine}`);

  const headers = new Headers();
  for (const line of lines) {
    if (!line) continue;
    const idx = line.indexOf(":");
    if (idx <= 0) continue;
    headers.append(line.slice(0, idx), line.slice(idx + 1).trimStart());
  }

  return { status: Number(match[1]), statusText: match[2] ?? "", headers };
}

class ChunkedDecoder {
  private buffer: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
  private expectedSize: number | null = null;
  done = false;

  push(bytes: Uint8Array, controller: ReadableStreamDefaultController<Uint8Array>): void {
    if (this.done) return;
    this.buffer = concatBytes(this.buffer, bytes);

    while (!this.done) {
      if (this.expectedSize === null) {
        const lineEnd = indexOfCrlf(this.buffer);
        if (lineEnd < 0) return;
        const line = new TextDecoder("latin1").decode(this.buffer.slice(0, lineEnd));
        const sizeHex = line.split(";", 1)[0].trim();
        const size = Number.parseInt(sizeHex, 16);
        if (!Number.isFinite(size)) throw new Error(`Invalid chunk size: ${line}`);
        this.buffer = this.buffer.slice(lineEnd + 2);
        this.expectedSize = size;
        if (size === 0) {
          this.done = true;
          return;
        }
      }

      if (this.buffer.byteLength < this.expectedSize + 2) return;
      const chunk = this.buffer.slice(0, this.expectedSize);
      controller.enqueue(chunk);
      this.buffer = this.buffer.slice(this.expectedSize + 2);
      this.expectedSize = null;
    }
  }
}

function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
  const out = new Uint8Array(a.byteLength + b.byteLength);
  if (a.byteLength > 0) out.set(a, 0);
  if (b.byteLength > 0) out.set(b, a.byteLength);
  return out;
}

function indexOfBytes(haystack: Uint8Array, needle: Uint8Array): number {
  outer: for (let i = 0; i <= haystack.byteLength - needle.byteLength; i++) {
    for (let j = 0; j < needle.byteLength; j++) {
      if (haystack[i + j] !== needle[j]) continue outer;
    }
    return i;
  }
  return -1;
}

function indexOfCrlf(bytes: Uint8Array): number {
  for (let i = 0; i < bytes.byteLength - 1; i++) {
    if (bytes[i] === 13 && bytes[i + 1] === 10) return i;
  }
  return -1;
}