@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +57 -14
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import HttpError from "../../errors/httpError.js";
|
|
2
|
+
import { SSE_STALL_TIMEOUT_MS } from "./SseStream.js";
|
|
3
|
+
import { MAX_TIMER_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import { AsyncResource } from "node:async_hooks";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
// nodefony/src/context/http/ByteStream.ts
|
|
8
|
+
/** Erreur d'une source après la fin de son flux : plus personne à prévenir. */
|
|
9
|
+
const IGNORE_LATE_ERROR = () => void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Un flux d'octets en cours sur une réponse — interne : on l'obtient par
|
|
12
|
+
* {@link openByteStream}, qui ne rend que sa promesse de fin.
|
|
13
|
+
*/
|
|
14
|
+
var ByteStream = class {
|
|
15
|
+
#lifecycle;
|
|
16
|
+
#source;
|
|
17
|
+
#settings;
|
|
18
|
+
done;
|
|
19
|
+
#resolve;
|
|
20
|
+
#bytes = 0;
|
|
21
|
+
/** Les en-têtes sont-ils partis ? Posé au premier morceau, ou à la fin. */
|
|
22
|
+
#headed = false;
|
|
23
|
+
/** Raison posée par le flux lui-même ; `null` : le cycle de vie tranchera. */
|
|
24
|
+
#reason = null;
|
|
25
|
+
/** La résolution attend la fin de la requête (fin propre, erreur rendue). */
|
|
26
|
+
#settleLater = false;
|
|
27
|
+
#waitingDrain = false;
|
|
28
|
+
/** Octets vus au dernier tour de garde où la source avait avancé. */
|
|
29
|
+
#seenBytes = 0;
|
|
30
|
+
/** Heure de ce tour (`Date.now()`) — une lecture par tour, jamais par morceau. */
|
|
31
|
+
#seenAt = 0;
|
|
32
|
+
#watchdog = null;
|
|
33
|
+
/** `Content-Length` annoncé, lu une fois aux en-têtes ; `-1` : aucun. */
|
|
34
|
+
#declared = -1;
|
|
35
|
+
/** Requête HEAD : aucun corps, la longueur annoncée est celle du GET. */
|
|
36
|
+
#head = false;
|
|
37
|
+
#onData = (chunk) => this.#write(chunk);
|
|
38
|
+
#onEnd = () => this.#finishClean();
|
|
39
|
+
#onSourceError = (error) => this.#fail(error, "error");
|
|
40
|
+
/** Une source détruite sans erreur ni fin : on la tient pour finie. */
|
|
41
|
+
#onSourceClose = () => {
|
|
42
|
+
if (this.#source.errored === null) this.#finishClean();
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Écouteur `drain` PERMANENT, posé une fois (comme `pipe`) : un `once` par
|
|
46
|
+
* épisode de contre-pression allouait un enrobage par morceau sur une socket
|
|
47
|
+
* rapide, où presque chaque écriture de 64 Kio rend `false`.
|
|
48
|
+
*/
|
|
49
|
+
#onDrain = () => {
|
|
50
|
+
if (!this.#waitingDrain) return;
|
|
51
|
+
this.#waitingDrain = false;
|
|
52
|
+
this.#lifecycle.clearStall();
|
|
53
|
+
this.#source.resume();
|
|
54
|
+
};
|
|
55
|
+
#onIdle = () => this.#checkIdle();
|
|
56
|
+
constructor(lifecycle, source, settings) {
|
|
57
|
+
this.#lifecycle = lifecycle;
|
|
58
|
+
this.#source = source;
|
|
59
|
+
this.#settings = settings;
|
|
60
|
+
this.done = new Promise((resolve) => {
|
|
61
|
+
this.#resolve = resolve;
|
|
62
|
+
});
|
|
63
|
+
lifecycle.onRelease(() => this.#release());
|
|
64
|
+
}
|
|
65
|
+
/** Démarre le flux : HEAD, source déjà finie, ou lecture de la source. */
|
|
66
|
+
start() {
|
|
67
|
+
const lifecycle = this.#lifecycle;
|
|
68
|
+
if (lifecycle.closed) return;
|
|
69
|
+
lifecycle.disarmIdleTimeout();
|
|
70
|
+
const source = this.#source;
|
|
71
|
+
source.on("error", this.#onSourceError);
|
|
72
|
+
if (lifecycle.context.getMethod() === "HEAD") {
|
|
73
|
+
this.#head = true;
|
|
74
|
+
this.#finishClean();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (source.errored !== null) {
|
|
78
|
+
this.#fail(source.errored, "error");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (source.destroyed || source.readableEnded) {
|
|
82
|
+
this.#finishClean();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
lifecycle.target.on("drain", this.#onDrain);
|
|
86
|
+
const idle = this.#settings.idleTimeout;
|
|
87
|
+
if (idle > 0) {
|
|
88
|
+
this.#seenAt = Date.now();
|
|
89
|
+
this.#watchdog = setInterval(this.#onIdle, Math.max(1, Math.floor(idle / 4)));
|
|
90
|
+
this.#watchdog.unref();
|
|
91
|
+
}
|
|
92
|
+
source.once("end", this.#onEnd);
|
|
93
|
+
source.once("close", this.#onSourceClose);
|
|
94
|
+
source.on("data", this.#onData);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* En-têtes au premier morceau (ou à la fin d'une source vide).
|
|
98
|
+
*
|
|
99
|
+
* @param first - octets du premier morceau (0 : source vide).
|
|
100
|
+
* @throws Error quand ce morceau contredit déjà `Content-Length` — levé
|
|
101
|
+
* AVANT `writeHead` : rien n'est parti, la page d'erreur le peut.
|
|
102
|
+
*/
|
|
103
|
+
#writeHead(first) {
|
|
104
|
+
const context = this.#lifecycle.context;
|
|
105
|
+
const { headers, live, status } = this.#settings;
|
|
106
|
+
const response = context.response;
|
|
107
|
+
if (headers !== null) response.setHeaders(headers);
|
|
108
|
+
response.setDefaultHeader("Content-Type", "application/octet-stream");
|
|
109
|
+
if (live) {
|
|
110
|
+
response.setDefaultHeader("Cache-Control", "no-cache, no-transform");
|
|
111
|
+
response.setDefaultHeader("X-Accel-Buffering", "no");
|
|
112
|
+
}
|
|
113
|
+
const length = response.getHeader("content-length");
|
|
114
|
+
if (length !== void 0) {
|
|
115
|
+
const declared = Number(length);
|
|
116
|
+
if (Number.isSafeInteger(declared) && declared >= 0) this.#declared = declared;
|
|
117
|
+
}
|
|
118
|
+
if (this.#declared !== -1 && !this.#head && (first === 0 ? this.#declared !== 0 : first > this.#declared)) throw this.#lengthMismatch(first > this.#declared ? "plus" : "moins");
|
|
119
|
+
context.writeHead(status);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Les en-têtes, sans jamais lever : appelé depuis un écouteur `data` ou
|
|
123
|
+
* `end`, une exception (valeur d'en-tête refusée) n'aurait personne pour la
|
|
124
|
+
* rattraper et abattrait le processus. Un échec passe par le chemin d'erreur
|
|
125
|
+
* d'avant le premier octet — rien n'est parti, la page d'erreur peut l'être.
|
|
126
|
+
*
|
|
127
|
+
* @returns `false` quand les en-têtes n'ont pas pu partir.
|
|
128
|
+
*/
|
|
129
|
+
#tryWriteHead(first) {
|
|
130
|
+
try {
|
|
131
|
+
this.#writeHead(first);
|
|
132
|
+
this.#headed = true;
|
|
133
|
+
return true;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
this.#fail(e instanceof Error ? e : new Error(String(e)), "error");
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
#write(chunk) {
|
|
140
|
+
if (this.#reason !== null || this.#lifecycle.closed) return;
|
|
141
|
+
const size = typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
142
|
+
if (!this.#headed && !this.#tryWriteHead(size)) return;
|
|
143
|
+
if (this.#declared !== -1 && this.#bytes + size > this.#declared) {
|
|
144
|
+
this.#fail(this.#lengthMismatch("plus"), "error");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.#bytes += size;
|
|
148
|
+
if (this.#lifecycle.target.write(chunk)) return;
|
|
149
|
+
this.#source.pause();
|
|
150
|
+
this.#waitingDrain = true;
|
|
151
|
+
this.#lifecycle.armStall();
|
|
152
|
+
}
|
|
153
|
+
/** Fin propre : en-têtes s'ils manquent, puis fin de la requête. */
|
|
154
|
+
#finishClean() {
|
|
155
|
+
const lifecycle = this.#lifecycle;
|
|
156
|
+
if (this.#reason !== null || lifecycle.closed) return;
|
|
157
|
+
if (!this.#headed && !this.#tryWriteHead(0)) return;
|
|
158
|
+
if (!this.#head && this.#declared !== -1 && this.#bytes !== this.#declared) {
|
|
159
|
+
this.#fail(this.#lengthMismatch("moins"), "error");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.#reason = "end";
|
|
163
|
+
this.#settleLater = true;
|
|
164
|
+
const closed = lifecycle.close();
|
|
165
|
+
if (closed === void 0) this.#settle();
|
|
166
|
+
else closed.then(this.#settleBound, this.#settleBound);
|
|
167
|
+
}
|
|
168
|
+
#settleBound = () => this.#settle();
|
|
169
|
+
/** L'erreur d'un corps qui ne fait pas la longueur annoncée. */
|
|
170
|
+
#lengthMismatch(direction) {
|
|
171
|
+
return /* @__PURE__ */ new Error(`Flux : la source rend ${direction} d'octets que Content-Length n'en annonce (${this.#declared})`);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Échec de la source, ou source muette. Avant le premier octet, la réponse
|
|
175
|
+
* est ROUVERTE et l'erreur confiée au moteur d'erreurs du noyau — même
|
|
176
|
+
* statut, même page qu'une action ordinaire. Après, le client voit une
|
|
177
|
+
* coupure, jamais une fin propre.
|
|
178
|
+
*/
|
|
179
|
+
#fail(failure, reason) {
|
|
180
|
+
const lifecycle = this.#lifecycle;
|
|
181
|
+
if (this.#reason !== null || lifecycle.closed) return;
|
|
182
|
+
this.#reason = reason;
|
|
183
|
+
const context = lifecycle.context;
|
|
184
|
+
if (!this.#headed && !context.response.isHeaderSent()) {
|
|
185
|
+
this.#settleLater = true;
|
|
186
|
+
lifecycle.finish();
|
|
187
|
+
const kernel = context.httpKernel;
|
|
188
|
+
if (kernel === null) {
|
|
189
|
+
this.#settle();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
context.sended = false;
|
|
193
|
+
this.#renderError(kernel, failure);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (reason === "error") context.log(failure, "ERROR", "STREAM");
|
|
197
|
+
context.response.statusCode = "code" in failure && typeof failure.code === "number" && failure.code >= 400 && failure.code <= 599 ? failure.code : 500;
|
|
198
|
+
const target = lifecycle.target;
|
|
199
|
+
if (target instanceof http.ServerResponse) target.destroy();
|
|
200
|
+
else target.destroy(failure);
|
|
201
|
+
lifecycle.finish();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Rend l'erreur d'avant le premier octet par le moteur d'erreurs du noyau,
|
|
205
|
+
* puis règle la promesse — qui ne rejette jamais, même si ce rendu échoue.
|
|
206
|
+
*/
|
|
207
|
+
async #renderError(kernel, failure) {
|
|
208
|
+
const context = this.#lifecycle.context;
|
|
209
|
+
try {
|
|
210
|
+
await kernel.onError(failure, context);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
context.log(e, "ERROR", "STREAM");
|
|
213
|
+
}
|
|
214
|
+
this.#settle();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Un tour du chien de garde, tous les quarts de `idleTimeout`. La source a
|
|
218
|
+
* avancé depuis le tour précédent : on note l'heure. Sinon, et depuis au
|
|
219
|
+
* moins `idleTimeout` : la source est muette.
|
|
220
|
+
*/
|
|
221
|
+
#checkIdle() {
|
|
222
|
+
if (this.#reason !== null || this.#lifecycle.closed) return;
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (this.#bytes !== this.#seenBytes || this.#waitingDrain) {
|
|
225
|
+
this.#seenBytes = this.#bytes;
|
|
226
|
+
this.#seenAt = now;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const idle = this.#settings.idleTimeout;
|
|
230
|
+
if (now - this.#seenAt < idle) return;
|
|
231
|
+
this.#fail(new HttpError(`Flux : source muette depuis ${idle} ms`, 504), "timeout");
|
|
232
|
+
}
|
|
233
|
+
/** Nettoyage, appelé UNE fois par le cycle de vie, quelle qu'en soit la cause. */
|
|
234
|
+
#release() {
|
|
235
|
+
const lifecycle = this.#lifecycle;
|
|
236
|
+
const reason = this.#reason ?? (lifecycle.cause === "stall" ? "stall" : lifecycle.cause === "server" ? "end" : "client");
|
|
237
|
+
this.#reason = reason;
|
|
238
|
+
const source = this.#source;
|
|
239
|
+
source.removeListener("data", this.#onData);
|
|
240
|
+
source.removeListener("end", this.#onEnd);
|
|
241
|
+
source.removeListener("close", this.#onSourceClose);
|
|
242
|
+
source.on("error", IGNORE_LATE_ERROR);
|
|
243
|
+
source.removeListener("error", this.#onSourceError);
|
|
244
|
+
source.destroy();
|
|
245
|
+
lifecycle.target.removeListener("drain", this.#onDrain);
|
|
246
|
+
if (this.#watchdog !== null) {
|
|
247
|
+
clearInterval(this.#watchdog);
|
|
248
|
+
this.#watchdog = null;
|
|
249
|
+
}
|
|
250
|
+
const context = lifecycle.context;
|
|
251
|
+
if (reason !== "end") context._abortIfPending(`Flux terminé : ${reason}`);
|
|
252
|
+
context.log(`Flux d'octets terminé (${reason}, ${this.#bytes} octets)`, "DEBUG", "STREAM");
|
|
253
|
+
const onClose = this.#settings.onClose;
|
|
254
|
+
if (onClose !== null) try {
|
|
255
|
+
onClose(reason);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
context.log(e, "ERROR", "STREAM");
|
|
258
|
+
}
|
|
259
|
+
if (!this.#settleLater) this.#settle();
|
|
260
|
+
}
|
|
261
|
+
#settle() {
|
|
262
|
+
this.#resolve({
|
|
263
|
+
reason: this.#reason ?? "end",
|
|
264
|
+
bytes: this.#bytes
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Un flux web (`fetch().body`) lu comme un flux Node.
|
|
270
|
+
*
|
|
271
|
+
* Pas `Readable.fromWeb` : il attend le `ReadableStream` de `node:stream/web`,
|
|
272
|
+
* que le type global d'une application compilée avec la lib DOM ne satisfait
|
|
273
|
+
* pas. La lecture tient en un lecteur, et la destruction ANNULE le flux web —
|
|
274
|
+
* c'est elle qui ferme la connexion amont d'un `fetch`.
|
|
275
|
+
*/
|
|
276
|
+
function fromWebStream(stream) {
|
|
277
|
+
const reader = stream.getReader();
|
|
278
|
+
const onRead = (result) => {
|
|
279
|
+
if (result.done) readable.push(null);
|
|
280
|
+
else readable.push(result.value);
|
|
281
|
+
};
|
|
282
|
+
const onFail = (error) => {
|
|
283
|
+
readable.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
284
|
+
};
|
|
285
|
+
const readable = new Readable({
|
|
286
|
+
read() {
|
|
287
|
+
reader.read().then(onRead, onFail);
|
|
288
|
+
},
|
|
289
|
+
destroy(failure, callback) {
|
|
290
|
+
reader.cancel(failure ?? void 0).catch(IGNORE_LATE_ERROR);
|
|
291
|
+
callback(failure);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
return readable;
|
|
295
|
+
}
|
|
296
|
+
/** Refuse une option de flux d'octets hors bornes. */
|
|
297
|
+
function checkOptions(options) {
|
|
298
|
+
const { status, idleTimeout } = options;
|
|
299
|
+
if (status !== void 0 && (!Number.isInteger(status) || status < 200 || status > 599)) throw new RangeError("Flux : status entier entre 200 et 599 attendu");
|
|
300
|
+
if (idleTimeout !== void 0 && (!Number.isInteger(idleTimeout) || idleTimeout < 0 || idleTimeout > 2147483647)) throw new RangeError(`Flux : idleTimeout entier entre 0 et ${MAX_TIMER_MS} ms attendu`);
|
|
301
|
+
checkStreamTimer("Flux", "stallTimeout", options.stallTimeout);
|
|
302
|
+
checkStreamTimer("Flux", "maxDuration", options.maxDuration);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Répond par un flux d'octets sur la réponse d'un contexte HTTP.
|
|
306
|
+
*
|
|
307
|
+
* Prend la réponse à l'appel (le noyau ne peut plus répondre par-dessus) ;
|
|
308
|
+
* les en-têtes partent au premier morceau de la source. La promesse se règle à
|
|
309
|
+
* la fin du flux, quelle qu'en soit la raison, et ne rejette JAMAIS.
|
|
310
|
+
*
|
|
311
|
+
* @param context - le contexte de la requête, pas encore répondu.
|
|
312
|
+
* @param source - flux Node, ou flux web (`fetch().body`) ; jamais en mode objet.
|
|
313
|
+
* @param options - statut, en-têtes, délais, écouteur de fin.
|
|
314
|
+
* @returns la raison de la fin et le nombre d'octets envoyés.
|
|
315
|
+
* @throws TypeError quand la source est en mode objet.
|
|
316
|
+
* @throws RangeError quand une option est hors bornes.
|
|
317
|
+
* @throws HttpError 500 quand la réponse est déjà partie ; 429 quand l'IP a
|
|
318
|
+
* déjà tous ses flux et sockets (`wsMaxConnectionsPerIp`).
|
|
319
|
+
*
|
|
320
|
+
* La source appartient au flux dès l'appel : quand l'appel lève, elle est
|
|
321
|
+
* DÉTRUITE (un flux web est annulé) avant que l'erreur ne remonte.
|
|
322
|
+
*/
|
|
323
|
+
function openByteStream(context, source, options = {}) {
|
|
324
|
+
const readable = source instanceof Readable ? source : fromWebStream(source);
|
|
325
|
+
let opened;
|
|
326
|
+
let settings;
|
|
327
|
+
try {
|
|
328
|
+
checkOptions(options);
|
|
329
|
+
if (readable.readableObjectMode) throw new TypeError("Flux : une source en mode objet ne porte pas d'octets — pour Readable.from(), passer { objectMode: false }.");
|
|
330
|
+
settings = settingsOf(context, options);
|
|
331
|
+
opened = openStreamLifecycle(context, {
|
|
332
|
+
label: "Flux",
|
|
333
|
+
stallTimeout: options.stallTimeout ?? SSE_STALL_TIMEOUT_MS,
|
|
334
|
+
maxDuration: options.maxDuration ?? false,
|
|
335
|
+
endsAtShutdown: settings.live
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
readable.on("error", IGNORE_LATE_ERROR);
|
|
339
|
+
readable.destroy();
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
const start = (lifecycle) => {
|
|
343
|
+
const stream = new ByteStream(lifecycle, readable, settings);
|
|
344
|
+
stream.start();
|
|
345
|
+
return stream.done;
|
|
346
|
+
};
|
|
347
|
+
if (opened instanceof Promise) {
|
|
348
|
+
readable.on("error", IGNORE_LATE_ERROR);
|
|
349
|
+
return opened.then(start);
|
|
350
|
+
}
|
|
351
|
+
return start(opened);
|
|
352
|
+
}
|
|
353
|
+
/** Les réglages du flux, défauts résolus sur le contexte. */
|
|
354
|
+
function settingsOf(context, options) {
|
|
355
|
+
const timeout = context.response.timeout;
|
|
356
|
+
return {
|
|
357
|
+
status: options.status ?? 200,
|
|
358
|
+
headers: options.headers ?? null,
|
|
359
|
+
live: options.live ?? true,
|
|
360
|
+
idleTimeout: options.idleTimeout ?? (typeof timeout === "number" ? timeout : 3e4),
|
|
361
|
+
onClose: options.onClose === void 0 ? null : AsyncResource.bind(options.onClose)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
export { openByteStream };
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import HttpError from "../../errors/httpError.js";
|
|
2
|
-
import { responseEnded } from "../responseEnded.js";
|
|
3
|
-
import { alpnProtocolOf } from "../transport.js";
|
|
4
|
-
import Cookie from "../../cookies/cookie.js";
|
|
5
|
-
import Context from "../Context.js";
|
|
6
2
|
import { sanitizeRequestId } from "../requestId.js";
|
|
3
|
+
import Context from "../Context.js";
|
|
7
4
|
import HttpRequest from "./Request.js";
|
|
8
5
|
import HttpResponse from "./Response.js";
|
|
9
6
|
import Http2Request from "../http2/Request.js";
|
|
10
7
|
import Http2Response from "../http2/Response.js";
|
|
8
|
+
import Cookie from "../../cookies/cookie.js";
|
|
11
9
|
import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
|
|
10
|
+
import { responseEnded } from "../responseEnded.js";
|
|
11
|
+
import { alpnProtocolOf } from "../transport.js";
|
|
12
12
|
import { openSseStream } from "./SseStream.js";
|
|
13
|
+
import { openByteStream } from "./ByteStream.js";
|
|
14
|
+
import { webResponseParts } from "./webResponse.js";
|
|
13
15
|
import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
|
|
16
|
+
import net from "node:net";
|
|
14
17
|
import url, { URL } from "node:url";
|
|
15
18
|
// nodefony/src/context/http/HttpContext.ts
|
|
16
19
|
/**
|
|
@@ -29,25 +32,35 @@ function switchUrlScheme(from, scheme, port) {
|
|
|
29
32
|
return target.href;
|
|
30
33
|
}
|
|
31
34
|
const socketActiveContext = /* @__PURE__ */ new WeakMap();
|
|
32
|
-
const socketTimeoutArmed = /* @__PURE__ */ new WeakSet();
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Route le délai d'inactivité d'une socket HTTP/1.1 vers la requête qu'elle
|
|
37
|
+
* porte : 503 si une requête est en cours, fermeture si la connexion
|
|
38
|
+
* persistante est inactive. Écouteur `timeout` du SERVEUR — un seul pour
|
|
39
|
+
* toutes ses connexions —, que Node appelle avec la socket en argument
|
|
40
|
+
* (`socketOnTimeout` → `server.emit("timeout", socket)`).
|
|
41
|
+
*
|
|
42
|
+
* Pourquoi au serveur et pas sur la socket : Node y pose déjà son propre
|
|
43
|
+
* écouteur `timeout`, et un second faisait créer à `EventEmitter` un tableau
|
|
44
|
+
* d'écouteurs par CONNEXION. Ce site d'allocation, partagé avec tout le
|
|
45
|
+
* processus et toujours survivant (une connexion vit longtemps), finissait
|
|
46
|
+
* par être basculé dans le vieux tas par V8 (pretenuring) — dans un démarrage
|
|
47
|
+
* sur trois environ, mesuré sous charge. Le graphe de chaque requête y
|
|
48
|
+
* restait alors accroché jusqu'à la collecte complète suivante : +45 % de CPU
|
|
49
|
+
* par requête, 14 à 19 collectes complètes au lieu de 3. Un écouteur par
|
|
50
|
+
* serveur supprime aussi une fermeture et une entrée de registre par connexion.
|
|
38
51
|
*
|
|
39
|
-
* @param socket - la socket
|
|
40
|
-
*
|
|
52
|
+
* @param socket - la socket dont le délai a expiré ; une session HTTP/2 (même
|
|
53
|
+
* événement sur un serveur `allowHTTP1`) est fermée par l'écouteur que pose
|
|
54
|
+
* `setHttp2SessionTimeout` (`src/servers/http2Limits.ts`)
|
|
41
55
|
*/
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
56
|
+
function routeSocketTimeout(socket) {
|
|
57
|
+
if (!(socket instanceof net.Socket)) return;
|
|
58
|
+
const ctx = socketActiveContext.get(socket);
|
|
59
|
+
if (ctx !== void 0 && !ctx.cleaned && !ctx.response.response?.writableEnded) {
|
|
60
|
+
ctx._onTimeout();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
socket.destroy();
|
|
51
64
|
}
|
|
52
65
|
const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
|
|
53
66
|
var HttpContext = class extends Context {
|
|
@@ -61,6 +74,15 @@ var HttpContext = class extends Context {
|
|
|
61
74
|
* son montage. `null` = servie par l'application (cas de toute requête).
|
|
62
75
|
*/
|
|
63
76
|
relayAccessLog = null;
|
|
77
|
+
/**
|
|
78
|
+
* L'appel RPC servi sur cette requête (gRPC, gRPC-Web, Connect), `null`
|
|
79
|
+
* hors appel RPC (ADR-0026 D10) : un champ, aucune allocation. Posé par le
|
|
80
|
+
* point d'entrée gRPC ; à l'échéance du délai de réponse, {@link _onTimeout}
|
|
81
|
+
* le termine au lieu d'écrire un 503.
|
|
82
|
+
*
|
|
83
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
84
|
+
*/
|
|
85
|
+
rpc = null;
|
|
64
86
|
request;
|
|
65
87
|
response;
|
|
66
88
|
uploadService;
|
|
@@ -131,11 +153,34 @@ var HttpContext = class extends Context {
|
|
|
131
153
|
_onTimeout() {
|
|
132
154
|
if (this.listenerCount("onTimeout")) this.fire("onTimeout", this);
|
|
133
155
|
this._abortIfPending("Request timeout");
|
|
156
|
+
if (this.rpc !== null) {
|
|
157
|
+
this.rpc.expire();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
134
160
|
this.httpKernel?.warnUnsentResponse(this);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
161
|
+
this.httpKernel?.onError(new HttpError("Service Unavailable", 503, this), this);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Ré-arme le délai de réponse de la requête à la valeur propre de sa route
|
|
165
|
+
* (`responseTimeout` d'une option de route), une fois celle-ci résolue.
|
|
166
|
+
*
|
|
167
|
+
* Même mécanisme que {@link setTimeout} — délai d'inactivité de la socket
|
|
168
|
+
* (HTTP/1.1) ou du flux (HTTP/2) —, sans second écouteur : sous HTTP/2,
|
|
169
|
+
* `setTimeout(ms)` sans rappel remplace le minuteur et garde celui posé par
|
|
170
|
+
* {@link setTimeout}. `0` désactive le délai pour la requête.
|
|
171
|
+
*
|
|
172
|
+
* @param ms - délai de la route, en millisecondes.
|
|
173
|
+
*/
|
|
174
|
+
rearmTimeout(ms) {
|
|
175
|
+
this.response.setTimeout(ms);
|
|
176
|
+
const res = this.response.response;
|
|
177
|
+
if (!res) return;
|
|
178
|
+
if (this.response.stream) {
|
|
179
|
+
res.setTimeout(ms);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const socket = this.activeSocket;
|
|
183
|
+
if (socket && socket.timeout !== ms) socket.setTimeout(ms);
|
|
139
184
|
}
|
|
140
185
|
setScheme() {
|
|
141
186
|
const req = this.request;
|
|
@@ -162,6 +207,8 @@ var HttpContext = class extends Context {
|
|
|
162
207
|
if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
|
|
163
208
|
if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
|
|
164
209
|
if (this.resolver?.resolve) {
|
|
210
|
+
const routeTimeout = this.resolver.route?.responseTimeout;
|
|
211
|
+
if (routeTimeout !== void 0 && routeTimeout !== null) this.rearmTimeout(routeTimeout);
|
|
165
212
|
this.setMetaData();
|
|
166
213
|
return this.resolver.callController();
|
|
167
214
|
}
|
|
@@ -182,27 +229,38 @@ var HttpContext = class extends Context {
|
|
|
182
229
|
this.activeSocket = socket;
|
|
183
230
|
const ms = this.response.timeout;
|
|
184
231
|
if (socket.timeout !== ms) socket.setTimeout(ms);
|
|
185
|
-
if (!socketTimeoutArmed.has(socket)) {
|
|
186
|
-
socketTimeoutArmed.add(socket);
|
|
187
|
-
socket.on("timeout", idleSocketTimeoutHandler(socket));
|
|
188
|
-
}
|
|
189
232
|
}
|
|
190
233
|
/**
|
|
191
234
|
* Ligne d'accès — pour une requête RELAYÉE, avec le statut que l'amont a
|
|
192
235
|
* réellement rendu (le relais écrit la réponse brute, pas celle du
|
|
193
236
|
* contexte) ; un montage `accessLog: "errors"` ne l'écrit qu'en échec.
|
|
194
237
|
*
|
|
238
|
+
* Le statut natif n'est recopié que si la réponse a été TERMINÉE
|
|
239
|
+
* ({@link responseEnded}) : sinon il n'a jamais atteint le client (200 par
|
|
240
|
+
* défaut de Node), et le statut du contexte — 499 d'un client parti — dit
|
|
241
|
+
* vrai. Règle unique pour toute réponse écrite par un tiers (#695, critère
|
|
242
|
+
* 38).
|
|
243
|
+
*
|
|
195
244
|
* @param httpError - l'erreur de la requête, s'il y en a une
|
|
196
245
|
*/
|
|
197
246
|
logRequest(httpError) {
|
|
247
|
+
if (this.rpc !== null) this.adoptNativeStatus();
|
|
198
248
|
if (this.relayAccessLog !== null) {
|
|
199
|
-
|
|
200
|
-
if (raw !== null) this.response.statusCode = raw.statusCode;
|
|
249
|
+
this.adoptNativeStatus();
|
|
201
250
|
if (this.relayAccessLog === "errors" && this.response.statusCode < 400 && !httpError && !this.error) return;
|
|
202
251
|
}
|
|
203
252
|
return super.logRequest(httpError);
|
|
204
253
|
}
|
|
205
254
|
/**
|
|
255
|
+
* Reprend le statut de la réponse native, écrite par un tiers (relais,
|
|
256
|
+
* adaptateur RPC) — si la réponse est TERMINÉE seulement : sinon ce statut
|
|
257
|
+
* n'a jamais atteint le client, et celui du contexte (499) dit vrai.
|
|
258
|
+
*/
|
|
259
|
+
adoptNativeStatus() {
|
|
260
|
+
const raw = this.response.response;
|
|
261
|
+
if (raw !== null && responseEnded(raw)) this.response.statusCode = raw.statusCode;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
206
264
|
* Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
|
|
207
265
|
* persistante survit à la requête, et l'entrée de `socketActiveContext`
|
|
208
266
|
* retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
|
|
@@ -346,6 +404,7 @@ var HttpContext = class extends Context {
|
|
|
346
404
|
*
|
|
347
405
|
* La requête ne se termine qu'à `close()` du flux, ou au départ du client.
|
|
348
406
|
*
|
|
407
|
+
* @typeParam Events - carte des événements du flux (cf `SseStream`).
|
|
349
408
|
* @param options - battement de cœur, délai `retry:` initial.
|
|
350
409
|
* @returns le flux, ou sa promesse quand une session est à sauver d'abord.
|
|
351
410
|
* @throws HttpError 500 quand la réponse est déjà partie.
|
|
@@ -353,6 +412,54 @@ var HttpContext = class extends Context {
|
|
|
353
412
|
openSse(options) {
|
|
354
413
|
return openSseStream(this, options);
|
|
355
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Répond par un FLUX D'OCTETS : un `<video src>`, un `fetch()`, un
|
|
417
|
+
* téléchargement ou un outil tiers (curl) le consomment comme n'importe quelle
|
|
418
|
+
* réponse HTTP, une réponse par consommateur.
|
|
419
|
+
*
|
|
420
|
+
* La réponse est prise à l'appel, ses en-têtes partent au premier morceau de
|
|
421
|
+
* la source. La source est DÉTRUITE à la fin, quelle qu'en soit la raison
|
|
422
|
+
* (jamais `unpipe`, qui laisse un producteur bloqué en écriture). Pour des
|
|
423
|
+
* événements texte lus par `EventSource`, voir {@link HttpContext.openSse}.
|
|
424
|
+
* Dans un middleware, la requête est répondue : ne pas appeler la suite.
|
|
425
|
+
*
|
|
426
|
+
* @param source - flux Node, ou flux web (`fetch().body`) ; jamais en mode objet.
|
|
427
|
+
* @param options - statut, en-têtes, délais, écouteur de fin.
|
|
428
|
+
* @returns la raison de la fin et le nombre d'octets ; ne rejette jamais.
|
|
429
|
+
* @throws HttpError 500 quand la réponse est déjà partie, 429 au-delà du
|
|
430
|
+
* plafond de connexions par IP ; TypeError pour une source en mode objet.
|
|
431
|
+
* Quand l'appel lève, la source est DÉTRUITE (un flux web est annulé).
|
|
432
|
+
*/
|
|
433
|
+
openStream(source, options) {
|
|
434
|
+
return openByteStream(this, source, options);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Répond par une `Response` Web standard — celle que rend `fetch()`, ou une
|
|
438
|
+
* réponse construite (`new Response(…)`, `Response.json(…)`) : son statut,
|
|
439
|
+
* ses en-têtes, et son corps EN FLUX, jamais lu d'avance.
|
|
440
|
+
*
|
|
441
|
+
* C'est ce que fait le framework quand une action renvoie une `Response`.
|
|
442
|
+
* Une réponse relayée (issue de `fetch`) perd `content-encoding`,
|
|
443
|
+
* `content-length` et `set-cookie` de l'amont ; une réponse construite garde
|
|
444
|
+
* les siens. Les en-têtes de connexion ne partent jamais. Le reste — arrêt au
|
|
445
|
+
* départ du client, contre-pression, délais, HEAD sans lire le corps — est
|
|
446
|
+
* celui de {@link HttpContext.openStream}.
|
|
447
|
+
*
|
|
448
|
+
* @param response - une `Response` dont le corps n'a pas été consommé.
|
|
449
|
+
* @param options - délais et écouteur de fin ; statut et en-têtes viennent de la réponse.
|
|
450
|
+
* @returns la raison de la fin et le nombre d'octets ; ne rejette jamais.
|
|
451
|
+
* @throws HttpError 502 pour `Response.error()` (statut 0, aucune réponse) ;
|
|
452
|
+
* les cas de {@link HttpContext.openStream} sinon.
|
|
453
|
+
*/
|
|
454
|
+
openWebResponse(response, options) {
|
|
455
|
+
if (response.type === "error") throw new HttpError("Réponse Web en erreur : Response.error() ne porte aucune réponse", 502, this);
|
|
456
|
+
const parts = webResponseParts(response);
|
|
457
|
+
return openByteStream(this, parts.source, {
|
|
458
|
+
...options,
|
|
459
|
+
status: parts.status,
|
|
460
|
+
headers: parts.headers
|
|
461
|
+
});
|
|
462
|
+
}
|
|
356
463
|
flush(chunk, encoding) {
|
|
357
464
|
return this.response.flush(chunk, encoding);
|
|
358
465
|
}
|
|
@@ -414,7 +521,7 @@ var HttpContext = class extends Context {
|
|
|
414
521
|
return this.request.getMethod();
|
|
415
522
|
}
|
|
416
523
|
setContentType(type, encoding) {
|
|
417
|
-
|
|
524
|
+
this.response.setContentType(type, encoding);
|
|
418
525
|
}
|
|
419
526
|
resolveIsHtml() {
|
|
420
527
|
return this.request.acceptHtml;
|
|
@@ -437,4 +544,4 @@ var HttpContext = class extends Context {
|
|
|
437
544
|
this.response.setContentType("html", encoding);
|
|
438
545
|
}
|
|
439
546
|
};
|
|
440
|
-
export { HttpContext as default, switchUrlScheme };
|
|
547
|
+
export { HttpContext as default, routeSocketTimeout, switchUrlScheme };
|