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 +584 -277
- package/package.json +1 -4
- package/src/api.d.ts +46 -16
- package/src/api.ts +355 -114
- package/src/config.ts +10 -1
- package/src/dispatch.ts +32 -43
- package/src/github.d.ts +11 -0
- package/src/github.ts +121 -0
package/README.md
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
<p align="center"><img src="docs/project-logo.png"/></p>
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# spooder ·  [](LICENSE)  
|
|
4
4
|
|
|
5
|
-
`spooder` is a purpose-built server solution
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
`
|
|
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
|
|
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
|
-
|
|
51
|
+
> [!NOTE]
|
|
52
|
+
> Configuration warnings **do not** raise `caution` events with the `spooder` canary functionality.
|
|
53
53
|
|
|
54
|
-
#
|
|
54
|
+
# CLI
|
|
55
55
|
|
|
56
|
-
`spooder`
|
|
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
|
-
|
|
67
|
+
<a id="cli-usage"></a>
|
|
68
|
+
## CLI > Usage
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
For convenience, it is recommended that you run this in a `screen` session.
|
|
67
71
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
server.webhook(process.env.WEBHOOK_SECRET, '/webhook', payload => {
|
|
145
|
+
setImmediate(() => server.stop(false));
|
|
146
|
+
return 200;
|
|
125
147
|
});
|
|
126
148
|
```
|
|
127
149
|
|
|
128
|
-
|
|
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,
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
438
|
+
<a id="api-routing"></a>
|
|
439
|
+
## API > Routing
|
|
357
440
|
|
|
358
|
-
|
|
441
|
+
<a id="api-routing-server-route"></a>
|
|
442
|
+
### 🔧 `server.route(path: string, handler: RequestHandler)`
|
|
359
443
|
|
|
360
|
-
|
|
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
|
-
|
|
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('/
|
|
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
|
-
|
|
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
|
|
381
|
-
return new Response('
|
|
491
|
+
server.route('/test', (req, url) => {
|
|
492
|
+
return new Response(url.searchParams.get('foo'), { status: 200 });
|
|
382
493
|
});
|
|
383
494
|
```
|
|
384
495
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
- `/test/`
|
|
388
|
-
- `/test/route`
|
|
389
|
-
- `/test/route/foo.txt`
|
|
496
|
+
```http
|
|
497
|
+
GET /test?foo=bar HTTP/1.1
|
|
390
498
|
|
|
391
|
-
|
|
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
|
-
|
|
507
|
+
> [!NOTE]
|
|
508
|
+
> Named parameters will overwrite existing query parameters with the same name.
|
|
394
509
|
|
|
395
510
|
```ts
|
|
396
|
-
server.route('/test
|
|
397
|
-
return new Response('
|
|
511
|
+
server.route('/test/:param', (req, url) => {
|
|
512
|
+
return new Response(url.searchParams.get('param'), { status: 200 });
|
|
398
513
|
});
|
|
399
514
|
```
|
|
400
515
|
|
|
401
|
-
|
|
516
|
+
<a id="api-routing-wildcards"></a>
|
|
517
|
+
## API > Routing > Wildcards
|
|
402
518
|
|
|
403
|
-
|
|
519
|
+
Wildcards can be used to match any path that starts with a given path.
|
|
404
520
|
|
|
405
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
537
|
+
// Accessing /test returns 301 here, because /* matches /test first.
|
|
538
|
+
```
|
|
417
539
|
|
|
418
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
609
|
+
By default, spooder will use the following default handler for serving directories.
|
|
466
610
|
|
|
467
611
|
```ts
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
return
|
|
473
|
-
name: this.name,
|
|
474
|
-
age: this.age,
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
}
|
|
617
|
+
if (stat.isDirectory())
|
|
618
|
+
return 401; // Unauthorized
|
|
478
619
|
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
495
|
-
|
|
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
|
-
|
|
504
|
-
|
|
650
|
+
> [!NOTE]
|
|
651
|
+
> The directory handler function is only called for files that exist on disk - including directories.
|
|
505
652
|
|
|
506
|
-
|
|
653
|
+
<a id="api-routing-server-sse"></a>
|
|
654
|
+
## API > Routing > Server-Sent Events
|
|
507
655
|
|
|
508
|
-
|
|
656
|
+
<a id="api-routing-server-sse"></a>
|
|
657
|
+
### 🔧 `server.sse(path: string, handler: ServerSentEventHandler)`
|
|
509
658
|
|
|
510
|
-
|
|
659
|
+
Setup a [server-sent event](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream.
|
|
511
660
|
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
666
|
+
client.message(JSON.stringify({ foo: 'bar' })); // JSON message.
|
|
667
|
+
});
|
|
518
668
|
```
|
|
519
669
|
|
|
520
|
-
|
|
670
|
+
`client.closed` is a promise that resolves when the client closes the connection.
|
|
521
671
|
|
|
522
672
|
```ts
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
694
|
+
<a id="api-routing-webhooks"></a>
|
|
695
|
+
## API > Routing > Webhooks
|
|
529
696
|
|
|
530
|
-
|
|
697
|
+
<a id="api-routing-server-webhook"></a>
|
|
698
|
+
### 🔧 `server.webhook(secret: string, path: string, handler: WebhookHandler)`
|
|
531
699
|
|
|
532
|
-
|
|
700
|
+
Setup a webhook handler.
|
|
533
701
|
|
|
534
702
|
```ts
|
|
535
|
-
server.
|
|
536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
719
|
+
<a id="api-server-control"></a>
|
|
720
|
+
## API > Server Control
|
|
548
721
|
|
|
549
|
-
|
|
722
|
+
<a id="api-server-control-stop"></a>
|
|
723
|
+
### 🔧 `server.stop(immediate: boolean)`
|
|
550
724
|
|
|
551
|
-
|
|
725
|
+
Stop the server process immediately, terminating all in-flight requests.
|
|
552
726
|
|
|
553
727
|
```ts
|
|
554
|
-
server.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
```
|
|
749
|
+
Functions and promises contained in the metadata will be resolved and the return value will be used instead.
|
|
582
750
|
|
|
583
|
-
|
|
751
|
+
```ts
|
|
752
|
+
throw new ErrorWithMetadata('Something went wrong', { foo: () => 'bar' });
|
|
753
|
+
```
|
|
584
754
|
|
|
585
|
-
|
|
755
|
+
<a id="api-error-handling-caution"></a>
|
|
756
|
+
### 🔧 `caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
|
|
586
757
|
|
|
587
|
-
|
|
758
|
+
Raise a warning issue on GitHub. This is useful for non-fatal issues which you want to be notified about.
|
|
588
759
|
|
|
589
|
-
|
|
760
|
+
> [!NOTE]
|
|
761
|
+
> This function is only available if the canary feature is enabled.
|
|
590
762
|
|
|
591
763
|
```ts
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
779
|
+
A custom error message can be provided as the first parameter
|
|
606
780
|
|
|
607
|
-
|
|
781
|
+
> [!NOTE]
|
|
782
|
+
> Avoid including dynamic information in the title that would prevent the issue from being unique.
|
|
608
783
|
|
|
609
784
|
```ts
|
|
610
|
-
|
|
785
|
+
caution('Custom error', e, { foo: 42 });
|
|
611
786
|
```
|
|
612
787
|
|
|
613
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
790
|
+
```json
|
|
791
|
+
{
|
|
792
|
+
"spooder": {
|
|
793
|
+
"canary": {
|
|
794
|
+
"throttle": 86400
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
```
|
|
619
799
|
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
+
<a id="api-content"></a>
|
|
835
|
+
## API > Content
|
|
635
836
|
|
|
636
|
-
|
|
837
|
+
<a id="api-content-template-sub"></a>
|
|
838
|
+
### 🔧 `template_sub(template: string, replacements: Record<string, string>): string`
|
|
637
839
|
|
|
638
|
-
|
|
840
|
+
Replace placeholders in a template string with values from a replacement object.
|
|
639
841
|
|
|
640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
883
|
+
Replace placeholders in a template file with values from a replacement object.
|
|
649
884
|
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
926
|
+
generate_hash_subs(40).then(...);
|
|
927
|
+
// d65c52a41a75db43e184d2268c6ea9f9741de63e
|
|
668
928
|
```
|
|
669
929
|
|
|
670
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
685
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
699
|
-
|
|
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
|
-
|
|
980
|
+
```http
|
|
981
|
+
HTTP/1.1 200 OK
|
|
982
|
+
Set-Cookie: my_test_cookie=my_cookie_value
|
|
983
|
+
Content-Length: 8
|
|
704
984
|
|
|
705
|
-
|
|
706
|
-
{
|
|
707
|
-
"spooder": {
|
|
708
|
-
"canary": {
|
|
709
|
-
"throttle": 86400
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
985
|
+
Cookies!
|
|
713
986
|
```
|
|
714
987
|
|
|
715
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1007
|
+
set_cookie(res, 'my_test_cookie', 'my cookie value', { encode: true });
|
|
1008
|
+
```
|
|
721
1009
|
|
|
722
|
-
|
|
723
|
-
|
|
1010
|
+
```http
|
|
1011
|
+
Set-Cookie: my_test_cookie=my%20cookie%20value
|
|
1012
|
+
```
|
|
724
1013
|
|
|
725
|
-
|
|
726
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
-
|
|
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.
|