@nifrajs/core 2.9.0 → 2.10.0
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 +17 -0
- package/dist/channel.d.ts +52 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +231 -0
- package/dist/channel.js.map +1 -0
- package/dist/data.d.ts +117 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +163 -0
- package/dist/data.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/range.d.ts +34 -0
- package/dist/range.d.ts.map +1 -0
- package/dist/range.js +91 -0
- package/dist/range.js.map +1 -0
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +17 -5
- package/dist/router/router.js.map +1 -1
- package/dist/server/platform-adapters.d.ts +80 -0
- package/dist/server/platform-adapters.d.ts.map +1 -0
- package/dist/server/platform-adapters.js +172 -0
- package/dist/server/platform-adapters.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +17 -2
- package/dist/server/server.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/package.json +16 -1
- package/src/channel.ts +299 -0
- package/src/data.ts +292 -0
- package/src/index.ts +1 -1
- package/src/range.ts +94 -0
- package/src/router/router.ts +16 -5
- package/src/server/platform-adapters.ts +255 -0
- package/src/server/server.ts +14 -2
- package/src/server.ts +15 -0
package/src/range.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Range: bytes=...` parsing - the shared owner for every byte-range surface in the framework.
|
|
3
|
+
*
|
|
4
|
+
* This lives in core rather than in the middleware package because two unrelated layers need the
|
|
5
|
+
* exact same answer: `@nifrajs/middleware`'s `rangeResponse` for caller-owned buffers, and
|
|
6
|
+
* `@nifrajs/web`'s `public/` static handler for files on disk. A second implementation that happens
|
|
7
|
+
* to agree today is how a media server ends up serving byte 0 of a seek request in one code path and
|
|
8
|
+
* byte N in the other.
|
|
9
|
+
*
|
|
10
|
+
* The input is request-controlled on every call, so the parser is bounded before it is clever: a
|
|
11
|
+
* capped header length and a capped specifier count keep a malformed range-set from turning a sort
|
|
12
|
+
* and merge into an allocation amplifier.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface ByteRange {
|
|
16
|
+
readonly start: number
|
|
17
|
+
readonly end: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ByteRangeResult =
|
|
21
|
+
| { readonly kind: "none" }
|
|
22
|
+
| { readonly kind: "unsatisfiable" }
|
|
23
|
+
| { readonly kind: "satisfiable"; readonly ranges: readonly ByteRange[] }
|
|
24
|
+
|
|
25
|
+
const INTEGER = /^(?:0|[1-9]\d*)$/
|
|
26
|
+
const MAX_RANGE_HEADER_LENGTH = 16 * 1024
|
|
27
|
+
const MAX_RANGE_SPECIFIERS = 16
|
|
28
|
+
|
|
29
|
+
function validSize(size: number): void {
|
|
30
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
31
|
+
throw new RangeError("parseByteRange: size must be a non-negative safe integer")
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse an HTTP `Range: bytes=...` header against a known representation size.
|
|
37
|
+
*
|
|
38
|
+
* Three outcomes, and the distinction matters to the caller's status code: `none` means the header
|
|
39
|
+
* was absent or syntactically invalid and must be ignored (200), `unsatisfiable` means a well-formed
|
|
40
|
+
* range-set selected nothing (416), `satisfiable` carries the coalesced ranges (206).
|
|
41
|
+
*/
|
|
42
|
+
export function parseByteRange(value: string | null, size: number): ByteRangeResult {
|
|
43
|
+
validSize(size)
|
|
44
|
+
if (value === null) return { kind: "none" }
|
|
45
|
+
const match = /^bytes\s*=\s*(.+)$/i.exec(value)
|
|
46
|
+
if (match === null) return { kind: "none" }
|
|
47
|
+
const source = match[1]!
|
|
48
|
+
if (source.length > MAX_RANGE_HEADER_LENGTH) return { kind: "none" }
|
|
49
|
+
let separators = 0
|
|
50
|
+
for (const character of source) {
|
|
51
|
+
if (character === "," && ++separators >= MAX_RANGE_SPECIFIERS) return { kind: "none" }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ranges: ByteRange[] = []
|
|
55
|
+
let validRangeSpec = false
|
|
56
|
+
for (const raw of source.split(",")) {
|
|
57
|
+
const item = raw.trim()
|
|
58
|
+
const dash = item.indexOf("-")
|
|
59
|
+
if (dash < 0) continue
|
|
60
|
+
const left = item.slice(0, dash).trim()
|
|
61
|
+
const right = item.slice(dash + 1).trim()
|
|
62
|
+
if (left === "" && (!INTEGER.test(right) || right === "0")) continue
|
|
63
|
+
if (left !== "" && !INTEGER.test(left)) continue
|
|
64
|
+
if (right !== "" && !INTEGER.test(right)) continue
|
|
65
|
+
validRangeSpec = true
|
|
66
|
+
|
|
67
|
+
let start: number
|
|
68
|
+
let end: number
|
|
69
|
+
if (left === "") {
|
|
70
|
+
const suffix = Number(right)
|
|
71
|
+
start = suffix >= size ? 0 : size - suffix
|
|
72
|
+
end = size - 1
|
|
73
|
+
} else {
|
|
74
|
+
start = Number(left)
|
|
75
|
+
end = right === "" ? size - 1 : Math.min(Number(right), size - 1)
|
|
76
|
+
}
|
|
77
|
+
if (start > end || start >= size || size === 0) continue
|
|
78
|
+
ranges.push({ start, end })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A syntactically invalid Range header is ignored; a valid range-set with no overlap is 416.
|
|
82
|
+
if (ranges.length === 0) return validRangeSpec ? { kind: "unsatisfiable" } : { kind: "none" }
|
|
83
|
+
ranges.sort((a, b) => a.start - b.start || a.end - b.end)
|
|
84
|
+
const merged: ByteRange[] = []
|
|
85
|
+
for (const range of ranges) {
|
|
86
|
+
const previous = merged[merged.length - 1]
|
|
87
|
+
if (previous !== undefined && range.start <= previous.end + 1) {
|
|
88
|
+
merged[merged.length - 1] = { start: previous.start, end: Math.max(previous.end, range.end) }
|
|
89
|
+
} else {
|
|
90
|
+
merged.push(range)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { kind: "satisfiable", ranges: merged }
|
|
94
|
+
}
|
package/src/router/router.ts
CHANGED
|
@@ -230,7 +230,9 @@ function resolve<T>(
|
|
|
230
230
|
const upper = method.toUpperCase()
|
|
231
231
|
const cached = upper === method ? undefined : terminal.staticMatches.get(upper)
|
|
232
232
|
if (cached !== undefined) return cached
|
|
233
|
-
|
|
233
|
+
const cacheable =
|
|
234
|
+
terminal.handlers.has(upper) || (upper === "HEAD" && terminal.handlers.has("GET"))
|
|
235
|
+
if (!cacheable) return resolveDirect(terminal, method, params)
|
|
234
236
|
|
|
235
237
|
const res = resolveDirect(terminal, method, params)
|
|
236
238
|
terminal.staticMatches.set(upper, res)
|
|
@@ -248,11 +250,20 @@ function resolveDirect<T>(
|
|
|
248
250
|
return { found: true, payload: terminal.handlers.get(method)!, params }
|
|
249
251
|
}
|
|
250
252
|
const upper = method.toUpperCase()
|
|
251
|
-
if (upper
|
|
252
|
-
return { found:
|
|
253
|
+
if (upper !== method && terminal.handlers.has(upper)) {
|
|
254
|
+
return { found: true, payload: terminal.handlers.get(upper)!, params }
|
|
253
255
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
// RFC 9110 §9.3.2: HEAD is GET without the body. A route registered for GET answers HEAD with
|
|
257
|
+
// the GET handler; the serving runtime strips the body, so status + headers mirror GET exactly.
|
|
258
|
+
// An explicitly registered HEAD handler takes precedence via the has() checks above.
|
|
259
|
+
if (upper === "HEAD") {
|
|
260
|
+
const get = terminal.handlers.get("GET")
|
|
261
|
+
if (get !== undefined) return { found: true, payload: get, params }
|
|
262
|
+
}
|
|
263
|
+
const allowed = [...terminal.handlers.keys()]
|
|
264
|
+
// Advertise the implicit HEAD support granted above.
|
|
265
|
+
if (terminal.handlers.has("GET") && !terminal.handlers.has("HEAD")) allowed.push("HEAD")
|
|
266
|
+
return { found: false, reason: "method-not-allowed", allowed }
|
|
256
267
|
}
|
|
257
268
|
|
|
258
269
|
/**
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin adapters for platforms that do not expose a Web `fetch` handler directly.
|
|
3
|
+
*
|
|
4
|
+
* The app remains a Web-standard handler. These adapters only translate the platform event envelope;
|
|
5
|
+
* they do not own identity, persistence, retries, or platform credentials.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Platform } from "./context.ts"
|
|
9
|
+
import type { MaybePromise } from "./server.ts"
|
|
10
|
+
|
|
11
|
+
export interface FetchApp<Env = unknown> {
|
|
12
|
+
fetch(request: Request, platform?: Platform<Env>): MaybePromise<Response>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type VercelHandler = (request: Request) => MaybePromise<Response>
|
|
16
|
+
|
|
17
|
+
/** Adapt a Web-standard app to a Vercel Edge Function default export. */
|
|
18
|
+
export function toVercelHandler<Env = unknown>(app: FetchApp<Env>): VercelHandler {
|
|
19
|
+
return (request) => app.fetch(request)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type HeaderValue = string | readonly string[] | undefined
|
|
23
|
+
type HeaderRecord = Readonly<Record<string, HeaderValue>>
|
|
24
|
+
|
|
25
|
+
function decodeBase64(value: string): Uint8Array<ArrayBuffer> {
|
|
26
|
+
const binary = atob(value)
|
|
27
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length))
|
|
28
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index)
|
|
29
|
+
return bytes
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function encodeBase64(bytes: Uint8Array): string {
|
|
33
|
+
let binary = ""
|
|
34
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
35
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000))
|
|
36
|
+
}
|
|
37
|
+
return btoa(binary)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function headersFromRecords(
|
|
41
|
+
single: HeaderRecord | undefined,
|
|
42
|
+
multi: HeaderRecord | undefined,
|
|
43
|
+
): Headers {
|
|
44
|
+
const headers = new Headers()
|
|
45
|
+
for (const [name, value] of Object.entries(single ?? {})) {
|
|
46
|
+
if (value === undefined) continue
|
|
47
|
+
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item)
|
|
48
|
+
}
|
|
49
|
+
for (const [name, value] of Object.entries(multi ?? {})) {
|
|
50
|
+
headers.delete(name)
|
|
51
|
+
if (value === undefined) continue
|
|
52
|
+
for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item)
|
|
53
|
+
}
|
|
54
|
+
return headers
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function absoluteUrl(
|
|
58
|
+
raw: string | undefined,
|
|
59
|
+
path: string,
|
|
60
|
+
query: string | undefined,
|
|
61
|
+
headers: Headers,
|
|
62
|
+
): string {
|
|
63
|
+
if (raw !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
return new URL(raw).toString()
|
|
66
|
+
} catch {
|
|
67
|
+
// Fall through to the structured event fields.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const host = headers.get("host") ?? "nifra.invalid"
|
|
71
|
+
const url = new URL(path || "/", `https://${host}`)
|
|
72
|
+
if (query !== undefined && query !== "") url.search = query.startsWith("?") ? query : `?${query}`
|
|
73
|
+
return url.toString()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function requestBody(
|
|
77
|
+
body: string | null | undefined,
|
|
78
|
+
encoded: boolean | undefined,
|
|
79
|
+
): string | Uint8Array<ArrayBuffer> | undefined {
|
|
80
|
+
if (body === undefined || body === null) return undefined
|
|
81
|
+
return encoded === true ? decodeBase64(body) : body
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Every `set-cookie` on a response, as separate values.
|
|
86
|
+
*
|
|
87
|
+
* `Headers.forEach` and `Headers.get` collapse repeated `set-cookie` into one comma-joined string,
|
|
88
|
+
* and a cookie's `Expires` attribute contains a comma - so the joined form cannot be split back
|
|
89
|
+
* apart, and a plain record keyed by header name keeps only one cookie regardless. Both platforms
|
|
90
|
+
* have a dedicated channel for this; `getSetCookie` is how we fill it.
|
|
91
|
+
*/
|
|
92
|
+
function setCookiesOf(headers: Headers): readonly string[] {
|
|
93
|
+
const getSetCookie = (headers as { getSetCookie?: () => string[] }).getSetCookie
|
|
94
|
+
if (typeof getSetCookie === "function") return getSetCookie.call(headers)
|
|
95
|
+
const single = headers.get("set-cookie")
|
|
96
|
+
return single === null ? [] : [single]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function platformResponse(
|
|
100
|
+
response: Response,
|
|
101
|
+
/** `cookies` is API Gateway v2's dedicated array; v1 and Netlify carry them in multi-value headers. */
|
|
102
|
+
cookieChannel: "cookies" | "multiValueHeaders",
|
|
103
|
+
): Promise<PlatformResponse> {
|
|
104
|
+
const bytes = new Uint8Array(await response.arrayBuffer())
|
|
105
|
+
const headers: Record<string, string> = {}
|
|
106
|
+
response.headers.forEach((value, name) => {
|
|
107
|
+
if (name.toLowerCase() === "set-cookie") return
|
|
108
|
+
headers[name] = value
|
|
109
|
+
})
|
|
110
|
+
const cookies = setCookiesOf(response.headers)
|
|
111
|
+
return {
|
|
112
|
+
statusCode: response.status,
|
|
113
|
+
headers,
|
|
114
|
+
...(cookies.length === 0
|
|
115
|
+
? {}
|
|
116
|
+
: cookieChannel === "cookies"
|
|
117
|
+
? { cookies }
|
|
118
|
+
: { multiValueHeaders: { "set-cookie": cookies } }),
|
|
119
|
+
body: encodeBase64(bytes),
|
|
120
|
+
isBase64Encoded: true,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface NetlifyEvent {
|
|
125
|
+
readonly httpMethod: string
|
|
126
|
+
readonly path: string
|
|
127
|
+
readonly rawUrl?: string
|
|
128
|
+
readonly rawQuery?: string
|
|
129
|
+
readonly headers?: HeaderRecord
|
|
130
|
+
readonly multiValueHeaders?: HeaderRecord
|
|
131
|
+
readonly body?: string | null
|
|
132
|
+
readonly isBase64Encoded?: boolean
|
|
133
|
+
readonly cookies?: readonly string[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface PlatformResponse {
|
|
137
|
+
readonly statusCode: number
|
|
138
|
+
/** Single-valued headers. `set-cookie` never appears here - see the two fields below. */
|
|
139
|
+
readonly headers: Readonly<Record<string, string>>
|
|
140
|
+
/**
|
|
141
|
+
* Repeated headers, currently only `set-cookie`. Netlify Functions and API Gateway REST (v1) read
|
|
142
|
+
* this; omitted when the response sets no cookies.
|
|
143
|
+
*/
|
|
144
|
+
readonly multiValueHeaders?: Readonly<Record<string, readonly string[]>>
|
|
145
|
+
/** API Gateway HTTP API (v2) takes cookies here rather than in a header. Omitted when there are none. */
|
|
146
|
+
readonly cookies?: readonly string[]
|
|
147
|
+
readonly body: string
|
|
148
|
+
readonly isBase64Encoded: true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type NetlifyHandler = (event: NetlifyEvent) => Promise<PlatformResponse>
|
|
152
|
+
|
|
153
|
+
/** Adapt a Web-standard app to a Netlify Functions event handler. */
|
|
154
|
+
export function toNetlifyHandler<Env = unknown>(app: FetchApp<Env>): NetlifyHandler {
|
|
155
|
+
return async (event) => {
|
|
156
|
+
const headers = headersFromRecords(event.headers, event.multiValueHeaders)
|
|
157
|
+
if (event.cookies !== undefined && !headers.has("cookie")) {
|
|
158
|
+
headers.set("cookie", event.cookies.join("; "))
|
|
159
|
+
}
|
|
160
|
+
const init: RequestInit = {
|
|
161
|
+
method: event.httpMethod,
|
|
162
|
+
headers,
|
|
163
|
+
}
|
|
164
|
+
const body = requestBody(event.body, event.isBase64Encoded)
|
|
165
|
+
if (body !== undefined) init.body = body
|
|
166
|
+
const request = new Request(
|
|
167
|
+
absoluteUrl(event.rawUrl, event.path, event.rawQuery, headers),
|
|
168
|
+
init,
|
|
169
|
+
)
|
|
170
|
+
return platformResponse(await app.fetch(request), "multiValueHeaders")
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface LambdaV2Event {
|
|
175
|
+
readonly version?: "2.0"
|
|
176
|
+
readonly rawPath?: string
|
|
177
|
+
readonly rawQueryString?: string
|
|
178
|
+
readonly headers?: HeaderRecord
|
|
179
|
+
readonly cookies?: readonly string[]
|
|
180
|
+
readonly body?: string | null
|
|
181
|
+
readonly isBase64Encoded?: boolean
|
|
182
|
+
readonly requestContext?: {
|
|
183
|
+
readonly domainName?: string
|
|
184
|
+
readonly http?: { readonly method: string; readonly path?: string }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface LambdaV1Event {
|
|
189
|
+
readonly httpMethod: string
|
|
190
|
+
readonly path: string
|
|
191
|
+
readonly headers?: HeaderRecord
|
|
192
|
+
readonly multiValueHeaders?: HeaderRecord
|
|
193
|
+
readonly queryStringParameters?: Readonly<Record<string, string | undefined>>
|
|
194
|
+
readonly multiValueQueryStringParameters?: Readonly<Record<string, readonly string[] | undefined>>
|
|
195
|
+
readonly body?: string | null
|
|
196
|
+
readonly isBase64Encoded?: boolean
|
|
197
|
+
readonly requestContext?: { readonly domainName?: string }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type LambdaEvent = LambdaV2Event | LambdaV1Event
|
|
201
|
+
export type LambdaResponse = PlatformResponse
|
|
202
|
+
export type LambdaHandler = (event: LambdaEvent, context?: unknown) => Promise<LambdaResponse>
|
|
203
|
+
|
|
204
|
+
function isLambdaV2(event: LambdaEvent): event is LambdaV2Event {
|
|
205
|
+
return "version" in event || "rawPath" in event || "rawQueryString" in event
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function queryFromLambdaV1(event: LambdaV1Event): string {
|
|
209
|
+
const params = new URLSearchParams()
|
|
210
|
+
for (const [name, value] of Object.entries(event.multiValueQueryStringParameters ?? {})) {
|
|
211
|
+
for (const item of value ?? []) params.append(name, item)
|
|
212
|
+
}
|
|
213
|
+
for (const [name, value] of Object.entries(event.queryStringParameters ?? {})) {
|
|
214
|
+
if (value !== undefined && !params.has(name)) params.set(name, value)
|
|
215
|
+
}
|
|
216
|
+
return params.toString()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Adapt API Gateway HTTP API (v2) and REST API (v1) events to a Web-standard app. */
|
|
220
|
+
export function toLambdaHandler<Env = unknown>(app: FetchApp<Env>): LambdaHandler {
|
|
221
|
+
return async (event) => {
|
|
222
|
+
const v2 = isLambdaV2(event)
|
|
223
|
+
const headers = headersFromRecords(event.headers, v2 ? undefined : event.multiValueHeaders)
|
|
224
|
+
let path: string
|
|
225
|
+
let query: string
|
|
226
|
+
let method: string
|
|
227
|
+
let rawUrl: string | undefined
|
|
228
|
+
if (v2) {
|
|
229
|
+
method = event.requestContext?.http?.method ?? "GET"
|
|
230
|
+
path = event.rawPath ?? event.requestContext?.http?.path ?? "/"
|
|
231
|
+
query = event.rawQueryString ?? ""
|
|
232
|
+
const domain = event.requestContext?.domainName
|
|
233
|
+
rawUrl =
|
|
234
|
+
domain === undefined ? undefined : `https://${domain}${path}${query ? `?${query}` : ""}`
|
|
235
|
+
if (event.cookies !== undefined && !headers.has("cookie"))
|
|
236
|
+
headers.set("cookie", event.cookies.join("; "))
|
|
237
|
+
} else {
|
|
238
|
+
method = event.httpMethod
|
|
239
|
+
path = event.path
|
|
240
|
+
query = queryFromLambdaV1(event)
|
|
241
|
+
const domain = event.requestContext?.domainName
|
|
242
|
+
rawUrl =
|
|
243
|
+
domain === undefined ? undefined : `https://${domain}${path}${query ? `?${query}` : ""}`
|
|
244
|
+
}
|
|
245
|
+
const body = event.body
|
|
246
|
+
const init: RequestInit = {
|
|
247
|
+
method,
|
|
248
|
+
headers,
|
|
249
|
+
}
|
|
250
|
+
const requestBodyValue = requestBody(body, event.isBase64Encoded)
|
|
251
|
+
if (requestBodyValue !== undefined) init.body = requestBodyValue
|
|
252
|
+
const request = new Request(absoluteUrl(rawUrl, path, query, headers), init)
|
|
253
|
+
return platformResponse(await app.fetch(request), v2 ? "cookies" : "multiValueHeaders")
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -2501,7 +2501,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
2501
2501
|
): MaybePromise<T> {
|
|
2502
2502
|
const hooks = this.onRequestHooks
|
|
2503
2503
|
const originalRequest = requestOf(source)
|
|
2504
|
-
|
|
2504
|
+
// A Web Request is already the exact object visible to the hooks and the response walk. Only
|
|
2505
|
+
// adapter sources need the source→request mapping; avoiding the WeakMap write keeps the common
|
|
2506
|
+
// Bun/Deno/edge lifecycle allocation-light while preserving Node's lazy Request identity.
|
|
2507
|
+
if (source !== originalRequest) this.responseSources.set(source as object, originalRequest)
|
|
2505
2508
|
let current: RequestSource = source
|
|
2506
2509
|
for (let i = 0; i < hooks.length; i++) {
|
|
2507
2510
|
const outcome = (hooks[i] as RawOnRequest)(requestOf(current), platform)
|
|
@@ -4447,7 +4450,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext> {
|
|
|
4447
4450
|
)
|
|
4448
4451
|
count += 1
|
|
4449
4452
|
}
|
|
4450
|
-
|
|
4453
|
+
if (count === 0) return undefined
|
|
4454
|
+
// RFC 9110 §9.3.2: a GET route answers HEAD with identical status + headers (Bun strips the
|
|
4455
|
+
// body on the wire). Alias the compiled GET handler under HEAD so HEAD stays on the native
|
|
4456
|
+
// lane instead of falling through to the portable dispatcher; the router applies the same
|
|
4457
|
+
// fallback there, so both lanes agree. An explicit HEAD registration wins via the ??=.
|
|
4458
|
+
for (const path of Object.keys(routes)) {
|
|
4459
|
+
const methods = routes[path]!
|
|
4460
|
+
if (methods.GET !== undefined) methods.HEAD ??= methods.GET
|
|
4461
|
+
}
|
|
4462
|
+
return routes
|
|
4451
4463
|
}
|
|
4452
4464
|
|
|
4453
4465
|
/**
|
package/src/server.ts
CHANGED
|
@@ -53,6 +53,21 @@ export {
|
|
|
53
53
|
redactLogFields,
|
|
54
54
|
silentLogger,
|
|
55
55
|
} from "./server/logger.ts"
|
|
56
|
+
export {
|
|
57
|
+
type FetchApp,
|
|
58
|
+
type LambdaEvent,
|
|
59
|
+
type LambdaHandler,
|
|
60
|
+
type LambdaResponse,
|
|
61
|
+
type LambdaV1Event,
|
|
62
|
+
type LambdaV2Event,
|
|
63
|
+
type NetlifyEvent,
|
|
64
|
+
type NetlifyHandler,
|
|
65
|
+
type PlatformResponse,
|
|
66
|
+
toLambdaHandler,
|
|
67
|
+
toNetlifyHandler,
|
|
68
|
+
toVercelHandler,
|
|
69
|
+
type VercelHandler,
|
|
70
|
+
} from "./server/platform-adapters.ts"
|
|
56
71
|
export type { Registry, RouteInfo } from "./server/registry.ts"
|
|
57
72
|
export {
|
|
58
73
|
type AdmissionController,
|