@whatwg-node/server 0.9.65 → 0.9.66-alpha-20250103124756-16736115b8febf6d963c207e6d75d57aa24645e6

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -258,9 +258,7 @@ as a first class citizen. So the configuration is really simple like any other J
258
258
  ```ts
259
259
  import myServerAdapter from './myServerAdapter'
260
260
 
261
- Bun.serve(myServerAdapter)
262
-
263
- const server = Bun.serve(yoga)
261
+ const server = Bun.serve(myServerAdapter)
264
262
 
265
263
  console.info(`Server is running on ${server.hostname}`)
266
264
  ```
@@ -299,3 +297,305 @@ We'd recommend to use `fets` to handle routing and middleware approach. It uses
299
297
  `@whatwg-node/server` under the hood.
300
298
 
301
299
  > Learn more about `fets` [here](https://github.com/ardatan/fets)
300
+
301
+ ## Plugin System
302
+
303
+ You can create your own plugins to extend the functionality of your server adapter.
304
+
305
+ ### `onRequest`
306
+
307
+ This hook is invoked for ANY incoming HTTP request. Here you can manipulate the request or create a
308
+ short circuit before the server adapter handles the request.
309
+
310
+ For example, you can shortcut the manually handle an HTTP request, short-circuiting the HTTP
311
+ handler:
312
+
313
+ ```ts
314
+ import { createServerAdapter, type ServerAdapterPlugin } from '@whatwg-node/server'
315
+
316
+ const myPlugin: ServerAdapterPlugin = {
317
+ onRequest({ request, endResponse, fetchAPI }) {
318
+ if (!request.headers.get('authorization')) {
319
+ endResponse(
320
+ new fetchAPI.Response(null, {
321
+ status: 401,
322
+ headers: {
323
+ 'Content-Type': 'application/json'
324
+ }
325
+ })
326
+ )
327
+ }
328
+ }
329
+ }
330
+
331
+ const myServerAdapter = createServerAdapter(
332
+ async request => {
333
+ return new Response(`Hello World!`, { status: 200 })
334
+ },
335
+ {
336
+ plugins: [myPlugin]
337
+ }
338
+ )
339
+ ```
340
+
341
+ Possible usage examples of this hook are:
342
+
343
+ - Manipulate the request
344
+ - Short circuit before the adapter handles the request
345
+
346
+ | Payload field | Description |
347
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
348
+ | `request` | The incoming HTTP request as WHATWG `Request` object. [Learn more about the request](https://developer.mozilla.org/en-US/docs/Web/API/Request). |
349
+ | `serverContext` | The early context object that is shared between all hooks and the entire execution. [Learn more about the context](/docs/features/context). |
350
+ | `fetchAPI` | WHATWG Fetch API implementation. [Learn more about the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). |
351
+ | `url` | WHATWG URL object of the incoming request. [Learn more about the URL object](https://developer.mozilla.org/en-US/docs/Web/API/URL). |
352
+ | `endResponse` | A function that allows you to end the request early and send a response to the client. |
353
+
354
+ ### `onResponse`
355
+
356
+ This hook is invoked after a HTTP request has been processed and after the response has been
357
+ forwarded to the client. Here you can perform any cleanup or logging operations, or you can
358
+ manipulate the outgoing response object.
359
+
360
+ ```ts
361
+ import { createServerAdapter, type ServerAdapterPlugin } from '@whatwg-node/server'
362
+
363
+ const requestTimeMap = new WeakMap<Request, number>()
364
+
365
+ const myPlugin: ServerAdapterPlugin = {
366
+ onRequest({ request }) {
367
+ requestTimeMap.set(request, Date.now())
368
+ },
369
+ onResponse({ request, serverContext, response }) {
370
+ console.log(`Request to ${request.url} has been processed with status ${response.status}`)
371
+ // Add some headers
372
+ response.headers.set('X-Server-Name', 'My Server')
373
+ console.log(`Request to ${request.url} took ${Date.now() - requestTimeMap.get(request)}ms`)
374
+ }
375
+ }
376
+ ```
377
+
378
+ **Example actions in this hook:**
379
+
380
+ - Specify custom response format
381
+ - Logging/Metrics
382
+
383
+ | Field Name | Description |
384
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
385
+ | `request` | The incoming HTTP request as WHATWG `Request` object. [Learn more about the request](https://developer.mozilla.org/en-US/docs/Web/API/Request). |
386
+ | `serverContext` | The final context object that is shared between all hooks and the execution. [Learn more about the context](/docs/features/context). |
387
+ | `response` | The outgoing HTTP response as WHATWG `Response` object. [Learn more about the response interface](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
388
+
389
+ ### `onDispose`
390
+
391
+ In order to clean up resources when the server is shut down, you can use `onDispose`,
392
+ `Symbol.asyncDispose` or `Symbol.syncDispose` to clean up resources.
393
+
394
+ ```ts
395
+ export const useMyPlugin = () => {
396
+ return {
397
+ async onDispose() {
398
+ // Clean up resources
399
+ await stopConnection()
400
+ }
401
+ }
402
+ }
403
+ ```
404
+
405
+ [You can learn more about Explicit Resource Management below](#explicit-resource-management)
406
+
407
+ ## `Request.signal` for awareness of client disconnection
408
+
409
+ In the real world, a lot of HTTP requests are dropped or canceled. This can happen due to a flakey
410
+ internet connection, navigation to a new view or page within a web or native app or the user simply
411
+ closing the app. In this case, the server can stop processing the request and save resources.
412
+
413
+ You can utilize `request.signal` to cancel pending asynchronous operations when the client
414
+ disconnects.
415
+
416
+ ```ts
417
+ import { createServerAdapter } from '@whatwg-node/server'
418
+
419
+ const myServerAdapter = createServerAdapter(async request => {
420
+ const upstreamRes = await fetch('https://api.example.com/data', {
421
+ // When the client disconnects, the fetch request will be canceled
422
+ signal: request.signal
423
+ })
424
+ return Response.json({
425
+ data: await upstreamRes.json()
426
+ })
427
+ })
428
+ ```
429
+
430
+ The execution cancelation API is built on top of the AbortController and AbortSignal APIs.
431
+
432
+ [Learn more about AbortController and AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
433
+
434
+ ## Explicit Resource Management
435
+
436
+ While implementing your server with `@whatwg-node/server`, you need to control over the lifecycle of
437
+ your resources. This is especially important when you are dealing with resources that need to be
438
+ cleaned up when they are no longer needed, or clean up the operations in a queue when the server is
439
+ shutting down.
440
+
441
+ ### Dispose the Server Adapter
442
+
443
+ The server adapter supports
444
+ [Explicit Resource Management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)
445
+ approach that allows you to dispose of resources when they are no longer needed. This can be done in
446
+ two ways shown below;
447
+
448
+ #### `await using` syntax
449
+
450
+ We use the `await using` syntax to create a new instance of `adapter` and dispose of it when the
451
+ block is exited. Notice that we are using a block to limit the scope of `adapter` within `{ }`. So
452
+ resources will be disposed of when the block is exited.
453
+
454
+ ```ts
455
+ console.log('Adapter is starting')
456
+ {
457
+ await using adapter = createServerAdapter(/* ... */)
458
+ }
459
+ console.log('Adapter is disposed')
460
+ ```
461
+
462
+ #### `dispose` method
463
+
464
+ We create a new instance of `adapter` and dispose of it using the `dispose` method.
465
+
466
+ ```ts
467
+ console.log('Adapter is starting')
468
+ const adapter = createServerAdapter(/* ... */)
469
+ await adapter.dispose()
470
+ console.log('Adapter is disposed')
471
+ ```
472
+
473
+ In the first example, we use the `await using` syntax to create a new instance of `adapter` and
474
+ dispose of it when the block is exited. In the second example,
475
+
476
+ #### Dispose on Node.js
477
+
478
+ When running your adapter on Node.js, you can use process event listeners or server's `close` event
479
+ to trigger the adapter's disposal. Or you can configure the adapter to handle this automatically by
480
+ listening `process` exit signals.
481
+
482
+ ##### Explicit disposal
483
+
484
+ We can dispose of the adapter instance when the server is closed like below.
485
+
486
+ ```ts
487
+ import { createServer } from 'http'
488
+ import { createServerAdapter } from '@whatwg-node/server'
489
+
490
+ const adapter = createServerAdapter(/* ... */)
491
+
492
+ const server = createServer(adapter)
493
+ server.listen(4000, () => {
494
+ console.info('Server is running on http://localhost:4000')
495
+ })
496
+ server.once('close', async () => {
497
+ await adapter.dispose()
498
+ console.info('Server is disposed so is adapter')
499
+ })
500
+ ```
501
+
502
+ ##### Automatic disposal
503
+
504
+ `disposeOnProcessTerminate` option will register an event listener for `process` termination in
505
+ Node.js
506
+
507
+ ```ts
508
+ import { createServer } from 'http'
509
+ import { createServerAdapter } from '@whatwg-node/server'
510
+
511
+ createServer(
512
+ createServerAdapter(/* ... */, {
513
+ disposeOnProcessTerminate: true,
514
+ plugins: [/* ... */]
515
+ })
516
+ ).listen(4000, () => {
517
+ console.info('Server is running on http://localhost:4000')
518
+ })
519
+ ```
520
+
521
+ ### Plugin Disposal
522
+
523
+ If you have plugins that need some internal resources to be disposed of, you can use the `onDispose`
524
+ hook to dispose of them. This hook will be invoked when the adapter instance is disposed like above.
525
+
526
+ ```ts
527
+ let dbConnection: Connection
528
+ const plugin = {
529
+ onPluginInit: async () => {
530
+ dbConnection = await createConnection()
531
+ },
532
+ onDispose: async () => {
533
+ // Dispose of resources
534
+ await dbConnection.close()
535
+ }
536
+ }
537
+ ```
538
+
539
+ Or you can flush a queue of operations when the server is shutting down.
540
+
541
+ ```ts
542
+ const backgroundJobs: Promise<void>[] = []
543
+
544
+ const plugin = {
545
+ onRequest() {
546
+ backgroundJobs.push(
547
+ sendAnalytics({
548
+ /* ... */
549
+ })
550
+ )
551
+ },
552
+ onDispose: async () => {
553
+ // Flush the queue of background jobs
554
+ await Promise.all(backgroundJobs)
555
+ }
556
+ }
557
+ ```
558
+
559
+ But for this kind of purposes, `waitUntil` can be a better choice.
560
+
561
+ ### Background jobs
562
+
563
+ If you have background jobs that need to be completed before the environment is shut down.
564
+ `waitUntil` is better choice than `onDispose`. In this case, those jobs will keep running in the
565
+ background but in case of disposal, they will be awaited. `waitUntil` works so similar to
566
+ [Cloudflare Workers' `waitUntil` function](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#parameters).
567
+
568
+ But the adapter handles `waitUntil` even if it is not provided by the environment.
569
+
570
+ ```ts
571
+ const adapter = createServerAdapter(async (request, context) => {
572
+ const args = await request.json()
573
+ if (!args.name) {
574
+ return Response.json({ error: 'Name is required' }, { status: 400 })
575
+ }
576
+ // This does not block the response
577
+ context.waitUntil(
578
+ fetch('http://my-analytics.com/analytics', {
579
+ method: 'POST',
580
+ body: JSON.stringify({
581
+ name: args.name,
582
+ userAgent: request.headers.get('User-Agent')
583
+ })
584
+ })
585
+ )
586
+ return Response.json({ greetings: `Hello, ${args.name}` })
587
+ })
588
+
589
+ const res = await adapter.fetch('http://localhost:4000', {
590
+ method: 'POST',
591
+ headers: {
592
+ 'Content-Type': 'application/json'
593
+ },
594
+ body: JSON.stringify({ name: 'John' })
595
+ })
596
+
597
+ console.log(await res.json()) // { greetings: "Hello, John" }
598
+
599
+ await adapter.dispose()
600
+ // The fetch request for `analytics` will be awaited here
601
+ ```
package/cjs/utils.js CHANGED
@@ -154,7 +154,7 @@ function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
154
154
  return new fetchAPI.Request(fullUrl, {
155
155
  method: nodeRequest.method,
156
156
  headers: normalizedHeaders,
157
- signal,
157
+ signal: signal || null,
158
158
  });
159
159
  }
160
160
  /**
@@ -167,16 +167,16 @@ function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
167
167
  if (maybeParsedBody != null && Object.keys(maybeParsedBody).length > 0) {
168
168
  if (isRequestBody(maybeParsedBody)) {
169
169
  return new fetchAPI.Request(fullUrl, {
170
- method: nodeRequest.method,
170
+ method: nodeRequest.method || 'GET',
171
171
  headers: normalizedHeaders,
172
172
  body: maybeParsedBody,
173
- signal,
173
+ signal: signal || null,
174
174
  });
175
175
  }
176
176
  const request = new fetchAPI.Request(fullUrl, {
177
- method: nodeRequest.method,
177
+ method: nodeRequest.method || 'GET',
178
178
  headers: normalizedHeaders,
179
- signal,
179
+ signal: signal || null,
180
180
  });
181
181
  if (!request.headers.get('content-type')?.includes('json')) {
182
182
  request.headers.set('content-type', 'application/json; charset=utf-8');
package/esm/utils.js CHANGED
@@ -131,7 +131,7 @@ export function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
131
131
  return new fetchAPI.Request(fullUrl, {
132
132
  method: nodeRequest.method,
133
133
  headers: normalizedHeaders,
134
- signal,
134
+ signal: signal || null,
135
135
  });
136
136
  }
137
137
  /**
@@ -144,16 +144,16 @@ export function normalizeNodeRequest(nodeRequest, fetchAPI, registerSignal) {
144
144
  if (maybeParsedBody != null && Object.keys(maybeParsedBody).length > 0) {
145
145
  if (isRequestBody(maybeParsedBody)) {
146
146
  return new fetchAPI.Request(fullUrl, {
147
- method: nodeRequest.method,
147
+ method: nodeRequest.method || 'GET',
148
148
  headers: normalizedHeaders,
149
149
  body: maybeParsedBody,
150
- signal,
150
+ signal: signal || null,
151
151
  });
152
152
  }
153
153
  const request = new fetchAPI.Request(fullUrl, {
154
- method: nodeRequest.method,
154
+ method: nodeRequest.method || 'GET',
155
155
  headers: normalizedHeaders,
156
- signal,
156
+ signal: signal || null,
157
157
  });
158
158
  if (!request.headers.get('content-type')?.includes('json')) {
159
159
  request.headers.set('content-type', 'application/json; charset=utf-8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whatwg-node/server",
3
- "version": "0.9.65",
3
+ "version": "0.9.66-alpha-20250103124756-16736115b8febf6d963c207e6d75d57aa24645e6",
4
4
  "description": "Fetch API compliant HTTP Server adapter",
5
5
  "sideEffects": false,
6
6
  "dependencies": {
@@ -5,19 +5,19 @@ import type { Readable } from 'stream';
5
5
  import type { FetchAPI, FetchEvent } from './types.cjs';
6
6
  export declare function isAsyncIterable(body: any): body is AsyncIterable<any>;
7
7
  export interface NodeRequest {
8
- protocol?: string;
9
- hostname?: string;
10
- body?: any;
11
- url?: string;
12
- originalUrl?: string;
13
- method?: string;
14
- headers?: any;
15
- req?: IncomingMessage | Http2ServerRequest;
16
- raw?: IncomingMessage | Http2ServerRequest;
17
- socket?: Socket;
18
- query?: any;
8
+ protocol?: string | undefined;
9
+ hostname?: string | undefined;
10
+ body?: any | undefined;
11
+ url?: string | undefined;
12
+ originalUrl?: string | undefined;
13
+ method?: string | undefined;
14
+ headers?: any | undefined;
15
+ req?: IncomingMessage | Http2ServerRequest | undefined;
16
+ raw?: IncomingMessage | Http2ServerRequest | undefined;
17
+ socket?: Socket | undefined;
18
+ query?: any | undefined;
19
19
  once?(event: string, listener: (...args: any[]) => void): void;
20
- aborted?: boolean;
20
+ aborted?: boolean | undefined;
21
21
  }
22
22
  export type NodeResponse = ServerResponse | Http2ServerResponse;
23
23
  export declare class ServerAdapterRequestAbortSignal extends EventTarget implements AbortSignal {
@@ -5,19 +5,19 @@ import type { Readable } from 'stream';
5
5
  import type { FetchAPI, FetchEvent } from './types.js';
6
6
  export declare function isAsyncIterable(body: any): body is AsyncIterable<any>;
7
7
  export interface NodeRequest {
8
- protocol?: string;
9
- hostname?: string;
10
- body?: any;
11
- url?: string;
12
- originalUrl?: string;
13
- method?: string;
14
- headers?: any;
15
- req?: IncomingMessage | Http2ServerRequest;
16
- raw?: IncomingMessage | Http2ServerRequest;
17
- socket?: Socket;
18
- query?: any;
8
+ protocol?: string | undefined;
9
+ hostname?: string | undefined;
10
+ body?: any | undefined;
11
+ url?: string | undefined;
12
+ originalUrl?: string | undefined;
13
+ method?: string | undefined;
14
+ headers?: any | undefined;
15
+ req?: IncomingMessage | Http2ServerRequest | undefined;
16
+ raw?: IncomingMessage | Http2ServerRequest | undefined;
17
+ socket?: Socket | undefined;
18
+ query?: any | undefined;
19
19
  once?(event: string, listener: (...args: any[]) => void): void;
20
- aborted?: boolean;
20
+ aborted?: boolean | undefined;
21
21
  }
22
22
  export type NodeResponse = ServerResponse | Http2ServerResponse;
23
23
  export declare class ServerAdapterRequestAbortSignal extends EventTarget implements AbortSignal {