Repository navigation
Expand file tree
/
Copy pathhttp.ts
More file actions
1482 lines (1394 loc) · 65.6 KB
/
Copy pathhttp.ts
File metadata and controls
1482 lines (1394 loc) · 65.6 KB
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// geatsc-compiled `node:http` — a real subset of Node's http module running on
// the native reactor (runtime/gea_node.cpp).
//
// The C++ layer parses full HTTP/1.x requests (request line, headers,
// content-length and chunked bodies, size limits, 100-continue) and dispatches
// each one with a connection id. Responses stream back through per-connection
// intrinsics (`__gea_http_write` / `__gea_http_done`), so handlers can be
// synchronous, async, or long-lived (chunked streaming). Keep-alive and
// pipelining ordering are enforced by the reactor: the next pipelined request
// on a connection is not dispatched until the previous response is done.
//
// Response serialization matches Node's observable behavior (verified
// byte-for-byte by apps/http-parity): app headers in insertion order, then
// Date / Connection / Keep-Alive, then the auto framing header; `writeHead`
// commits the header block (so the body streams chunked unless the app set
// Content-Length); a plain `end(body)` without writeHead emits an exact
// Content-Length; HTTP/1.0 without a known length falls back to
// close-delimited; 1xx/204/304 and HEAD suppress the body.
//
// Known deviations (documented, not silent): request `set-cookie` duplicates
// join with ", " instead of becoming an array (headers stays a string→string
// map); `getHeader` returns multi-value headers joined with ", ";
// `writeHead(status, message, headers)` 3-arg form is not supported — set
// `res.statusMessage` instead; chunk trailers are parsed and discarded;
// body chunks are byte-preserving strings, not Buffers; `res.req` is not
// provided (the request is in scope wherever the response is); req/res
// listeners compile to NATIVE closures with no JS function identity, so
// `removeListener(name, fn)` on them cannot match — use
// `removeAllListeners(name)`.
import { EventEmitter, EventHandler, EventName } from './events'
import { Buffer, BufferEncoding } from './buffer-types'
import { nodeNotImplemented } from './not-implemented'
import { DestroyCallback, Duplex, Readable, StreamCallback, Writable } from './stream'
import { Socket as NetSocket } from './net'
import type { URL as WhatwgURL } from './whatwg-url'
// The narrower contract (see `net.ts` for the argument). `serve` stores the
// dispatch handler; `done` reaches `responseComplete` -> `processInput`, which
// runs the program's own handler for the next pipelined request; `destroy` and
// `stop` tear C++ state down. Running program code is permitted here and
// forbidden by `@gea-host-inert`, which is why these four carry this tag and
// not that one -- none of them writes a property on any JavaScript object.
/** @gea-host-no-property-writes */
declare function __gea_http_serve(
port: number,
onRequest: (connId: number, flags: number, method: string, url: string, httpVersion: string, rawHead: string, body: string) => void
): void
// `@gea-host-inert` is the per-declaration host effect contract (compiler
// `src/semantics/normalize/host-effect-contracts.ts`). `write`/`write_bytes`
// copy their payload into the connection's own buffer (`enqueueResponseBytes`
// builds a `std::string` from the bytes) and `peer`/`date` only read, so none
// retains an argument or runs program code. `done` and `destroy` are NOT
// tagged and must not be: `responseComplete` calls `processInput()`, which
// dispatches the next pipelined request into the program's own `onRequest`
// closure -- program code, inside the call. `serve` takes that closure, and
// `stop` runs the listener closer.
/** @gea-host-inert */
declare function __gea_http_write(connId: number, data: string): void
// The header block, serialized piece by piece into the connection's retained
// output buffer -- `commitHeaders` below says why. `head_end` answers a bit
// pair: 1 = the body is chunked, 2 = the connection stays open.
/** @gea-host-inert */
declare function __gea_http_head_begin(connId: number, status: number, message: string): void
/** @gea-host-inert */
declare function __gea_http_head_field(connId: number, name: string, value: string): void
/** @gea-host-inert */
declare function __gea_http_head_end(connId: number, flags: number, autoContentLength: number): number
// Body bytes, taken by reference by the reactor. `__gea_http_write` takes its
// string by value, which copies an lvalue body just to form the argument.
/** @gea-host-inert */
declare function __gea_http_body(connId: number, data: string): void
// A whole chunked body in one call: size line, text, terminator.
/** @gea-host-inert */
declare function __gea_http_final_chunk(connId: number, data: string): void
// A string's length on the wire. Strings are UTF-8 in this runtime, so this is
// the storage size -- one load. `Buffer.byteLength(text)` answers the same
// number, but through `normalizeEncoding('utf8')`: a by-value string, a
// `tolower` per character and a comparison chain, per response, to conclude
// what the caller already knew. Measured at ~1% of a request.
/** @gea-host-inert */
declare function __gea_http_text_bytes(text: string): number
// Octets rather than text, for a `Uint8Array`/`Buffer` body: a TypeScript
// `string` cannot carry arbitrary bytes across this boundary intact. See the
// definition in `runtime/gea_node.cpp` for why both calls exist.
/** @gea-host-inert */
declare function __gea_http_write_bytes(connId: number, data: Uint8Array): void
// The inbound direction: the reactor's request body string as its own octets.
// `Buffer.from(body)` would decode it as UTF-8 and replace every invalid byte.
/** @gea-host-inert */
declare function __gea_http_body_bytes(body: string): Buffer
/** @gea-host-no-property-writes */
declare function __gea_http_done(connId: number, keepAlive: boolean): void
/** @gea-host-no-property-writes */
declare function __gea_http_destroy(connId: number): void
/** @gea-host-inert */
declare function __gea_http_peer(connId: number): string
/** @gea-host-inert */
declare function __gea_http_connection_open(connId: number): boolean
/** @gea-host-no-property-writes */
declare function __gea_http_stop(): void
// Dispatch flags from the reactor (keep in sync with gea_node.cpp).
const FLAG_KEEP_ALIVE = 1
const FLAG_FIRST_ON_CONNECTION = 2
// NOTE: deliberately NOT named `METHODS`: geatsc keys module-level globals by
// bare name and hono's router exports its own `METHODS` const. The compiler
// now REJECTS such collisions at compile time (rename this back to see the
// diagnostic); classes got full symbol-based disambiguation, globals keep the
// loud error until they get the same. `http.METHODS` is available via the
// default export.
export const httpMethods: string[] = [
'ACL',
'BIND',
'CHECKOUT',
'CONNECT',
'COPY',
'DELETE',
'GET',
'HEAD',
'LINK',
'LOCK',
'M-SEARCH',
'MERGE',
'MKACTIVITY',
'MKCALENDAR',
'MKCOL',
'MOVE',
'NOTIFY',
'OPTIONS',
'PATCH',
'POST',
'PROPFIND',
'PROPPATCH',
'PURGE',
'PUT',
'REBIND',
'REPORT',
'SEARCH',
'SOURCE',
'SUBSCRIBE',
'TRACE',
'UNBIND',
'UNLINK',
'UNLOCK',
'UNSUBSCRIBE'
]
export const STATUS_CODES: { [code: string]: string } = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a Teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'509': 'Bandwidth Limit Exceeded',
'510': 'Not Extended',
'511': 'Network Authentication Required'
}
/**
* `name` lowercased, without allocating when it already is.
*
* Header lookup is by lowercase name, so every `setHeader`/`getHeader` ran
* `toLowerCase()` -- which allocates a fresh string AND goes through the
* runtime's `asciiCase`, calling libc's locale-aware `tolower` once per
* character. Well-behaved callers already pass lowercase (`'content-type'`),
* and so does Hono, so that work produced a byte-for-byte copy of its input.
* Scanning first is a few compares over a short string with no allocation;
* only a name that genuinely has an uppercase letter pays for the conversion.
*/
function lowerHeaderName(name: string): string {
for (let i = 0; i < name.length; i++) {
const c = name.charCodeAt(i)
if (c >= 65 && c <= 90) return name.toLowerCase()
}
return name
}
export function statusText(code: number): string {
// Fast path for the overwhelmingly common codes — the map lookup below is a
// String(code) allocation plus a linear scan of ~60 entries per response.
if (code === 200) return 'OK'
if (code === 404) return 'Not Found'
if (code === 204) return 'No Content'
if (code === 500) return 'Internal Server Error'
const text = STATUS_CODES[String(code)]
if (text === undefined || text === '') return 'unknown'
return text
}
// Node joins duplicate request headers per header semantics: singleton headers
// keep the FIRST value, `cookie` joins with "; ", everything else with ", ".
function addIncomingHeader(headers: { [name: string]: string }, lower: string, value: string): void {
if (!headers.hasOwnProperty(lower)) headers[lower] = value
else if (!isSingletonHeader(lower)) headers[lower] = headers[lower] + (lower === 'cookie' ? '; ' : ', ') + value
}
function isSingletonHeader(lower: string): boolean {
return (
lower === 'age' ||
lower === 'authorization' ||
lower === 'content-length' ||
lower === 'content-type' ||
lower === 'etag' ||
lower === 'expires' ||
lower === 'from' ||
lower === 'host' ||
lower === 'if-modified-since' ||
lower === 'if-unmodified-since' ||
lower === 'last-modified' ||
lower === 'location' ||
lower === 'max-forwards' ||
lower === 'proxy-authorization' ||
lower === 'referer' ||
lower === 'retry-after' ||
lower === 'server' ||
lower === 'user-agent'
)
}
// This used to be a second, unrelated implementation of Node's `net.Socket` -- a disconnected
// class with its own private fields, never sharing so much as a common ancestor with net.ts's
// real `Socket` (which tls.ts's `TLSSocket` correctly extends). Two Socket classes standing in
// for one Node type is exactly the kind of two-authorities defect this codebase avoids elsewhere:
// hono's `request.ts` does `(incoming.socket as TLSSocket).encrypted` (a plain duck-typed check
// for HTTPS), and TypeScript refuses that cast unless the source is a real super/subtype of
// TLSSocket. Extending net.ts's Socket instead of inventing a third shape fixes the cast without
// touching net.ts or tls.ts.
//
// net.Socket's own constructor and its TCP-specific methods (connect/write/pipe/setNoDelay/...)
// are never invoked here: this class only ever wraps a connId from the HTTP reactor, never a
// native TCP connection, so the inherited `nativeId_` stays 0 for the lifetime of the instance
// and every method that guards on it (`write`, `pause`, `setNoDelay`, ...) is simply inert, not
// live behaviour. `destroy()`/`destroySoon()` are overridden because the inherited ones talk to
// the wrong reactor (`__gea_node_net_destroy` on a `nativeId_` that is always 0 here, instead of
// `__gea_http_destroy` on the real `connId_`).
export class Socket extends NetSocket {
private connId_: number
// The HTTP reactor serves plaintext only. `@hono/node-server` reads this
// through a `TLSSocket` cast to pick the URL scheme; a plain Node socket has
// no such property, which reads the same falsy way.
readonly encrypted: boolean = false
constructor(connId: number = 0) {
super()
this.connId_ = connId
this.remoteFamily = 'IPv4'
this.loadPeer()
}
// Not lazy any more (it was, via a private remoteAddress_/remotePort_ pair, before this class
// extended net.Socket): TypeScript will not let a subclass override a plain inherited property
// with an accessor pair (TS2611/TS2855), so remoteAddress/remotePort now come straight from
// net.Socket's own writable fields, populated once, eagerly, right here. Guarded on connId_ !==
// 0 so the module-load-time placeholder socket (connId 0, see below) never calls into the
// reactor for a connection that was never accepted. This is scoped by the same laziness the
// getter used to add on top of: `IncomingMessage.socket` only constructs a Socket at all when
// the app reads `.socket`, so the peer syscall still isn't paid by requests that never touch it.
private loadPeer(): void {
if (this.connId_ === 0) return
const peer = __gea_http_peer(this.connId_)
const space = peer.indexOf(' ')
if (space >= 0) {
this.remoteAddress = peer.substring(0, space)
this.remotePort = Number(peer.substring(space + 1))
}
}
// Real destroySoon waits for pending writes to flush before closing. This handle never
// buffers writes of its own (ServerResponse writes straight to the reactor via
// __gea_http_write, bypassing Socket entirely), so there is nothing to wait for -- an
// immediate destroy is the honest equivalent.
override destroySoon(): void {
this.destroy()
}
override destroy(): this {
if (this.destroyed) return this
this.destroyed = true
__gea_http_destroy(this.connId_)
return this
}
}
// One `Socket` per connection, as in Node: every request a keep-alive
// connection carries reads the same `req.socket`. Building one per request cost
// @hono/node-server 7% of its CPU, because `newRequest` reads
// `incoming.socket.encrypted` on every request. The reactor reports no close to
// JavaScript, so the table is pruned against it whenever it doubles past the
// size it had after the last prune -- amortized O(1) per new connection, and
// bounded by twice the live connections.
const connectionSockets = new Map<number, Socket>()
let connectionSocketPruneAt = 64
function connectionSocket(connId: number): Socket {
const known = connectionSockets.get(connId)
if (known !== undefined) return known
if (connectionSockets.size >= connectionSocketPruneAt) {
const closed: number[] = []
for (const id of connectionSockets.keys()) if (!__gea_http_connection_open(id)) closed.push(id)
for (let i = 0; i < closed.length; i++) connectionSockets.delete(closed[i])
connectionSocketPruneAt = Math.max(64, connectionSockets.size * 2)
}
const socket = new Socket(connId)
connectionSockets.set(connId, socket)
return socket
}
// Shared placeholder: ServerResponse's socket slot is overwritten with the
// request's socket right after construction (Node shares ONE socket object
// between req and res), so constructing a fresh Socket per response would be
// a wasted allocation.
const placeholderSocket = new Socket(0)
// Body/lifecycle listeners are stored as TYPED native closures — a
// `(chunk: string) => void` slot covers 'data' (one string arg) and the
// no-arg 'end'/'close' listeners (an arrow with fewer params adapts, exactly
// like JS). No gea_cpp_value boxing anywhere in listener storage or dispatch.
export type MessageListener = (chunk: string) => void
export type ClientRequestErrorListener = (error: Error) => void
export type ClientResponseListener = (response: IncomingMessage) => void
export type OutgoingHttpHeader = string | number | boolean | readonly string[]
// Same shape Node ships: an arbitrary header bag keyed by name, values
// widened past plain strings (numbers for things like Content-Length,
// arrays for repeated headers such as set-cookie). `writeHead` accepts this
// directly and funnels every value through `setHeader`, which already knows
// how to serialize each of these shapes.
export interface OutgoingHttpHeaders {
[name: string]: OutgoingHttpHeader | undefined
}
export interface RequestOptions {
headers?: { [name: string]: OutgoingHttpHeader | undefined }
host?: string
hostname?: string
method?: string
path?: string
port?: number | string
protocol?: string
timeout?: number
}
// `IncomingMessage` used to keep its own hand-rolled body_/bodyPending_/paused_/endEmitted_/
// destroyed_/errored_/encoding_/didRead_ state, driven entirely off EventEmitter with a custom
// deliverBody() -- a private, parallel reimplementation of exactly what stream.ts's Readable
// already does (a pending-chunk queue, pause/resume, push/read, readable*/destroyed/errored). It
// only existed because IncomingMessage did not extend Readable. Now that it does (see the
// `extends Readable` below, and the "Writable extends Stream" comment on stream.ts for the
// hierarchy change that made this possible without paying for the writable side too),
// IncomingMessage drives Readable's own storage: the whole body arrives from the reactor in one
// shot, so it is `push()`ed in full in the constructor, and consumption goes through Readable's
// real push/read/pause/resume/destroy machinery instead of a duplicate.
export class IncomingMessage extends Readable {
method: string
url: string
statusCode?: number
httpVersion: string
httpVersionMajor: number
httpVersionMinor: number
complete: boolean
// Not a Node property but a widely honoured adapter convention: a body
// parser that has already drained the request stashes the raw bytes here,
// and `@hono/node-server`'s `request.ts` re-reads them (`'rawBody' in
// incoming && incoming.rawBody instanceof Buffer`) inside a ReadableStream
// `start` callback. TypeScript drops an `instanceof` narrowing of a
// property path once it crosses into a callback, so with the field
// undeclared that read is `unknown` there and `controller.enqueue` refuses
// it (TS2345); declared, it is `Buffer | undefined` everywhere and the
// optional `enqueue(chunk?: Uint8Array)` takes it as written. Nothing this
// target ships assigns it.
rawBody?: Buffer
private connId_: number
private socket_: Socket | undefined
private headersMap_: { [name: string]: string } | undefined
private rawHead_: string
private rawHeaders_: string[] | undefined
// Dedupes the deferred-resume microtask newListenerAdded schedules below -- 'data' and 'end'
// listeners attached in the same synchronous turn (the common case) should drain once, not twice.
private deliveryScheduled_: boolean
constructor(connId: number, method: string, url: string, httpVersion: string, rawHead: string, body: string) {
super()
this.connId_ = connId
this.method = method
this.url = url
this.statusCode = undefined
// The wire form is "HTTP/1.1"; Node exposes "1.1".
this.httpVersion = httpVersion === 'HTTP/1.0' ? '1.0' : httpVersion === 'HTTP/1.1' ? '1.1' : httpVersion
this.httpVersionMajor = 1
this.httpVersionMinor = this.httpVersion === '1.0' ? 0 : 1
this.socket_ = undefined
this.headersMap_ = undefined
this.rawHead_ = rawHead
this.rawHeaders_ = undefined
this.deliveryScheduled_ = false
// Node's `complete` means "the parser has seen the whole message", not
// "the app has drained it": for a body-less GET it is already true inside
// the 'request' listener. The reactor only dispatches once the full body is
// in hand, so it is true from construction here too. This used to be set
// from a `once('end')` closure, which cost every request a boxed listener,
// three entry arrays, the 'newListener'/'removeListener' dispatches and a
// splice -- for a flag the constructor already knows.
this.complete = true
if (body.length > 0) this.push(body)
this.pushEnd()
}
// Typed as `net.Socket` (the real Node return type for `IncomingMessage.socket`), not the
// concrete `Socket` subclass constructed below. hono's `request.ts` does
// `(incoming.socket as TLSSocket).encrypted`: TLSSocket extends net.Socket directly, so casting
// from the WIDER net.Socket type is a plain, valid downcast. Casting from the narrower `Socket`
// subclass declared in this file would not be (it carries its own private `connId_` that
// TLSSocket does not, which breaks the structural comparability TypeScript needs for the
// assertion) -- this is why the getter's declared return type matters here, not just the
// runtime value.
get socket(): NetSocket {
let socket = this.socket_
if (socket === undefined) {
socket = connectionSocket(this.connId_)
this.socket_ = socket
}
return socket
}
// Lazy: the reactor hands over the raw header block as ONE buffer; the
// alternating [name, value, ...] array only materializes if the app reads
// it. Trimming rules mirror the reactor's parse (OWS around values).
get rawHeaders(): string[] {
let rawHeaders = this.rawHeaders_
if (rawHeaders === undefined) {
rawHeaders = []
const head = this.rawHead_
let pos = 0
while (pos < head.length) {
let eol = head.indexOf('\r\n', pos)
if (eol < 0) eol = head.length
const line = head.substring(pos, eol)
pos = eol + 2
if (line.length === 0) continue
const colon = line.indexOf(':')
if (colon <= 0) continue
let valueStart = colon + 1
while (valueStart < line.length && (line[valueStart] === ' ' || line[valueStart] === '\t')) valueStart++
let valueEnd = line.length
while (valueEnd > valueStart && (line[valueEnd - 1] === ' ' || line[valueEnd - 1] === '\t')) valueEnd--
rawHeaders.push(line.substring(0, colon))
rawHeaders.push(line.substring(valueStart, valueEnd))
}
this.rawHeaders_ = rawHeaders
}
return rawHeaders
}
// Lazy joined headers — built on first access so requests that never read
// `headers` (the common hot path) pay nothing.
get headers(): { [name: string]: string } {
let headers = this.headersMap_
if (headers === undefined) {
headers = {}
const raw = this.rawHeaders_
if (raw !== undefined) {
for (let i = 0; i + 1 < raw.length; i += 2) addIncomingHeader(headers, lowerHeaderName(raw[i]), raw[i + 1])
} else {
// Straight off the head: `@hono/node-server` reads `headers.host` on
// every request, and going through `rawHeaders` built an array of every
// name and value only to copy each one again into this map.
const head = this.rawHead_
let pos = 0
while (pos < head.length) {
let eol = head.indexOf('\r\n', pos)
if (eol < 0) eol = head.length
const lineStart = pos
pos = eol + 2
const colon = head.indexOf(':', lineStart)
if (colon <= lineStart || colon >= eol) continue
let valueStart = colon + 1
while (valueStart < eol && (head[valueStart] === ' ' || head[valueStart] === '\t')) valueStart++
let valueEnd = eol
while (valueEnd > valueStart && (head[valueEnd - 1] === ' ' || head[valueEnd - 1] === '\t')) valueEnd--
addIncomingHeader(headers, lowerHeaderName(head.substring(lineStart, colon)), head.substring(valueStart, valueEnd))
}
}
this.headersMap_ = headers
}
return headers
}
// Readable's own `newListenerAdded` resumes (synchronously) the instant a 'data' listener is
// added, draining whatever is already pushed. That is exactly right for a stream that fills up
// over time, but this reactor hands the WHOLE body to the constructor before the request
// handler ever runs, so a synchronously-attached 'data' listener would drain immediately inside
// the very `.on('data', ...)` call -- before the handler has had its full synchronous turn to
// attach every listener it means to (e.g. 'end' right after 'data'). Deferring one microtask
// (deduped via deliveryScheduled_ so 'data' and 'end' attached in the same tick only schedule
// once) reproduces Node's actual contract: listener registration is synchronous, delivery is
// not. This intentionally does NOT call `super.newListenerAdded` -- Readable's synchronous
// resume is exactly the behaviour being replaced. An 'end'-only listener (no 'data') still
// triggers delivery here, matching this runtime's documented deviation from strict Node
// semantics (a real un-flowing, un-resumed Readable with only an 'end' listener never fires it).
protected override newListenerAdded(name: string): void {
if ((name !== 'data' && name !== 'end') || this.deliveryScheduled_ || this.readableFlowing === true) return
this.deliveryScheduled_ = true
const self = this
queueMicrotask(() => {
self.deliveryScheduled_ = false
if (self.readableFlowing !== false) self.resume()
})
}
/**
* Native server requests arrive from the reactor with their complete body. This narrow bridge
* lets compiled adapters consume that body without manufacturing a pending JavaScript Promise
* around EventEmitter callbacks. `super.read()` (Readable's real, synchronous read) already
* does exactly that; this only normalizes the empty case to `''` instead of `null` and widens
* the return type to plain `string` (see `read()` below for why a public `.read()` call wraps
* bytes in a Buffer instead -- this bridge is for compiled adapters that already know they want
* text).
*/
readBody(): string {
// `Readable.read` hands the drained chunk through `emittedChunk`, which
// this class overrides to wrap the stored byte-string in a Buffer whenever
// no encoding was set -- so the chunk arriving here is a Buffer, not the
// string Readable stores. Unwrapped through the same byte-preserving
// mapping (`latin1`: one char per octet) the reactor stored it in.
const chunk: unknown = super.read(0)
if (typeof chunk === 'string') return chunk
return Buffer.isBuffer(chunk) ? chunk.toString('latin1') : ''
}
/** `readBody()` as the octets the client sent, for binary bodies (multipart files). */
readBodyBytes(): Buffer {
const chunk: unknown = super.read(0)
if (Buffer.isBuffer(chunk)) return chunk
return __gea_http_body_bytes(typeof chunk === 'string' ? chunk : '')
}
// Node's Readable#read(): pull whatever is buffered and unread, as a Buffer unless setEncoding
// was called (real Node hands out Buffers by default and strings once an encoding is set).
// Readable's own storage always holds this reactor's body as a plain string (byte-preserving,
// not a Buffer -- see this file's header comment), so the Buffer-wrapping has to happen here,
// one layer up, rather than in stream.ts's generic `emittedChunk`. This target hands over the
// whole body up front (no partial/streamed reads from the reactor), so `size` has nothing to
// act on -- every call drains whatever remains in one shot, same as a `size` larger than the
// buffer would in real Node.
override read(size: number = 0): Buffer | string | null {
const chunk: unknown = super.read(size)
if (chunk === null) return null
return this.readableEncoding === null ? __gea_http_body_bytes(chunk as string) : (chunk as string)
}
// Node's 'data' hands out Buffers unless setEncoding was called; the stored
// reactor string is wrapped for the same reason `read()` wraps it.
// `Readable.toWeb` and @hono/node-server's body reader both expect bytes.
protected override emittedChunk(chunk: unknown): unknown {
const emitted = super.emittedChunk(chunk)
return this.readableEncoding === null && typeof emitted === 'string' ? __gea_http_body_bytes(emitted) : emitted
}
override _destroy(error: Error | null, callback: DestroyCallback): void {
__gea_http_destroy(this.connId_)
// Mirrors the original behaviour: destroying a request owes listeners their 'close' off the
// dispatch stack, not synchronously inside destroy().
queueMicrotask(() => callback(error))
}
}
// Response lifecycle listeners ('finish', 'close') take no arguments — a
// typed no-arg closure slot, no boxing.
export type ResponseListener = () => void
// Extends Writable (not EventEmitter) so this is assignable everywhere `@hono/node-server`
// types a response as `Writable` -- see the "Writable extends Stream" comment on stream.ts for
// the hierarchy change that made this cheap: ServerResponse now carries Writable's own field set
// (writable/writableAborted/writableCorked/writableHighWaterMark/writableLength/writableNeedDrain/
// writableObjectMode/destroyed/closed/errored, plus a small unused write-queue for corking), not
// Readable's, since Writable no longer drags Readable's ~20 fields along for the ride.
//
// write()/end()/destroy() below stay ServerResponse's OWN implementations rather than routing
// through Writable's generic performWrite/_write plumbing: they drive HTTP framing byte-for-byte
// (chunked-encoding hex prefixes, Content-Length auto-computation, HEAD/1xx/204/304 body
// suppression, keep-alive headers) verified elsewhere in this repo to match Node exactly, and
// write straight to the reactor (`__gea_http_write`/`__gea_http_write_bytes`) with no intermediate
// queue. Re-deriving all of that from `_write`/`_final` hooks would be a much larger, riskier
// rewrite of code with a byte-for-byte parity requirement, for no behavioural gain -- Writable's
// own `_write`/`_writev`/`_final`/`_destroy` defaults are simply never called here. `destroyed`,
// `closed` and `errored` are Writable's real fields now (no more private `destroyed_` shadow).
export class ServerResponse extends Writable {
statusCode: number
statusMessage: string
sendDate: boolean
finished: boolean
private connId_: number
private request_: IncomingMessage | undefined
private isHead_: boolean
private http10_: boolean
private keepAliveRequested_: boolean
private headersCommitted_: boolean // header block serialized (writeHead / first write)
private headWritten_: boolean // header bytes handed to the reactor
private chunked_: boolean
private suppressBody_: boolean // HEAD / 1xx / 204 / 304
private keepAliveFinal_: boolean
private firstHeaderPresent_: boolean
private firstHeaderName_: string
private firstHeaderValue_: string
// The header's name AS THE CALLER SPELLED IT -- not a prebuilt
// `'Name: value\r\n'` line. Storing the assembled line meant a concat, and
// so an allocation, for every header of every response; nothing ever read
// the line back, it was only appended to the block and shuffled on removal.
// Keeping the raw name lets the serializer append name, ': ', value and the
// CRLF as four pieces, three of which are strings that already exist and
// two of which are literals.
private firstHeaderRawName_: string
private extraHeaderNames_: string[] | undefined
private extraHeaderValues_: string[] | undefined
private extraHeaderRawNames_: string[] | undefined
constructor(connId: number, isHead: boolean, keepAliveRequested: boolean, http10: boolean, request: IncomingMessage) {
super()
this.connId_ = connId
this.request_ = request
this.isHead_ = isHead
this.http10_ = http10
this.keepAliveRequested_ = keepAliveRequested
this.statusCode = 200
this.statusMessage = ''
this.sendDate = true
this.finished = false
this.headersCommitted_ = false
this.headWritten_ = false
this.chunked_ = false
this.suppressBody_ = false
this.keepAliveFinal_ = keepAliveRequested
this.firstHeaderPresent_ = false
this.firstHeaderName_ = ''
this.firstHeaderValue_ = ''
this.firstHeaderRawName_ = ''
this.extraHeaderNames_ = undefined
this.extraHeaderValues_ = undefined
this.extraHeaderRawNames_ = undefined
}
get socket(): NetSocket {
return this.request_ === undefined ? placeholderSocket : this.request_.socket
}
get headersSent(): boolean {
return this.headersCommitted_
}
// `writable` is Writable's own plain field (defaults true at construction), not a getter here:
// TypeScript will not let a subclass override a plain inherited property with an accessor
// (TS2611 -- the same rule that keeps http.ts's `Socket.remoteAddress` a plain field instead of
// a lazy getter now, see that class). So `end()` and `destroy()` below flip it to `false`
// directly, at the same points they flip `finished`/`destroyed`, instead of deriving it.
private indexOfHeader(lower: string): number {
if (this.firstHeaderPresent_ && this.firstHeaderName_ === lower) return 0
const names = this.extraHeaderNames_
if (names === undefined) return -1
for (let i = 0; i < names.length; i++) {
if (names[i] === lower) return i + 1
}
return -1
}
private removeHeaderAt(index: number): void {
const names = this.extraHeaderNames_
const values = this.extraHeaderValues_
const lines = this.extraHeaderRawNames_
if (index === 0) {
if (names !== undefined && values !== undefined && lines !== undefined && names.length > 0) {
this.firstHeaderName_ = names[0]
this.firstHeaderValue_ = values[0]
this.firstHeaderRawName_ = lines[0]
names.splice(0, 1)
values.splice(0, 1)
lines.splice(0, 1)
} else {
this.firstHeaderPresent_ = false
this.firstHeaderName_ = ''
this.firstHeaderValue_ = ''
this.firstHeaderRawName_ = ''
}
return
}
if (names !== undefined && values !== undefined && lines !== undefined) {
names.splice(index - 1, 1)
values.splice(index - 1, 1)
lines.splice(index - 1, 1)
}
}
private removeAllOfHeader(lower: string): void {
let index = this.indexOfHeader(lower)
while (index >= 0) {
this.removeHeaderAt(index)
index = this.indexOfHeader(lower)
}
}
// Typed fast path used by writeHead / end / the serializer.
private setHeaderString(name: string, value: string): void {
const lower = lowerHeaderName(name)
const index = this.indexOfHeader(lower)
if (index >= 0) {
const names = this.extraHeaderNames_
const values = this.extraHeaderValues_
const lines = this.extraHeaderRawNames_
if (index === 0) {
this.firstHeaderValue_ = value
this.firstHeaderRawName_ = name
} else if (names !== undefined && values !== undefined && lines !== undefined) {
values[index - 1] = value
lines[index - 1] = name
}
if (names !== undefined && values !== undefined && lines !== undefined) {
for (let i = names.length - 1; i >= 0; i--) {
if (names[i] === lower && i !== index - 1) {
names.splice(i, 1)
values.splice(i, 1)
lines.splice(i, 1)
}
}
}
return
}
this.appendHeaderNormalized(name, lower, value)
}
private appendHeaderString(name: string, value: string): void {
const lower = lowerHeaderName(name)
this.appendHeaderNormalized(name, lower, value)
}
private appendHeaderNormalized(name: string, lower: string, value: string): void {
if (!this.firstHeaderPresent_) {
this.firstHeaderPresent_ = true
this.firstHeaderName_ = lower
this.firstHeaderValue_ = value
this.firstHeaderRawName_ = name
return
}
let names = this.extraHeaderNames_
let values = this.extraHeaderValues_
let lines = this.extraHeaderRawNames_
if (names === undefined || values === undefined || lines === undefined) {
names = []
values = []
lines = []
this.extraHeaderNames_ = names
this.extraHeaderValues_ = values
this.extraHeaderRawNames_ = lines
}
names.push(lower)
values.push(value)
lines.push(name)
}
// Node-compatible surface: accepts a string, number, or array of strings.
setHeader(name: string, value: OutgoingHttpHeader): ServerResponse {
if (Array.isArray(value)) {
// Bind the narrowed array into its own `readonly string[]`-declared
// local. The manifest deliberately carries no computed `get` recipe
// for a tagged union mixing array and primitive arms (only
// uniform-kind unions -- all typed-array, all dictionary, all
// array-object -- have one; see certify/property-access-keys.ts), and
// `value[i]` otherwise still carries the whole union's carrier even
// inside this `Array.isArray` guard.
const values: readonly string[] = value
this.removeAllOfHeader(lowerHeaderName(name))
for (let i = 0; i < values.length; i++) this.appendHeaderString(name, String(values[i]))
return this
}
this.setHeaderString(name, String(value))
return this
}
appendHeader(name: string, value: OutgoingHttpHeader): ServerResponse {
if (Array.isArray(value)) {
// Same narrowing local as setHeader above, for the same reason: `value[i]`
// on the mixed array/primitive union has no manifest recipe.
const values: readonly string[] = value
for (let i = 0; i < values.length; i++) this.appendHeaderString(name, String(values[i]))
return this
}
this.appendHeaderString(name, String(value))
return this
}
// Multi-value headers come back joined with ", " (deviation: Node returns
// the array that was set).
getHeader(name: string): string {
const lower = lowerHeaderName(name)
let out = ''
if (this.firstHeaderPresent_ && this.firstHeaderName_ === lower) out = this.firstHeaderValue_
const names = this.extraHeaderNames_
const values = this.extraHeaderValues_
if (names === undefined || values === undefined) return out
for (let i = 0; i < names.length; i++) {
if (names[i] === lower) out = out === '' ? values[i] : out + ', ' + values[i]
}
return out
}
hasHeader(name: string): boolean {
return this.indexOfHeader(lowerHeaderName(name)) >= 0
}
removeHeader(name: string): void {
this.removeAllOfHeader(lowerHeaderName(name))
}
getHeaderNames(): string[] {
const out: string[] = []
if (this.firstHeaderPresent_) out.push(this.firstHeaderName_)
const names = this.extraHeaderNames_
if (names === undefined) return out
for (let i = 0; i < names.length; i++) {
let seen = false
for (let j = 0; j < out.length; j++) {
if (out[j] === names[i]) {
seen = true
break
}
}
if (!seen) out.push(names[i])
}
return out
}
getHeaders(): { [name: string]: string } {
const out: { [name: string]: string } = {}
if (this.firstHeaderPresent_) out[this.firstHeaderName_] = this.firstHeaderValue_
const names = this.extraHeaderNames_
const values = this.extraHeaderValues_
if (names === undefined || values === undefined) return out
for (let i = 0; i < names.length; i++) {
const name = names[i]
const value = values[i]
if (out.hasOwnProperty(name)) {
out[name] = out[name] + ', ' + value
} else {
out[name] = value
}
}
return out
}
// Serialize the header block, matching Node byte-for-byte: status line, app
// headers in insertion order, Date, Connection (+ Keep-Alive), then the
// auto-computed framing header last. autoContentLength >= 0 is the
// single-shot end() fast path; -1 means streaming (chunked on 1.1,
// close-delimited on 1.0).
//
// SERIALIZED BY THE REACTOR, INTO THE CONNECTION'S OWN BUFFER. This used to
// assemble the block in a `pendingHead_` string field and hand it over with
// the first body bytes. A `ServerResponse` is a new object per request, so
// that field started empty every time and regrew through 15, 30, 60, 120 and
// 240 bytes -- four allocations and four copies per response, measured with
// `GEA_ALLOC_CENSUS=2`, for bytes whose only destination was the connection
// buffer. That buffer is retained across responses, so appending there costs
// the memcpy and nothing else. The block is not flushed by being written:
// it leaves with the first body bytes, as Node's `_header` does.
//
// The framing decision moved with it. It reads four header names
// (content-length, transfer-encoding, date, connection), and the reactor
// sees every name go past on its way into the buffer, so the scan this
// method used to run over the stored headers first is gone rather than
// duplicated. `headEnd` answers the two facts still needed here.
private commitHeaders(autoContentLength: number): void {
if (this.headersCommitted_) return
this.beginHead()
if (this.firstHeaderPresent_) __gea_http_head_field(this.connId_, this.firstHeaderRawName_, this.firstHeaderValue_)
const lines = this.extraHeaderRawNames_
const extraValues = this.extraHeaderValues_
if (lines !== undefined && extraValues !== undefined) {
for (let i = 0; i < lines.length; i++) __gea_http_head_field(this.connId_, lines[i], extraValues[i])
}
this.endHead(autoContentLength)
}
private beginHead(): void {
this.headersCommitted_ = true
const code = this.statusCode
const message = this.statusMessage === '' ? statusText(code) : this.statusMessage
__gea_http_head_begin(this.connId_, code, message)
}
private endHead(autoContentLength: number): void {
const code = this.statusCode
const noBody = this.isHead_ || code === 204 || code === 304 || (code >= 100 && code < 200)
if (noBody) this.suppressBody_ = true
// Bit values are `kHead*` in `runtime/gea_node.cpp`.
let flags = 0
if (noBody) flags += 1
if (this.http10_) flags += 2
if (this.keepAliveRequested_) flags += 4
if (this.sendDate) flags += 8
const framing = __gea_http_head_end(this.connId_, flags, autoContentLength)
this.chunked_ = framing === 1 || framing === 3
this.keepAliveFinal_ = framing >= 2
}
// Hand bytes to the reactor. The header block is already in the connection
// buffer (`commitHeaders`), so the first write releases it along with these
// bytes in one flush; an empty first write still has to reach the reactor
// for that reason.
private emitPayload(data: string): void {
if (!this.headWritten_) {
this.headWritten_ = true
__gea_http_body(this.connId_, data)
return
}
if (data.length > 0) __gea_http_body(this.connId_, data)
}
// The same hand-off for a body that is OCTETS. `prefix` and `suffix` are the
// chunked framing around it (a hex length line, a terminating `0\r\n\r\n`) --
// ASCII by construction, so they stay on the string call; only the body
// itself takes the byte call, because a TypeScript string cannot carry
// arbitrary bytes to the reactor intact.
private emitPayloadBytes(prefix: string, data: Uint8Array, suffix: string): void {
if (!this.headWritten_) {
this.headWritten_ = true
__gea_http_body(this.connId_, prefix)
} else if (prefix.length > 0) __gea_http_body(this.connId_, prefix)
if (data.length > 0) __gea_http_write_bytes(this.connId_, data)
if (suffix.length > 0) __gea_http_body(this.connId_, suffix)
}
writeHead(status: number, headers: OutgoingHttpHeaders = {}): ServerResponse {
this.statusCode = status
if (!this.headersCommitted_ && !this.firstHeaderPresent_) {
// Nothing was staged with `setHeader`, so there is nothing for these
// entries to replace or be merged with, and Node does not store them
// either: `writeHead(200, { 'x-a': '1' })` followed by
// `getHeader('x-a')` answers `undefined` there too. They go straight
// into the header block instead of through the staged-header fields --