spooder 3.2.8 → 4.0.1

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 CHANGED
@@ -1,24 +1,16 @@
1
1
  <p align="center"><img src="docs/project-logo.png"/></p>
2
2
 
3
- # Spooder &middot; ![typescript](https://img.shields.io/badge/language-typescript-blue) [![license badge](https://img.shields.io/github/license/Kruithne/spooder?color=yellow)](LICENSE) ![npm version](https://img.shields.io/npm/v/spooder?color=c53635) ![bun](https://img.shields.io/badge/runtime-bun-f9f1e1)
3
+ # spooder &middot; ![typescript](https://img.shields.io/badge/language-typescript-blue) [![license badge](https://img.shields.io/github/license/Kruithne/spooder?color=yellow)](LICENSE) ![npm version](https://img.shields.io/npm/v/spooder?color=c53635) ![bun](https://img.shields.io/badge/runtime-bun-f9f1e1)
4
4
 
5
- `spooder` is a purpose-built server solution written using the [Bun](https://bun.sh/) runtime.
5
+ `spooder` is a purpose-built server solution that shifts away from the dependency hell of the Node.js ecosystem, with a focus on stability and performance, which is why:
6
+ - It is built using the [Bun](https://bun.sh/) runtime and not designed to be compatible with Node.js or other runtimes.
7
+ - It uses zero dependencies and only relies on code written explicitly for `spooder` or APIs provided by the Bun runtime, often implemented in native code.
8
+ - It provides streamlined APIs for common server tasks in a minimalistic way, without the overhead of a full-featured web framework.
9
+ - It is opinionated in its design to reduce complexity and overhead.
6
10
 
7
- ### What does it do?
8
-
9
- `spooder` consists of a command-line tool which provides automatic updating/restarting and canary functionality, and a building-block API for creating servers.
10
-
11
- ### Should I use it?
12
-
13
- Probably not. You are free to use `spooder` if you fully understand the risks and limitations of doing so, however here is a list of things you should consider before using it:
14
-
15
- ⚠️ This is not a Node.js package. It is built using the [Bun](https://bun.sh/) runtime, which is still experimental as of writing.
16
-
17
- ⚠️ It is designed to be highly opinionated and is not intended to be a general-purpose server, so configuration is limited.
18
-
19
- ⚠️ It is not a full-featured web server and only provides the functionality as required for the projects it has been built for.
20
-
21
- ⚠️ It has not been battle-tested and may contain bugs or security issues. The authors of this project are not responsible for any problems caused by using this software.
11
+ It consists of two components, the `CLI` and the `API`.
12
+ - The `CLI` is responsible for keeping the server process running, applying updates in response to source control changes, and automatically raising issues on GitHub via the canary feature.
13
+ - The `API` provides a minimal building-block style API for developing servers, with a focus on simplicity and performance.
22
14
 
23
15
  # Installation
24
16
 
@@ -32,42 +24,58 @@ bun add spooder
32
24
 
33
25
  # Configuration
34
26
 
35
- Both the runner and the API are configured in the same way by providing a `spooder` object in your `package.json` file.
27
+ Both the `CLI` and the API are configured in the same way by providing a `spooder` object in your `package.json` file.
36
28
 
37
29
  ```json
38
30
  {
39
31
  "spooder": {
40
32
  "auto_restart": 5000,
41
- "run": "bun run index.ts",
42
33
  "update": [
43
34
  "git pull",
44
35
  "bun install"
45
- ]
36
+ ],
37
+ "canary": {
38
+ "account": "",
39
+ "repository": "",
40
+ "labels": [],
41
+ "crash_console_history": 64,
42
+ "throttle": 86400,
43
+ "sanitize": true
44
+ }
46
45
  }
47
46
  }
48
47
  ```
49
48
 
50
49
  If there are any issues with the provided configuration, a warning will be printed to the console but will not halt execution. `spooder` will always fall back to default values where invalid configuration is provided.
51
50
 
52
- Configuration warnings **do not** raise `caution` events with the `spooder` canary functionality.
51
+ > [!NOTE]
52
+ > Configuration warnings **do not** raise `caution` events with the `spooder` canary functionality.
53
53
 
54
- # Runner
54
+ # CLI
55
55
 
56
- `spooder` includes a global command-line tool for running servers. It is recommended that you run this in a `screen` session.
56
+ The `CLI` component of `spooder` is a global command-line tool for running server processes.
57
+
58
+ - [CLI > Usage](#cli-usage)
59
+ - [CLI > Auto Restart](#cli-auto-restart)
60
+ - [CLI > Auto Update](#cli-auto-update)
61
+ - [CLI > Canary](#cli-canary)
62
+ - [CLI > Canary > Crash](#cli-canary-crash)
63
+ - [CLI > Canary > Sanitization](#cli-canary-sanitization)
64
+ - [CLI > Canary > System Information](#cli-canary-system-information)
57
65
 
58
- ```bash
59
- screen -S spooder # Create a new screen session
60
- cd /var/www/my_server/
61
- spooder
62
- ```
63
66
 
64
- While the intended use of this runner is for web servers, it can be used to run anything. It provides two primary features: automatic updating and automatic restarting.
67
+ <a id="cli-usage"></a>
68
+ ## CLI > Usage
65
69
 
66
- ## Entry Point
70
+ For convenience, it is recommended that you run this in a `screen` session.
67
71
 
68
- `spooder` will attempt to launch the server from the current working directory using the command `bun run index.ts` as a default.
72
+ ```bash
73
+ screen -S my-website-about-fish.net
74
+ cd /var/www/my-website-about-fish.net/
75
+ spooder
76
+ ```
69
77
 
70
- To customize this, provide an alternative command via the `run` configuration.
78
+ `spooder` will launch your server either by executing the `run` command provided in the configuration, or by executing `bun run index.ts` by default.
71
79
 
72
80
  ```json
73
81
  {
@@ -77,11 +85,22 @@ To customize this, provide an alternative command via the `run` configuration.
77
85
  }
78
86
  ```
79
87
 
80
- While `spooder` uses a `bun run` command by default, it is possible to use any command string.
88
+ While `spooder` uses a `bun run` command by default, it is possible to use any command string. For example if you wanted to launch a server using `node` instead of `bun`, you could do the following.
81
89
 
82
- ## Auto Restart
90
+ ```json
91
+ {
92
+ "spooder": {
93
+ "run": "node my_server.js"
94
+ }
95
+ }
96
+ ```
97
+ <a id="cli-auto-restart"></a>
98
+ ## CLI > Auto Restart
83
99
 
84
- In the event that the server exits (regardless of exit code), `spooder` can automatically restart it after a short delay. To enable this feature specify the restart delay in milliseconds as `auto_restart` in the configuration.
100
+ > [!NOTE]
101
+ > This feature is not enabled by default.
102
+
103
+ In the event that the server process exits, regardless of exit code, `spooder` can automatically restart it after a short delay. To enable this feature specify the restart delay in milliseconds as `auto_restart` in the configuration.
85
104
 
86
105
  ```json
87
106
  {
@@ -93,9 +112,13 @@ In the event that the server exits (regardless of exit code), `spooder` can auto
93
112
 
94
113
  If set to `0`, the server will be restarted immediately without delay. If set to `-1`, the server will not be restarted at all.
95
114
 
96
- ## Auto Update
115
+ <a id="cli-auto-update"></a>
116
+ ## CLI > Auto Update
117
+
118
+ > [!NOTE]
119
+ > This feature is not enabled by default.
97
120
 
98
- When starting your server, `spooder` can automatically update the source code in the working directory. To enable this feature, the necessary update commands can be provided in the configuration as an array of strings.
121
+ When starting or restarting a server process, `spooder` can automatically update the source code in the working directory. To enable this feature, the necessary update commands can be provided in the configuration as an array of strings.
99
122
 
100
123
  ```json
101
124
  {
@@ -108,28 +131,31 @@ When starting your server, `spooder` can automatically update the source code in
108
131
  }
109
132
  ```
110
133
 
111
- Commands will be executed in sequence, and the server will not be started until after the commands have resolved.
134
+ Each command should be a separate entry in the array and will be executed in sequence. The server process will be started once all commands have resolved.
112
135
 
113
- Each command should be a separate item in the array. Chaining commands in a single string using the `&&` or `||` operators will not work.
136
+ > [!IMPORTANT]
137
+ > Chainging commands using `&&` or `||` operators does not work.
114
138
 
115
139
  If a command in the sequence fails, the remaining commands will not be executed, however the server will still be started. This is preferred over entering a restart loop or failing to start the server at all.
116
140
 
117
- As well as being executed when the server is first started, the `update` commands are also run when `spooder` automatically restarts the server after it exits.
118
-
119
- You can utilize this to automatically update your server in response to a webhook or other event by simply exiting the process.
141
+ You can utilize this to automatically update your server in response to a webhook by exiting the process.
120
142
 
121
143
  ```ts
122
- events.on('receive-webhook', () => {
123
- // <- Gracefully finish processing here.
124
- process.exit(0);
144
+ server.webhook(process.env.WEBHOOK_SECRET, '/webhook', payload => {
145
+ setImmediate(() => server.stop(false));
146
+ return 200;
125
147
  });
126
148
  ```
127
149
 
128
- ## Canary
150
+ <a id="cli-canary"></a>
151
+ ## CLI > Canary
152
+
153
+ > [!NOTE]
154
+ > This feature is not enabled by default.
129
155
 
130
156
  `canary` is a feature in `spooder` which allows server problems to be raised as issues in your repository on GitHub.
131
157
 
132
- To enable this feature, there are a couple of steps you need to take.
158
+ To enable this feature, you will need to configure a GitHub App and configure it:
133
159
 
134
160
  ### 1. Create a GitHub App
135
161
 
@@ -142,7 +168,8 @@ Once created, install the GitHub App to your account. The app will need to be gi
142
168
 
143
169
  In addition to the **App ID** that is assigned automatically, you will also need to generate a **Private Key** for the app. This can be done by clicking the **Generate a private key** button on the app page.
144
170
 
145
- > Note: The private keys provided by GitHub are in PKCS#1 format, but only PKCS#8 is supported. You can convert the key file with the following command.
171
+ > [!NOTE]
172
+ > The private keys provided by GitHub are in PKCS#1 format, but only PKCS#8 is supported. You can convert the key file with the following command.
146
173
 
147
174
  ```bash
148
175
  openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key
@@ -164,7 +191,7 @@ Each server that intends to use the canary feature will need to have the private
164
191
 
165
192
  Replace `<GITHUB_ACCOUNT_NAME>` with the account name you have installed the GitHub App to, and `<GITHUB_REPOSITORY>` with the repository name you want to use for issues.
166
193
 
167
- The repository name must in the format `owner/repo` (e.g. `facebook/react`).
194
+ The repository name must in the full-name format `owner/repo` (e.g. `facebook/react`).
168
195
 
169
196
  The `labels` property can be used to provide a list of labels to automatically add to the issue. This property is optional and can be omitted.
170
197
 
@@ -178,19 +205,29 @@ SPOODER_CANARY_KEY=/home/bond/.ssh/id_007_pcks8.key
178
205
  ```
179
206
 
180
207
  `SPOODER_CANARY_APP_ID` is the **App ID** as shown on the GitHub App page.
208
+
181
209
  `SPOODER_CANARY_KEY` is the path to the private key file in PKCS#8 format.
182
210
 
211
+ > [!NOTE]
212
+ > Since `spooder` uses the Bun runtime, you can use the `.env.local` file in the project root directory to set these environment variables per-project.
213
+
183
214
  ### 4. Use canary
184
215
 
185
216
  Once configured, `spooder` will automatically raise an issue when the server exits with a non-zero exit code.
186
217
 
187
218
  In addition, you can manually raise issues using the `spooder` API by calling `caution()` or `panic()`. More information about these functions can be found in the `API` section.
188
219
 
189
- ## Crash
220
+ If `canary` has not been configured correctly, `spooder` will only print warnings to the console when it attempts to raise an issue.
221
+
222
+ > [!WARNING]
223
+ > Consider testing the canary feature with the `caution()` function before relying on it for critical issues.
224
+
225
+ <a id="cli-canary-crash"></a>
226
+ ## CLI > Canary > Crash
190
227
 
191
228
  It is recommended that you harden your server code against unexpected exceptions and use `panic()` and `caution()` to raise issues with selected diagnostic information.
192
229
 
193
- In the event that the server does encounter an unexpected exception which causes it to exit with a non-zero exit code, `spooder` will automatically raise an issue on GitHub using the canary feature, if configured.
230
+ In the event that the server does encounter an unexpected exception which causes it to exit with a non-zero exit code, `spooder` will provide some diagnostic information in the canary report.
194
231
 
195
232
  Since this issue has been caught externally, `spooder` has no context of the exception which was raised. Instead, the canary report will contain the output from both `stdout` and `stderr`.
196
233
 
@@ -218,7 +255,7 @@ Since this issue has been caught externally, `spooder` has no context of the exc
218
255
 
219
256
  The `proc_exit_code` property contains the exit code that the server exited with.
220
257
 
221
- The `console_output` will contain the last `64` lines of output from `stdout` and `stderr` combined. This can be configured by setting the `spooder.canary.crash_console_history` property.
258
+ The `console_output` will contain the last `64` lines of output from `stdout` and `stderr` combined. This can be configured by setting the `spooder.canary.crash_console_history` property to a length of your choice.
222
259
 
223
260
  ```json
224
261
  {
@@ -230,13 +267,12 @@ The `console_output` will contain the last `64` lines of output from `stdout` an
230
267
  }
231
268
  ```
232
269
 
233
- This information is subject to sanitization, as described in the `Sanitization` section, however you should be aware that stack traces may contain sensitive information.
234
-
235
- Additionally, Bun includes a relevant code snippet from the source file where the exception was raised. This is intended to help you identify the source of the problem.
270
+ This information is subject to sanitization, as described in the `CLI > Canary > Sanitization` section, however you should be aware that stack traces may contain sensitive information.
236
271
 
237
272
  Setting `spooder.canary.crash_console_history` to `0` will omit the `console_output` property from the report entirely, which may make it harder to diagnose the problem but will ensure that no sensitive information is leaked.
238
273
 
239
- ## Sanitization
274
+ <a id="cli-canary-sanitization"></a>
275
+ ## CLI > Canary > Sanitization
240
276
 
241
277
  All reports sent via the canary feature are sanitized to prevent sensitive information from being leaked. This includes:
242
278
 
@@ -281,9 +317,11 @@ The sanitization behavior can be disabled by setting `spooder.canary.sanitize` t
281
317
  }
282
318
  ```
283
319
 
284
- While this sanitization adds a layer of protection against information leaking, it does not catch everything. You should pay special attention to messages and objects provided to the canary to not unintentionally leak sensitive information.
320
+ > [!WARNING]
321
+ > While this sanitization adds a layer of protection against information leaking, it does not catch everything. You should pay special attention to messages and objects provided to the canary to not unintentionally leak sensitive information.
285
322
 
286
- ## System Information
323
+ <a id="cli-canary-system-information"></a>
324
+ ## CLI > Canary > System Information
287
325
 
288
326
  In addition to the information provided by the developer, `spooder` also includes some system information in the canary reports.
289
327
 
@@ -322,28 +360,72 @@ In addition to the information provided by the developer, `spooder` also include
322
360
  },
323
361
  "bun": {
324
362
  "version": "0.6.4",
325
- "rev": "f02561530fda1ee9396f51c8bc99b38716e38296"
363
+ "rev": "f02561530fda1ee9396f51c8bc99b38716e38296",
364
+ "memory_usage": {
365
+ "rss": 99672064,
366
+ "heapTotal": 3039232,
367
+ "heapUsed": 2332783,
368
+ "external": 0,
369
+ "arrayBuffers": 0
370
+ },
371
+ "cpu_usage": {
372
+ "user": 50469,
373
+ "system": 0
374
+ }
326
375
  }
327
376
  }
328
377
  ```
329
378
 
330
379
  # API
331
380
 
332
- `spooder` exposes a building-block style API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
381
+ `spooder` exposes a simple yet powerful API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
382
+
383
+ - [API > Serving](#api-serving)
384
+ - [`serve(port: number): Server`](#api-serving-serve)
385
+ - [API > Routing](#api-routing)
386
+ - [`server.route(path: string, handler: RequestHandler)`](#api-routing-server-route)
387
+ - [Redirection Routes](#api-routing-redirection-routes)
388
+ - [API > Routing > RequestHandler](#api-routing-request-handler)
389
+ - [API > Routing > Fallback Handling](#api-routing-fallback-handlers)
390
+ - [`server.handle(status_code: number, handler: RequestHandler)`](#api-routing-server-handle)
391
+ - [`server.default(handler: DefaultHandler)`](#api-routing-server-default)
392
+ - [`server.error(handler: ErrorHandler)`](#api-routing-server-error)
393
+ - [API > Routing > Directory Serving](#api-routing-directory-serving)
394
+ - [`server.dir(path: string, dir: string, handler?: DirHandler)`](#api-routing-server-dir)
395
+ - [API > Routing > Server-Sent Events](#api-routing-server-sent-events)
396
+ - [`server.sse(path: string, handler: ServerSentEventHandler)`](#api-routing-server-sse)
397
+ - [API > Routing > Webhooks](#api-routing-webhooks)
398
+ - [`server.webhook(secret: string, path: string, handler: WebhookHandler)`](#api-routing-server-webhook)
399
+ - [API > Server Control](#api-server-control)
400
+ - [`server.stop(immediate: boolean)`](#api-server-control-server-stop)
401
+ - [API > Error Handling](#api-error-handling)
402
+ - [`ErrorWithMetadata(message: string, metadata: object)`](#api-error-handling-error-with-metadata)
403
+ - [`caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-caution)
404
+ - [`panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`](#api-error-handling-panic)
405
+ - [API > Content](#api-content)
406
+ - [`template_sub(template: string, replacements: Record<string, string>): string`](#api-content-template-sub)
407
+ - [`template_sub_file(template_file: string, replacements: Record<string, string>): Promise<string>`](#api-content-template-sub-file)
408
+ - [`generate_hash_subs(length: number, prefix: string): Promise<Record<string, string>>`](#api-content-generate-hash-subs)
409
+ - [`apply_range(file: BunFile, request: Request): HandlerReturnType`](#api-content-apply-range)
410
+ - [API > State Management](#api-state-management)
411
+ - [`set_cookie(res: Response, name: string, value: string, options?: CookieOptions)`](#api-state-management-set-cookie)
412
+ - [`get_cookies(source: Request | Response): Record<string, string>`](#api-state-management-get-cookies)
413
+
414
+ <a id="api-serving"></a>
415
+ ## API > Serving
416
+
417
+ <a id="api-serving-serve"></a>
418
+ ### `serve(port: number): Server`
419
+
420
+ Bootstrap a server on the specified port.
333
421
 
334
422
  ```ts
335
- import { ... } from 'spooder';
336
- ```
337
-
338
- #### `serve(port: number): Server`
423
+ import { serve } from 'spooder';
339
424
 
340
- The `serve` function simplifies the process of boostrapping a server. Setting up a functioning server is as simple as calling the function and passing a port number to listen on.
341
-
342
- ```ts
343
425
  const server = serve(8080);
344
426
  ```
345
427
 
346
- Without any additional configuration, this will create a server which listens on the specified port and responds to all requests with the following response.
428
+ By default, the server responds with:
347
429
 
348
430
  ```http
349
431
  HTTP/1.1 404 Not Found
@@ -353,11 +435,13 @@ Content-Type: text/plain;charset=utf-8
353
435
  Not Found
354
436
  ```
355
437
 
356
- To build functionality on top of this, there are a number of functions that can be called from the `Server` object.
438
+ <a id="api-routing"></a>
439
+ ## API > Routing
357
440
 
358
- #### `server.route(path: string, handler: RequestHandler)`
441
+ <a id="api-routing-server-route"></a>
442
+ ### 🔧 `server.route(path: string, handler: RequestHandler)`
359
443
 
360
- The `route` function allows you to register a handler for a specific path. The handler will be called for all requests that exactly match the given path.
444
+ Register a handler for a specific path.
361
445
 
362
446
  ```ts
363
447
  server.route('/test/route', (req, url) => {
@@ -365,372 +449,595 @@ server.route('/test/route', (req, url) => {
365
449
  });
366
450
  ```
367
451
 
368
- Additionally, routes also support named parameters. These are defined by prefixing a path segment with a colon. These are added directly to the `searchParams` property of the `URL` object.
452
+ <a id="api-routing-redirection-routes"></a>
453
+ ### Redirection Routes
454
+
455
+ `spooder` does not provide a built-in redirection handler since it's trivial to implement one using [`Response.redirect`](https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static), part of the standard Web API.
369
456
 
370
457
  ```ts
371
- server.route('/test/:param', (req, url) => {
372
- return new Response(url.searchParams.get('param'), { status: 200 });
373
- });
458
+ server.route('/redirect', () => Response.redirect('/redirected', 301));
374
459
  ```
375
- > Note: Named parameters will overwrite existing search parameters with the same name.
376
460
 
377
- By default routes are matched exactly, but you can also use a wildcard to match any path that starts with a given path.
461
+ <a id="api-routing-request-handler"></a>
462
+ ## API > Routing > RequestHandler
463
+
464
+ `RequestHandler` is a function that accepts a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object and a [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object and returns a `HandlerReturnType`.
465
+
466
+ `HandlerReturnType` must be one of the following.
467
+
468
+ | Type | Description |
469
+ | --- | --- |
470
+ | `Response` | https://developer.mozilla.org/en-US/docs/Web/API/Response |
471
+ | `Blob` | https://developer.mozilla.org/en-US/docs/Web/API/Blob |
472
+ | `BunFile` | https://bun.sh/docs/api/file-io |
473
+ | `object` | Will be serialized to JSON. |
474
+ | `string` | Will be sent as `text/html``. |
475
+ | `number` | Sets status code and sends status message as plain text. |
476
+
477
+ > [!NOTE]
478
+ > For custom JSON serialization on an object/class, implement the [`toJSON()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) method.
479
+
480
+ `HandleReturnType` can also be a promise resolving to any of the above types, which will be awaited before sending the response.
481
+
482
+ > [!NOTE]
483
+ > Returning `Bun.file()` directly is the most efficient way to serve static files as it uses system calls to stream the file directly to the client without loading into user-space.
484
+
485
+ <a id="api-routing-query-parameters"></a>
486
+ ## API > Routing > Query Parameters
487
+
488
+ Query parameters can be accessed from the `searchParams` property on the `URL` object.
378
489
 
379
490
  ```ts
380
- server.route('/test/*', (req, url) => {
381
- return new Response('Hello, world!', { status: 200 });
491
+ server.route('/test', (req, url) => {
492
+ return new Response(url.searchParams.get('foo'), { status: 200 });
382
493
  });
383
494
  ```
384
495
 
385
- The above will match any path the starts with `/test`, such as:
386
- - `/test`
387
- - `/test/`
388
- - `/test/route`
389
- - `/test/route/foo.txt`
496
+ ```http
497
+ GET /test?foo=bar HTTP/1.1
390
498
 
391
- If you intend to use this for directory serving, you may be better suited looking at the `server.dir()` function.
499
+ HTTP/1.1 200 OK
500
+ Content-Length: 3
501
+
502
+ bar
503
+ ```
504
+
505
+ Named parameters can be used in paths by prefixing a path segment with a colon.
392
506
 
393
- Wildcards can also be placed anywhere in the path, allowing anything to be placed in a given single segment - it does not span multiple segments.
507
+ > [!NOTE]
508
+ > Named parameters will overwrite existing query parameters with the same name.
394
509
 
395
510
  ```ts
396
- server.route('/test/*/route', (req, url) => {
397
- return new Response('Hello, world!', { status: 200 });
511
+ server.route('/test/:param', (req, url) => {
512
+ return new Response(url.searchParams.get('param'), { status: 200 });
398
513
  });
399
514
  ```
400
515
 
401
- The above would allow anything to be placed in the middle segment. This behavior is documented for clarity as it is a byproduct of wildcard implementation for directories, but it is recommended you use the named parameters feature instead.
516
+ <a id="api-routing-wildcards"></a>
517
+ ## API > Routing > Wildcards
402
518
 
403
- Using the standard Web API, the route handler above receives a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object and returns a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object, which is then sent to the client.
519
+ Wildcards can be used to match any path that starts with a given path.
404
520
 
405
- All handle registration functions in `spooder` support registering async functions which will be awaited before sending the response.
521
+ > [!NOTE]
522
+ > If you intend to use this for directory serving, you may be better suited looking at the `server.dir()` function.
406
523
 
407
524
  ```ts
408
- server.route('/test/route', async (req, url) => {
409
- await new Promise((resolve) => setTimeout(resolve, 1000));
525
+ server.route('/test/*', (req, url) => {
410
526
  return new Response('Hello, world!', { status: 200 });
411
527
  });
412
528
  ```
413
529
 
414
- To streamline this process, `spooder` allows a number of other return types to be used as shortcuts.
530
+ > [!IMPORTANT]
531
+ > Routes are [FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)) and wildcards are greedy. Wildcards should be registered last to ensure they do not consume more specific routes.
532
+
533
+ ```ts
534
+ server.route('/*', () => 301);
535
+ server.route('/test', () => 200);
415
536
 
416
- Returning a `number` type treats the number as a status code and sends a relevant response.
537
+ // Accessing /test returns 301 here, because /* matches /test first.
538
+ ```
417
539
 
418
- By default, this will be a plain text response with the applicable status message as the body. This can be overridden with `server.handle()` or `server.default()`, which will be covered later.
540
+ <a id="api-routing-fallback-handlers"></a>
541
+ ## API > Routing > Fallback Handlers
419
542
 
543
+ <a id="api-routing-server-handle"></a>
544
+ ### 🔧 `server.handle(status_code: number, handler: RequestHandler)`
545
+ Register a custom handler for a specific status code.
420
546
  ```ts
421
- server.route('/test/route', (req) => {
422
- return 500;
547
+ server.handle(500, (req) => {
548
+ return new Response('Custom Internal Server Error Message', { status: 500 });
423
549
  });
424
550
  ```
425
- ```http
426
- HTTP/1.1 500 Internal Server Error
427
- Content-Length: 21
428
- Content-Type: text/plain;charset=utf-8
429
551
 
430
- Internal Server Error
552
+ <a id="api-routing-server-default"></a>
553
+ ### 🔧 `server.default(handler: DefaultHandler)`
554
+ Register a handler for all unhandled response codes.
555
+ > [!NOTE]
556
+ > If you return a `Response` object from here, you must explicitly set the status code.
557
+ ```ts
558
+ server.default((req, status_code) => {
559
+ return new Response(`Custom handler for: ${status_code}`, { status: status_code });
560
+ });
431
561
  ```
432
562
 
433
- Returning a `Blob` type, such as the `FileBlob` returned from the `Bun.file()` API, will send the blob as the response body with the appropriate content type and length headers.
563
+ <a id="api-routing-server-error"></a>
564
+ ### 🔧 `server.error(handler: ErrorHandler)`
565
+ Register a handler for uncaught errors.
434
566
 
567
+ > [!NOTE]
568
+ > This handler does not accept asynchronous functions and must return a `Response` object.
435
569
  ```ts
436
- server.route('test/route', (req) => {
437
- // Note that calling Bun.file() does not immediately read
438
- // the file from disk, it will be streamed with the response.
439
- return Bun.file('test.png');
570
+ server.error((err, req, url) => {
571
+ return new Response('Custom Internal Server Error Message', { status: 500 });
440
572
  });
441
573
  ```
442
- ```http
443
- HTTP/1.1 200 OK
444
- Content-Length: 12345
445
- Content-Type: image/png
446
574
 
447
- <binary data>
575
+ > [!IMPORTANT]
576
+ > It is highly recommended to use `caution()` or some form of reporting to notify you when this handler is called, as it means an error went entirely uncaught.
577
+
578
+ ```ts
579
+ server.error((err, req, url) => {
580
+ // Notify yourself of the error.
581
+ caution({ err, url });
582
+
583
+ // Return a response to the client.
584
+ return new Response('Custom Internal Server Error Message', { status: 500 });
585
+ });
448
586
  ```
449
587
 
450
- Return an `object` type, such as an array or a plain object, will send the object as JSON with the appropriate content type and length headers.
588
+ <a id="api-routing-directory-serving"></a>
589
+ ## API > Routing > Directory Serving
451
590
 
591
+ <a id="api-routing-server-dir"></a>
592
+ ### 🔧 `server.dir(path: string, dir: string, handler?: DirHandler)`
593
+ Serve files from a directory.
452
594
  ```ts
453
- server.route('test/route', (req) => {
454
- return { message: 'Hello, world!' };
455
- });
595
+ server.dir('/content', './public/content');
456
596
  ```
457
- ```http
458
- HTTP/1.1 200 OK
459
- Content-Length: 25
460
- Content-Type: application/json;charset=utf-8
461
597
 
462
- {"message":"Hello, world!"}
598
+ > [!IMPORTANT]
599
+ > `server.dir` registers a wildcard route. Routes are [FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)) and wildcards are greedy. Directories should be registered last to ensure they do not consume more specific routes.
600
+
601
+ ```ts
602
+ server.dir('/', '/files');
603
+ server.route('/test', () => 200);
604
+
605
+ // Route / is equal to /* with server.dir()
606
+ // Accessing /test returns 404 here because /files/test does not exist.
463
607
  ```
464
608
 
465
- Since custom classes are also objects, you can also return a custom class instance and it will be serialized to JSON. To control the serialization process, you can implement the `toJSON()` method on your class.
609
+ By default, spooder will use the following default handler for serving directories.
466
610
 
467
611
  ```ts
468
- class User {
469
- constructor(public name: string, public age: number) {}
612
+ function default_directory_handler(file_path: string, file: BunFile, stat: DirStat, request: Request): HandlerReturnType {
613
+ // ignore hidden files by default, return 404 to prevent file sniffing
614
+ if (path.basename(file_path).startsWith('.'))
615
+ return 404; // Not Found
470
616
 
471
- toJSON() {
472
- return {
473
- name: this.name,
474
- age: this.age,
475
- };
476
- }
477
- }
617
+ if (stat.isDirectory())
618
+ return 401; // Unauthorized
478
619
 
479
- server.route('test/route', (req) => {
480
- return new User('Bob', 42);
481
- });
620
+ return apply_range(file, request);
621
+ }
482
622
  ```
483
- ```http
484
- HTTP/1.1 200 OK
485
- Content-Length: 25
486
- Content-Type: application/json;charset=utf-8
487
623
 
488
- {"name":"Bob","age":42}
489
- ```
624
+ > [!NOTE]
625
+ > Uncaught `ENOENT` errors throw from the directory handler will return a `404` response, other errors will return a `500` response.
626
+
627
+ > [!NOTE]
628
+ > The call to `apply_range` in the default directory handler will automatically slice the file based on the [`Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) header. This function is also exposed as part of the `spooder` API for use in your own handlers.
490
629
 
491
- Any other type that is returned from a route handler will be converted to a string and sent as the response body with the appropriate length header and the content type `text/plain`.
630
+ Provide your own directory handler for fine-grained control.
631
+
632
+ > [!IMPORTANT]
633
+ > Providing your own handler will override the default handler defined above. Be sure to implement the same logic if you want to retain the default behavior.
634
+
635
+ | Parameter | Type | Reference |
636
+ | --- | --- | --- |
637
+ | `file_path` | `string` | The path to the file on disk. |
638
+ | `file` | `BunFile` | https://bun.sh/docs/api/file-io |
639
+ | `stat` | `fs.Stats` | https://nodejs.org/api/fs.html#class-fsstats |
640
+ | `request` | `Request` | https://developer.mozilla.org/en-US/docs/Web/API/Request |
641
+ | `url` | `URL` | https://developer.mozilla.org/en-US/docs/Web/API/URL |
492
642
 
493
643
  ```ts
494
- server.route('test/route', (req) => {
495
- return Symbol('foo');
644
+ server.dir('/static', '/static', (file_path, file, stat, request, url) => {
645
+ // Implement custom logic.
646
+ return file; // HandlerReturnType
496
647
  });
497
648
  ```
498
- ```http
499
- HTTP/1.1 200 OK
500
- Content-Length: 7
501
- Content-Type: text/plain;charset=utf-8
502
649
 
503
- Symbol(foo)
504
- ```
650
+ > [!NOTE]
651
+ > The directory handler function is only called for files that exist on disk - including directories.
505
652
 
506
- #### `server.default(handler: DefaultHandler)`
653
+ <a id="api-routing-server-sse"></a>
654
+ ## API > Routing > Server-Sent Events
507
655
 
508
- The server uses a default handler which responds to requests for which there was no handler registered, or the registered handler returned a numeric status code.
656
+ <a id="api-routing-server-sse"></a>
657
+ ### 🔧 `server.sse(path: string, handler: ServerSentEventHandler)`
509
658
 
510
- This default handler sends a simple response to the client with the status code and a body containing the status message.
659
+ Setup a [server-sent event](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream.
511
660
 
512
- ```http
513
- HTTP/1.1 404 Not Found
514
- Content-Length: 9
515
- Content-Type: text/plain;charset=utf-8
661
+ ```ts
662
+ server.sse('/sse', (req, url, client) => {
663
+ client.message('Hello, client!'); // Unnamed event.
664
+ client.event('named_event', 'Hello, client!'); // Named event.
516
665
 
517
- Not Found
666
+ client.message(JSON.stringify({ foo: 'bar' })); // JSON message.
667
+ });
518
668
  ```
519
669
 
520
- To customize the behavior of this handler, you can register a custom default handler using the `default` function.
670
+ `client.closed` is a promise that resolves when the client closes the connection.
521
671
 
522
672
  ```ts
523
- server.default((req, status_code) => {
524
- return new Response(`Custom error: ${status_code}`, { status: status_code });
673
+ const clients = new Set();
674
+
675
+ server.sse('/sse', (req, url, client) => {
676
+ clients.add(client);
677
+ client.closed.then(() => clients.delete(client));
678
+ });
679
+ ```
680
+
681
+ Connections can be manually closed with `client.close()`. This will also trigger the `client.closed` promise to resolve.
682
+
683
+ ```ts
684
+ server.sse('/sse', (req, url, client) => {
685
+ client.message('Hello, client!');
686
+
687
+ setTimeout(() => {
688
+ client.message('Goodbye, client!');
689
+ client.close();
690
+ }, 5000);
525
691
  });
526
692
  ```
527
693
 
528
- Using your own default handler allows you to provide a custom response for unhandled requests based on the status code.
694
+ <a id="api-routing-webhooks"></a>
695
+ ## API > Routing > Webhooks
529
696
 
530
- The return type from this handler can be any of the expected return types from a normal route handler with the exception that returning a `number` type will not be treated as a status code and will instead be treated as a plain text response.
697
+ <a id="api-routing-server-webhook"></a>
698
+ ### 🔧 `server.webhook(secret: string, path: string, handler: WebhookHandler)`
531
699
 
532
- If is worth noting that if you return a `Response` object from this handler, you must implicitly set the status code. If you do not, the status code will be set to `200` by default.
700
+ Setup a webhook handler.
533
701
 
534
702
  ```ts
535
- server.default((req, status_code) => {
536
- return new Response(`Custom error: ${status_code}`);
703
+ server.webhook(process.env.WEBHOOK_SECRET, '/webhook', payload => {
704
+ // React to the webhook.
705
+ return 200;
537
706
  });
538
707
  ```
539
- ```http
540
- HTTP/1.1 200 OK
541
- Content-Length: 18
542
- Content-Type: text/plain;charset=utf-8
543
708
 
544
- Custom error: 404
545
- ```
709
+ A webhook callback will only be called if the following critera is met by a request:
710
+ - Request method is `POST` (returns `405` otherwise)
711
+ - Header `X-Hub-Signature-256` is present (returns `400` otherwise)
712
+ - Header `Content-Type` is `application/json` (returns `401` otherwise)
713
+ - Request body is a valid JSON object (returns `500` otherwise)
714
+ - HMAC signature of the request body matches the `X-Hub-Signature-256` header (returns `401` otherwise)
715
+
716
+ > [!NOTE]
717
+ > Constant-time comparison is used to prevent timing attacks when comparing the HMAC signature.
546
718
 
547
- Returning anything else, such as a `Blob`, `object` or `string`, the status code will automatically be set to `status_code`. To override this behavior you must provide a `Response` object.
719
+ <a id="api-server-control"></a>
720
+ ## API > Server Control
548
721
 
549
- #### `server.handle(status_code: number, handler: RequestHandler)`
722
+ <a id="api-server-control-stop"></a>
723
+ ### 🔧 `server.stop(immediate: boolean)`
550
724
 
551
- The `handle` function allows you to register a handler for a specific status code. This handler will take priority over the default handler.
725
+ Stop the server process immediately, terminating all in-flight requests.
552
726
 
553
727
  ```ts
554
- server.handle(500, (req) => {
555
- return new Response('Custom Internal Server Error Message', { status: 500 });
556
- });
728
+ server.stop(true);
557
729
  ```
558
- ```http
559
- HTTP/1.1 500 Internal Server Error
560
- Content-Length: 36
561
- Content-Type: text/plain;charset=utf-8
562
730
 
563
- Custom Internal Server Error Message
731
+ Stop the server process gracefully, waiting for all in-flight requests to complete.
732
+
733
+ ```ts
734
+ server.stop(false);
564
735
  ```
565
736
 
566
- The return type from this handler can be any of the expected return types from a normal route handler with the exception that returning a `number` type will not be treated as a status code and will instead be treated as a plain text response.
737
+ <a id="api-error-handling"></a>
738
+ ## API > Error Handling
739
+
740
+ <a id="api-error-handling-error-with-metadata"></a>
741
+ ### 🔧 `ErrorWithMetadata(message: string, metadata: object)`
567
742
 
568
- If is worth noting that if you return a `Response` object from this handler, you must implicitly set the status code. If you do not, the status code will be set to `200` by default.
743
+ The `ErrorWithMetadata` class allows you to attach metadata to errors, which can be used for debugging purposes when errors are dispatched to the canary.
569
744
 
570
745
  ```ts
571
- server.handle(500, (req) => {
572
- return new Response('Custom Internal Server Error Message');
573
- });
746
+ throw new ErrorWithMetadata('Something went wrong', { foo: 'bar' });
574
747
  ```
575
- ```http
576
- HTTP/1.1 200 OK
577
- Content-Length: 36
578
- Content-Type: text/plain;charset=utf-8
579
748
 
580
- Custom Internal Server Error Message
581
- ```
749
+ Functions and promises contained in the metadata will be resolved and the return value will be used instead.
582
750
 
583
- Returning anything else, such as a `Blob`, `object` or `string`, the status code will automatically be set. To override this behavior you must provide a `Response` object.
751
+ ```ts
752
+ throw new ErrorWithMetadata('Something went wrong', { foo: () => 'bar' });
753
+ ```
584
754
 
585
- #### `server.error(handler: ErrorHandler)`
755
+ <a id="api-error-handling-caution"></a>
756
+ ### 🔧 `caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
586
757
 
587
- The `error` function allows you to register a handler for any uncaught errors that occur during the request handling process.
758
+ Raise a warning issue on GitHub. This is useful for non-fatal issues which you want to be notified about.
588
759
 
589
- Unlike other handlers, it does not accept asynchronous functions and it must return a `Response` object.
760
+ > [!NOTE]
761
+ > This function is only available if the canary feature is enabled.
590
762
 
591
763
  ```ts
592
- server.error((req, err) => {
593
- return new Response('Custom Internal Server Error Message', { status: 500 });
594
- });
764
+ try {
765
+ // Perform a non-critical action, such as analytics.
766
+ // ...
767
+ } catch (e) {
768
+ // `caution` is async, you can use it without awaiting.
769
+ caution(e);
770
+ }
595
771
  ```
596
- ```http
597
- HTTP/1.1 500 Internal Server Error
598
- Content-Length: 36
599
- Content-Type: text/plain;charset=utf-8
600
772
 
601
- Custom Internal Server Error Message
773
+ Additional data can be provided as objects which will be serialized to JSON and included in the report.
774
+
775
+ ```ts
776
+ caution(e, { foo: 42 });
602
777
  ```
603
- This should be used as a last resort to catch unintended errors and should not be part of your normal request handling process. Generally speaking, this handler should only be called if you have a bug in your code.
604
778
 
605
- #### `server.dir(path: string, dir: string)`
779
+ A custom error message can be provided as the first parameter
606
780
 
607
- The `dir` function allows you to serve static files from a directory on your file system.
781
+ > [!NOTE]
782
+ > Avoid including dynamic information in the title that would prevent the issue from being unique.
608
783
 
609
784
  ```ts
610
- server.dir('/content', './public/content');
785
+ caution('Custom error', e, { foo: 42 });
611
786
  ```
612
787
 
613
- The above example will serve all files from `./public/content` to any requests made to `/content`. For example `/content/test.txt` will serve the file `./public/content/test.txt`.
788
+ Issues raised with `caution()` are rate-limited. By default, the rate limit is `86400` seconds (24 hours), however this can be configured in the `spooder.canary.throttle` property.
614
789
 
615
- - This function is recursive and will serve all files from the specified directory and any subdirectories.
616
- - Requesting a directory will return a 401 response (subject to your configured handlers).
617
- - Requesting a file that does not exist will return a 404 response (subject to your configured handlers).
618
- - Requesting a file that is not readable will return a 500 response (subject to your configured handlers).
790
+ ```json
791
+ {
792
+ "spooder": {
793
+ "canary": {
794
+ "throttle": 86400
795
+ }
796
+ }
797
+ }
798
+ ```
619
799
 
620
- By default, hidden files (files prefixed with `.`) will not be served. To serve hidden files, you must set `ignoreHidden` to `false` in the `options` parameter.
800
+ Issues are considered unique by the `err_message` parameter, so avoid using dynamic information that would prevent this from being unique.
801
+
802
+ If you need to provide unique information, you can use the `err` parameter to provide an object which will be serialized to JSON and included in the issue body.
621
803
 
622
804
  ```ts
623
- server.dir('/content', './public/content', { ignoreHidden: false });
805
+ const some_important_value = Math.random();
806
+
807
+ // Bad: Do not use dynamic information in err_message.
808
+ await caution('Error with number ' + some_important_value);
809
+
810
+ // Good: Use err parameter to provide dynamic information.
811
+ await caution('Error with number', { some_important_value });
624
812
  ```
625
813
 
626
- If `ignoreHidden` is set to `true` (default) then requesting a hidden file will return a 404 response (subject to your configured handlers).
814
+ <a id="api-error-handling-panic"></a>
815
+ ### 🔧 `panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
816
+
817
+ This behaves the same as `caution()` with the difference that once `panic()` has raised the issue, it will exit the process with a non-zero exit code.
627
818
 
628
- Additionally, the `index` property can be set to a filename such as `index.html` to serve a default file when a directory is requested.
819
+ > [!NOTE]
820
+ > This function is only available if the canary feature is enabled.
821
+
822
+ This should only be used as an absolute last resort when the server cannot continue to run and will be unable to respond to requests.
629
823
 
630
824
  ```ts
631
- server.dir('/content', './public/content', { index: 'index.html' });
825
+ try {
826
+ // Perform a critical action.
827
+ // ...
828
+ } catch (e) {
829
+ // You should await `panic` since the process will exit.
830
+ await panic(e);
831
+ }
632
832
  ```
633
833
 
634
- The above will serve `./public/content/index.html` when `/content` is requested.
834
+ <a id="api-content"></a>
835
+ ## API > Content
635
836
 
636
- #### `server.stop(method: ServerStop)`
837
+ <a id="api-content-template-sub"></a>
838
+ ### 🔧 `template_sub(template: string, replacements: Record<string, string>): string`
637
839
 
638
- The `stop` function allows you to stop the server. `method` is one of `ServerStop.IMMEDIATE` or `ServerStop.GRACEFUL`.
840
+ Replace placeholders in a template string with values from a replacement object.
639
841
 
640
- `ServerStop.GRACEFUL` will stop accepting new requests and wait for all in-flight requests to complete before stopping the server. This is the default behavior.
842
+ > [!NOTE]
843
+ > Placeholders that do not appear in the replacement object will be left as-is. See `ignored` in below example.
641
844
 
642
- `ServerStop.IMMEDIATE` will immediately stop the server, terminating all in-flight requests.
845
+ ```ts
846
+ const template = `
847
+ <html>
848
+ <head>
849
+ <title>{title}</title>
850
+ </head>
851
+ <body>
852
+ <h1>{title}</h1>
853
+ <p>{content}</p>
854
+ <p>{ignored}</p>
855
+ </body>
856
+ </html>
857
+ `;
858
+
859
+ const replacements = {
860
+ title: 'Hello, world!',
861
+ content: 'This is a test.'
862
+ };
863
+
864
+ const html = template_sub(template, replacements);
865
+ ```
643
866
 
644
- ---
867
+ ```html
868
+ <html>
869
+ <head>
870
+ <title>Hello, world!</title>
871
+ </head>
872
+ <body>
873
+ <h1>Hello, world!</h1>
874
+ <p>This is a test.</p>
875
+ <p>{ignored}</p>
876
+ </body>
877
+ </html>
878
+ ```
645
879
 
646
- #### `ErrorWithMetadata(message: string, metadata: object)`
880
+ <a id="api-content-template-sub-file"></a>
881
+ ### 🔧 `template_sub_file(template_file: string, replacements: Record<string, string>): Promise<string>`
647
882
 
648
- The `ErrorWithMetadata` class is a thin wrapper around the built-in `Error` class that allows you to attach metadata to the error.
883
+ Replace placeholders in a template file with values from a replacement object.
649
884
 
650
- Providing additional information to errors can be used for debugging purposes when errors are dispatched to the canary.
885
+ > [!NOTE]
886
+ > This function is a convenience wrapper around `template_sub` and `Bun.file().text()` to reduce boilerplate. See `template_sub` for more information.
651
887
 
652
888
  ```ts
653
- throw new ErrorWithMetadata('Something went wrong', { foo: 'bar' });
889
+ const html = await template_sub_file('./template.html', replacements);
890
+
891
+ // Is equivalent to:
892
+ const file = Bun.file('./template.html');
893
+ const file_contents = await file.text();
894
+ const html = await template_sub(file_contents, replacements);
654
895
  ```
655
896
 
656
- For convinience, if any of the values in the `metadata` are functions, they will be called and the return value will be used instead.
657
897
 
658
- Additionally, promises will be resolved and readable streams will be converted to strings.
898
+ <a id="api-content-generate-hash-subs"></a>
899
+ ### 🔧 `generate_hash_subs(prefix: string): Promise<Record<string, string>>`
900
+
901
+ Generate a replacement table for mapping file paths to hashes in templates. This is useful for cache-busting static assets.
659
902
 
660
- ---
903
+ > [!IMPORTANT]
904
+ > Internally `generate_hash_subs()` uses `git ls-tree -r HEAD`, so the working directory must be a git repository.
661
905
 
662
- #### `route_location(redirect_url: string)`
906
+ ```ts
907
+ let hash_sub_table = {};
908
+
909
+ generate_hash_subs().then(subs => hash_sub_table = subs).catch(caution);
910
+
911
+ server.route('/test', (req, url) => {
912
+ return template_sub('Hello world {hash=docs/project-logo.png}', hash_sub_table);
913
+ });
914
+ ```
915
+
916
+ ```html
917
+ Hello world 754d9ea
918
+ ```
919
+
920
+ > [!IMPORTANT]
921
+ > Specify paths as they appear in git, relative to the repository root and with forward slashes (no leading slash).
663
922
 
664
- The `route_location` is a built-in request handler that redirects the client to a specified URL with the status code `301 Moved Permanently`.
923
+ By default hashes are truncated to `7` characters (a short hash), a custom length can be provided instead.
665
924
 
666
925
  ```ts
667
- server.route('test/route', route_location('https://example.com');
926
+ generate_hash_subs(40).then(...);
927
+ // d65c52a41a75db43e184d2268c6ea9f9741de63e
668
928
  ```
669
929
 
670
- The above is a much shorter equivalent to the following:
930
+ > [!NOTE]
931
+ > SHA-1 hashes are `40` characters. Git is transitioning to SHA-256, which are `64` characters. Short hashes of `7` are generally sufficient for cache-busting.
932
+
933
+ Use a different prefix other than `hash=` by passing it as the first parameter.
671
934
 
672
935
  ```ts
673
- server.route('test/route', (req, url) => {
674
- return new Response(null, {
675
- status: 301,
676
- headers: {
677
- Location: 'https://example.com',
678
- },
679
- });
936
+ generate_hash_subs(7, '#').then(subs => hash_sub_table = subs).catch(caution);
937
+
938
+ server.route('/test', (req, url) => {
939
+ return template_sub('Hello world {#docs/project-logo.png}', hash_sub_table);
680
940
  });
681
941
  ```
682
- ---
683
942
 
684
- #### `caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
685
- Raise a warning issue on GitHub. This is useful for non-fatal errors which you want to be notified about.
943
+ <a id="api-apply-range"></a>
944
+ ### 🔧 `apply_range(file: BunFile, request: Request): HandlerReturnType`
945
+
946
+ `apply_range` parses the `Range` header for a request and slices the file accordingly. This is used internally by `server.dir()` and exposed for convenience.
686
947
 
687
948
  ```ts
688
- try {
689
- // connect to database
690
- } catch (e) {
691
- await caution('Failed to connect to database', e);
692
- }
949
+ server.route('/test', (req, url) => {
950
+ const file = Bun.file('./test.txt');
951
+ return apply_range(file, req);
952
+ });
953
+ ```
954
+
955
+ ```http
956
+ GET /test HTTP/1.1
957
+ Range: bytes=0-5
958
+
959
+ HTTP/1.1 206 Partial Content
960
+ Content-Length: 6
961
+ Content-Range: bytes 0-5/6
962
+ Content-Type: text/plain;charset=utf-8
963
+
964
+ Hello,
693
965
  ```
694
966
 
695
- Providing a custom error message is optional and can be omitted. Additionally you can also provide additional error objects which will be serialized to JSON and included in the report.
967
+ <a id="api-state-management"></a>
968
+ ## API > State Management
969
+
970
+ <a id="api-state-management-set-cookie"></a>
971
+ ### 🔧 `set_cookie(res: Response, name: string, value: string, options?: CookieOptions)`
972
+
973
+ Set a cookie onto a `Response` object.
696
974
 
697
975
  ```ts
698
- caution(e); // provide just the error
699
- caution(e, { foo: 42 }); // additional data
700
- caution('Custom error', e, { foo: 42 }); // all
976
+ const res = new Response('Cookies!', { status: 200 });
977
+ set_cookie(res, 'my_test_cookie', 'my_cookie_value');
701
978
  ```
702
979
 
703
- To prevent spam, issues raised with `caution()` are rate-limited based on a configurable threshold in seconds. By default, the threshold is set to 24 hours per unique issue.
980
+ ```http
981
+ HTTP/1.1 200 OK
982
+ Set-Cookie: my_test_cookie=my_cookie_value
983
+ Content-Length: 8
704
984
 
705
- ```json
706
- {
707
- "spooder": {
708
- "canary": {
709
- "throttle": 86400
710
- }
711
- }
712
- }
985
+ Cookies!
713
986
  ```
714
987
 
715
- Issues are considered unique by the `err_message` parameter, so it is recommended that you do not include any dynamic information in this parameter that would prevent the issue from being unique.
988
+ > [!IMPORTANT]
989
+ > Spooder does not URL encode cookies by default. This can result in invalid cookies if they contain special characters. See `encode` option on `CookieOptions` below.
716
990
 
717
- If you need to provide unique information, you can use the `err` parameter to provide an object which will be serialized to JSON and included in the issue body.
991
+ ```ts
992
+ type CookieOptions = {
993
+ same_site?: 'Strict' | 'Lax' | 'None',
994
+ secure?: boolean,
995
+ http_only?: boolean,
996
+ path?: string,
997
+ expires?: number,
998
+ encode?: boolean
999
+ };
1000
+ ```
1001
+
1002
+ Most of the options that can be provided as `CookieOptions` are part of the standard `Set-Cookie` header. See [HTTP Cookies - MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies).
1003
+
1004
+ Passing `encode` as `true` will URL encode the cookie value.
718
1005
 
719
1006
  ```ts
720
- const some_important_value = Math.random();
1007
+ set_cookie(res, 'my_test_cookie', 'my cookie value', { encode: true });
1008
+ ```
721
1009
 
722
- // Bad: Do not use dynamic information in err_message.
723
- await caution('Error with number ' + some_important_value);
1010
+ ```http
1011
+ Set-Cookie: my_test_cookie=my%20cookie%20value
1012
+ ```
724
1013
 
725
- // Good: Use err parameter to provide dynamic information.
726
- await caution('Error with number', { some_important_value });
1014
+ <a id="api-state-management-get-cookies"></a>
1015
+ ### 🔧 `get_cookies(source: Request | Response, decode: boolean = false): Record<string, string>`
1016
+
1017
+ Get cookies from a `Request` or `Response` object.
1018
+
1019
+ ```http
1020
+ GET /test HTTP/1.1
1021
+ Cookie: my_test_cookie=my_cookie_value
727
1022
  ```
728
- It is not required that you `await` the `caution()`, and in situations where parallel processing is required, it is recommended that you do not.
729
1023
 
730
- #### `panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
731
- This behaves the same as `caution()` with the difference that once `panic()` has raised the issue, it will exit the process with a non-zero exit code.
1024
+ ```ts
1025
+ const cookies = get_cookies(req);
1026
+ { my_test_cookie: 'my_cookie_value' }
1027
+ ```
1028
+
1029
+ Cookies are not URL decoded by default. This can be enabled by passing `true` as the second parameter.
1030
+
1031
+ ```http
1032
+ GET /test HTTP/1.1
1033
+ Cookie: my_test_cookie=my%20cookie%20value
1034
+ ```
1035
+ ```ts
1036
+ const cookies = get_cookies(req, true);
1037
+ { my_test_cookie: 'my cookie value' }
1038
+ ```
732
1039
 
733
- This should only be called in worst-case scenarios where the server cannot continue to run. Since the process will exit, it is recommended that you `await` the `panic()` call.
1040
+ ## Legal
1041
+ This software is provided as-is with no warranty or guarantee. The authors of this project are not responsible or liable for any problems caused by using this software or any part thereof. Use of this software does not entitle you to any support or assistance from the authors of this project.
734
1042
 
735
- ## License
736
1043
  The code in this repository is licensed under the ISC license. See the [LICENSE](LICENSE) file for more information.