@jsenv/server 17.6.9 → 17.7.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/dist/jsenv_server.js +39 -26
- package/docs/handling_requests.md +15 -10
- package/package.json +2 -2
- package/src/router/router.js +17 -6
- package/src/start_server.js +4 -1
package/dist/jsenv_server.js
CHANGED
|
@@ -5899,37 +5899,36 @@ const createResourcePattern = (pattern) => {
|
|
|
5899
5899
|
});
|
|
5900
5900
|
}
|
|
5901
5901
|
|
|
5902
|
-
|
|
5902
|
+
// "/docs/*" also answers the directory itself ("/docs/" and "/docs") with an
|
|
5903
|
+
// empty star, so one route serves a directory and its index. Only a star
|
|
5904
|
+
// widens a pattern: "/docs/" is that one path and "/" the home page.
|
|
5905
|
+
const directoryPattern = pathnamePatternString.endsWith("/*")
|
|
5906
|
+
? PATTERN.create(pathnamePatternString.slice(0, -1), {
|
|
5907
|
+
namedGroupDelimiter: "/",
|
|
5908
|
+
})
|
|
5909
|
+
: null;
|
|
5903
5910
|
// a pattern constraining nothing: every resource matches it, so whoever uses
|
|
5904
5911
|
// it cannot name the resource that matched
|
|
5905
5912
|
const matchesEveryResource =
|
|
5906
5913
|
!searchPattern &&
|
|
5907
5914
|
!hashPattern &&
|
|
5908
|
-
(pathnamePatternString === "*" ||
|
|
5909
|
-
pathnamePatternString === "/" ||
|
|
5910
|
-
pathnamePatternString === "/*");
|
|
5915
|
+
(pathnamePatternString === "*" || pathnamePatternString === "/*");
|
|
5911
5916
|
|
|
5912
5917
|
return {
|
|
5913
5918
|
matchesEveryResource,
|
|
5914
5919
|
match: (resource) => {
|
|
5915
5920
|
const [pathname, search, hash] = resourceToParts(resource);
|
|
5916
5921
|
let decodedPathname = decodeURIComponent(pathname);
|
|
5917
|
-
let result;
|
|
5918
|
-
if (
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
result = {
|
|
5927
|
-
named: {},
|
|
5928
|
-
stars: [decodedPathname.slice(pathnamePatternString.length)],
|
|
5929
|
-
};
|
|
5922
|
+
let result = pathnamePattern.match(decodedPathname);
|
|
5923
|
+
if (!result && directoryPattern) {
|
|
5924
|
+
result = directoryPattern.match(
|
|
5925
|
+
decodedPathname.endsWith("/")
|
|
5926
|
+
? decodedPathname
|
|
5927
|
+
: `${decodedPathname}/`,
|
|
5928
|
+
);
|
|
5929
|
+
if (result) {
|
|
5930
|
+
result.stars = [...(result.stars || []), ""];
|
|
5930
5931
|
}
|
|
5931
|
-
} else {
|
|
5932
|
-
result = pathnamePattern.match(decodedPathname);
|
|
5933
5932
|
}
|
|
5934
5933
|
if (!result) {
|
|
5935
5934
|
return null;
|
|
@@ -6318,9 +6317,7 @@ const createRouter = (
|
|
|
6318
6317
|
};
|
|
6319
6318
|
};
|
|
6320
6319
|
const forEachMethodAllowed = (route, onMethodAllowed) => {
|
|
6321
|
-
const
|
|
6322
|
-
route.method === "*" ? HTTP_METHODS : [route.method];
|
|
6323
|
-
for (const supportedMethod of supportedMethods) {
|
|
6320
|
+
for (const supportedMethod of route.methods) {
|
|
6324
6321
|
onMethodAllowed(supportedMethod);
|
|
6325
6322
|
}
|
|
6326
6323
|
};
|
|
@@ -6553,10 +6550,12 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
|
|
|
6553
6550
|
}
|
|
6554
6551
|
if (!route.matchMethod(request.method)) {
|
|
6555
6552
|
// a 405 asserts the resource exists with other methods: a route
|
|
6556
|
-
// matching every resource ("GET *", "GET
|
|
6553
|
+
// matching every resource ("GET *", "GET /*") names no resource, so
|
|
6557
6554
|
// it cannot make that assertion and an unknown address stays a 404
|
|
6558
6555
|
if (!route.isFallback && !route.matchesEveryResource) {
|
|
6559
|
-
|
|
6556
|
+
for (const routeMethod of route.methods) {
|
|
6557
|
+
wouldHaveMatched.methodSet.add(routeMethod);
|
|
6558
|
+
}
|
|
6560
6559
|
}
|
|
6561
6560
|
continue;
|
|
6562
6561
|
}
|
|
@@ -6877,6 +6876,14 @@ const createRoute = ({
|
|
|
6877
6876
|
) {
|
|
6878
6877
|
availableMediaTypes.push(extensionWellKnownContentType);
|
|
6879
6878
|
}
|
|
6879
|
+
// HEAD is GET without the body (RFC 9110 §9.3.2): a GET route answers it and
|
|
6880
|
+
// the server drops the body when sending
|
|
6881
|
+
const methods =
|
|
6882
|
+
method === "*"
|
|
6883
|
+
? HTTP_METHODS
|
|
6884
|
+
: method === "GET"
|
|
6885
|
+
? ["GET", "HEAD"]
|
|
6886
|
+
: [method];
|
|
6880
6887
|
const resourcePattern = createResourcePattern(resource);
|
|
6881
6888
|
const headersPattern = headers ? createHeadersPattern(headers) : null;
|
|
6882
6889
|
|
|
@@ -6896,8 +6903,11 @@ const createRoute = ({
|
|
|
6896
6903
|
availableVersions,
|
|
6897
6904
|
availableEncodings,
|
|
6898
6905
|
acceptedMediaTypes,
|
|
6906
|
+
methods,
|
|
6899
6907
|
matchMethod:
|
|
6900
|
-
method === "*"
|
|
6908
|
+
method === "*"
|
|
6909
|
+
? () => true
|
|
6910
|
+
: (requestMethod) => methods.includes(requestMethod),
|
|
6901
6911
|
matchResource:
|
|
6902
6912
|
resource === "*"
|
|
6903
6913
|
? () => true
|
|
@@ -7318,7 +7328,10 @@ const TIMING_NOOP = () => {
|
|
|
7318
7328
|
* - `endpoint` {string} — Required. `"GET /users/:id"`: an http method (or `*` for any)
|
|
7319
7329
|
* and a resource pattern. `:name` captures a segment and `*` a run of segments, both
|
|
7320
7330
|
* land in `request.params`; `?page=:page` captures a search param. `"*"` alone
|
|
7321
|
-
* matches everything.
|
|
7331
|
+
* matches everything. Only `*` widens a pattern: `GET /` is the root alone and
|
|
7332
|
+
* `GET /docs/` that one path, while `GET /docs/*` answers `/docs`, `/docs/` and
|
|
7333
|
+
* everything under it (`request.params[0]` is `""` for the first two): declare the
|
|
7334
|
+
* routes it covers before it. An endpoint ending in `.websocket` marks a websocket route.
|
|
7322
7335
|
* - `fetch` {Function} — Required. `(request, helpers) => response`, async or not.
|
|
7323
7336
|
* Returns a `Response`, a plain `{ status, statusText, statusMessage, headers, body,
|
|
7324
7337
|
* timing }` object (`status` defaults to 404, `statusMessage` feeds the body of 4xx/5xx
|
|
@@ -19,16 +19,21 @@ await startServer({
|
|
|
19
19
|
|
|
20
20
|
`endpoint` is an http method (or `*`) followed by a resource pattern:
|
|
21
21
|
|
|
22
|
-
| endpoint | matches
|
|
23
|
-
| ---------------------- |
|
|
24
|
-
| `GET /` |
|
|
25
|
-
| `GET /users/:id` | `/users/42`, with `request.params.id === "42"`
|
|
26
|
-
| `GET /
|
|
27
|
-
| `GET /
|
|
28
|
-
|
|
|
29
|
-
| `GET
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
| endpoint | matches |
|
|
23
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `GET /` | `/` alone, whatever its search (`/?page=2`): the home page |
|
|
25
|
+
| `GET /users/:id` | `/users/42`, with `request.params.id === "42"` |
|
|
26
|
+
| `GET /docs/` | `/docs/` alone |
|
|
27
|
+
| `GET /docs/*` | `/docs/intro`, with `request.params[0] === "intro"`, and the directory itself: `/docs`, `/docs/` (`""`) |
|
|
28
|
+
| `GET /assets/*` | `/assets/css/main.css`, with `request.params[0] === "css/main.css"` |
|
|
29
|
+
| `GET /search?q=:query` | `/search?q=hello`, with `request.params.query === "hello"` |
|
|
30
|
+
| `* /api/*` | any method |
|
|
31
|
+
| `GET /*` | anything, with the path in `request.params[0]` |
|
|
32
|
+
| `GET *` | anything: a catch-all, put it last |
|
|
33
|
+
|
|
34
|
+
A `GET` route also answers `HEAD`: the response is built the same way and sent without its body.
|
|
35
|
+
|
|
36
|
+
Only `*` widens a pattern. Routes are tried in order, so a route covering others (`GET /docs/*`, `GET *`) goes after them. `fetch` can be async; returning `null` or `undefined` declines the request and the next route is tried.
|
|
32
37
|
|
|
33
38
|
When no route answers, the router builds the response from what almost matched:
|
|
34
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/server",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run build"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"launch-editor": "2.14.
|
|
40
|
+
"launch-editor": "2.14.2"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@jsenv/abort": "../../tooling/abort",
|
package/src/router/router.js
CHANGED
|
@@ -121,9 +121,7 @@ export const createRouter = (
|
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
123
|
const forEachMethodAllowed = (route, onMethodAllowed) => {
|
|
124
|
-
const
|
|
125
|
-
route.method === "*" ? HTTP_METHODS : [route.method];
|
|
126
|
-
for (const supportedMethod of supportedMethods) {
|
|
124
|
+
for (const supportedMethod of route.methods) {
|
|
127
125
|
onMethodAllowed(supportedMethod);
|
|
128
126
|
}
|
|
129
127
|
};
|
|
@@ -356,10 +354,12 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
|
|
|
356
354
|
}
|
|
357
355
|
if (!route.matchMethod(request.method)) {
|
|
358
356
|
// a 405 asserts the resource exists with other methods: a route
|
|
359
|
-
// matching every resource ("GET *", "GET
|
|
357
|
+
// matching every resource ("GET *", "GET /*") names no resource, so
|
|
360
358
|
// it cannot make that assertion and an unknown address stays a 404
|
|
361
359
|
if (!route.isFallback && !route.matchesEveryResource) {
|
|
362
|
-
|
|
360
|
+
for (const routeMethod of route.methods) {
|
|
361
|
+
wouldHaveMatched.methodSet.add(routeMethod);
|
|
362
|
+
}
|
|
363
363
|
}
|
|
364
364
|
continue;
|
|
365
365
|
}
|
|
@@ -683,6 +683,14 @@ const createRoute = ({
|
|
|
683
683
|
) {
|
|
684
684
|
availableMediaTypes.push(extensionWellKnownContentType);
|
|
685
685
|
}
|
|
686
|
+
// HEAD is GET without the body (RFC 9110 §9.3.2): a GET route answers it and
|
|
687
|
+
// the server drops the body when sending
|
|
688
|
+
const methods =
|
|
689
|
+
method === "*"
|
|
690
|
+
? HTTP_METHODS
|
|
691
|
+
: method === "GET"
|
|
692
|
+
? ["GET", "HEAD"]
|
|
693
|
+
: [method];
|
|
686
694
|
const resourcePattern = createResourcePattern(resource);
|
|
687
695
|
const headersPattern = headers ? createHeadersPattern(headers) : null;
|
|
688
696
|
|
|
@@ -702,8 +710,11 @@ const createRoute = ({
|
|
|
702
710
|
availableVersions,
|
|
703
711
|
availableEncodings,
|
|
704
712
|
acceptedMediaTypes,
|
|
713
|
+
methods,
|
|
705
714
|
matchMethod:
|
|
706
|
-
method === "*"
|
|
715
|
+
method === "*"
|
|
716
|
+
? () => true
|
|
717
|
+
: (requestMethod) => methods.includes(requestMethod),
|
|
707
718
|
matchResource:
|
|
708
719
|
resource === "*"
|
|
709
720
|
? () => true
|
package/src/start_server.js
CHANGED
|
@@ -59,7 +59,10 @@ const TIMING_NOOP = () => {
|
|
|
59
59
|
* - `endpoint` {string} — Required. `"GET /users/:id"`: an http method (or `*` for any)
|
|
60
60
|
* and a resource pattern. `:name` captures a segment and `*` a run of segments, both
|
|
61
61
|
* land in `request.params`; `?page=:page` captures a search param. `"*"` alone
|
|
62
|
-
* matches everything.
|
|
62
|
+
* matches everything. Only `*` widens a pattern: `GET /` is the root alone and
|
|
63
|
+
* `GET /docs/` that one path, while `GET /docs/*` answers `/docs`, `/docs/` and
|
|
64
|
+
* everything under it (`request.params[0]` is `""` for the first two): declare the
|
|
65
|
+
* routes it covers before it. An endpoint ending in `.websocket` marks a websocket route.
|
|
63
66
|
* - `fetch` {Function} — Required. `(request, helpers) => response`, async or not.
|
|
64
67
|
* Returns a `Response`, a plain `{ status, statusText, statusMessage, headers, body,
|
|
65
68
|
* timing }` object (`status` defaults to 404, `statusMessage` feeds the body of 4xx/5xx
|