reachable-url 1.8.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/index.js +79 -16
- package/package.json +10 -15
package/README.md
CHANGED
|
@@ -35,6 +35,59 @@ The target URL to be resolved.
|
|
|
35
35
|
|
|
36
36
|
Same as [got#options](https://github.com/sindresorhus/got#goturl-options)
|
|
37
37
|
|
|
38
|
+
Passing `cache` keeps the whole body, since a cache entry is only written once the body has been read in full:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const cache = new Map()
|
|
42
|
+
const response = await reachableUrl('https://example.com/video.mp4', { cache })
|
|
43
|
+
response.body // => the whole entity, so it can be cached
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`cache` needs [@kikobeats/cacheable-request](https://github.com/Kikobeats/cacheable-request): the version [got](https://github.com/sindresorhus/got) pulls in never settles when the origin keeps the connection alive, and no timeout recovers from it. Declare the override, otherwise passing `cache` throws:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# pnpm-workspace.yaml
|
|
50
|
+
overrides:
|
|
51
|
+
got>cacheable-request: npm:@kikobeats/cacheable-request
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### returns
|
|
55
|
+
|
|
56
|
+
The [got response](https://github.com/sindresorhus/got#response), plus `requestUrl`, `redirectUrls`, `redirectStatusCodes` and the `followRedirect` in effect.
|
|
57
|
+
|
|
58
|
+
The request asks for a single byte (`Range: bytes=0-0`). When a server ignores that and starts sending the whole entity, the download is cancelled: the status and headers already say whether the URL is reachable, so `body` is `undefined` on those responses.
|
|
59
|
+
|
|
60
|
+
A `206` that did answer the range is reported as the `200` it stands for, with `content-length` taken from `content-range`.
|
|
61
|
+
|
|
62
|
+
### reachableUrl.isReachable(response)
|
|
63
|
+
|
|
64
|
+
#### response
|
|
65
|
+
|
|
66
|
+
*Required*<br>
|
|
67
|
+
Type: `object`
|
|
68
|
+
|
|
69
|
+
The response returned by `reachableUrl`, which echoes back `followRedirect` so it can be handed straight over.
|
|
70
|
+
|
|
71
|
+
A URL is reachable when the response is a final 2xx.
|
|
72
|
+
|
|
73
|
+
A redirect status is the final answer only when redirects were not being followed:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const response = await reachableUrl('https://example.com', { followRedirect: false })
|
|
77
|
+
reachableUrl.isReachable(response) // => true, the 3xx is the destination
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
With redirect following on (the default), a 3xx is the hop the follow stopped at (a `beforeRedirect` hook threw, `maxRedirects` ran out), meaning the URL was never reached:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
const response = await reachableUrl('https://example.com', {
|
|
84
|
+
hooks: { beforeRedirect: [() => { throw new Error('refused') }] }
|
|
85
|
+
})
|
|
86
|
+
reachableUrl.isReachable(response) // => false
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A partial object missing `followRedirect` is judged as if redirects were being followed, so a bare `{ statusCode: 302 }` is unreachable.
|
|
90
|
+
|
|
38
91
|
## License
|
|
39
92
|
|
|
40
93
|
**reachable-url** © [Kiko Beats](https://kikobeats.com), released under the [MIT](https://github.com/Kikobeats/reachable-url/blob/master/LICENSE.md) License.<br>
|
package/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const pReflect = require('p-reflect')
|
|
4
3
|
const { URL } = require('url')
|
|
5
4
|
|
|
5
|
+
const RANGE_LENGTH = 1
|
|
6
|
+
|
|
6
7
|
const got = require('got').extend({
|
|
7
8
|
decompress: false,
|
|
8
9
|
responseType: 'buffer',
|
|
9
10
|
retry: 1,
|
|
10
11
|
headers: {
|
|
11
|
-
Range:
|
|
12
|
+
Range: `bytes=0-${RANGE_LENGTH - 1}`
|
|
12
13
|
},
|
|
13
14
|
hooks: {
|
|
14
15
|
beforeRetry: [
|
|
@@ -19,15 +20,60 @@ const got = require('got').extend({
|
|
|
19
20
|
}
|
|
20
21
|
})
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// `headers` is a prototype getter on IncomingMessage, so spreading never carries it.
|
|
24
|
+
const toResponse = response => ({
|
|
23
25
|
statusMessage: 'Not Found',
|
|
24
26
|
statusCode: 404,
|
|
25
|
-
|
|
26
|
-
...
|
|
27
|
-
...responseDestination
|
|
27
|
+
...response,
|
|
28
|
+
headers: { ...response?.headers }
|
|
28
29
|
})
|
|
29
30
|
|
|
31
|
+
const CACHE_ERROR = `The \`cache\` option needs @kikobeats/cacheable-request.
|
|
32
|
+
|
|
33
|
+
got resolved the unpatched cacheable-request@7, which never settles when the
|
|
34
|
+
origin keeps the connection alive, and no timeout recovers from it. Add the
|
|
35
|
+
override to your pnpm-workspace.yaml:
|
|
36
|
+
|
|
37
|
+
overrides:
|
|
38
|
+
got>cacheable-request: npm:@kikobeats/cacheable-request`
|
|
39
|
+
|
|
40
|
+
const loadCacheableRequestManifest = () =>
|
|
41
|
+
require(require.resolve('cacheable-request/package.json', { paths: [require.resolve('got')] }))
|
|
42
|
+
|
|
43
|
+
// The override is an alias, which keeps the real package name, so the manifest says
|
|
44
|
+
// which one got resolved. Only a positive match rejects: the lookup can legitimately
|
|
45
|
+
// fail under a bundler, and refusing to run then is worse than missing the warning.
|
|
46
|
+
const assertCacheSupport = (load = loadCacheableRequestManifest) => {
|
|
47
|
+
let name
|
|
48
|
+
try {
|
|
49
|
+
name = load().name
|
|
50
|
+
} catch {}
|
|
51
|
+
if (name === 'cacheable-request') throw new TypeError(CACHE_ERROR)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const honoredRange = res =>
|
|
55
|
+
res.statusCode === 206 && Number(res.headers['content-length']) <= RANGE_LENGTH
|
|
56
|
+
|
|
57
|
+
const willRetry = res => {
|
|
58
|
+
const { retry, method } = res.request.options
|
|
59
|
+
return (
|
|
60
|
+
res.retryCount < retry.limit &&
|
|
61
|
+
retry.methods.includes(method) &&
|
|
62
|
+
retry.statusCodes.includes(res.statusCode) &&
|
|
63
|
+
// got declines a 413 outright unless it carries a `retry-after`.
|
|
64
|
+
(res.statusCode !== 413 || res.headers['retry-after'] !== undefined)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// `responseType: 'buffer'` would read the whole entity a server sent despite
|
|
69
|
+
// `Range`; the status and headers already answer reachability. A cache entry is
|
|
70
|
+
// written on `end` and a retry needs a retryable error, so both keep the body.
|
|
71
|
+
const shouldAbortDownload = res =>
|
|
72
|
+
!res.request.options.cache && !honoredRange(res) && !willRetry(res)
|
|
73
|
+
|
|
30
74
|
const reachableUrl = async (url, opts) => {
|
|
75
|
+
if (opts?.cache) assertCacheSupport()
|
|
76
|
+
const followRedirect = opts?.followRedirect ?? got.defaults.options.followRedirect
|
|
31
77
|
const req = got(url, opts)
|
|
32
78
|
|
|
33
79
|
const redirectStatusCodes = []
|
|
@@ -36,6 +82,11 @@ const reachableUrl = async (url, opts) => {
|
|
|
36
82
|
|
|
37
83
|
req.on('response', res => {
|
|
38
84
|
response = res
|
|
85
|
+
if (!shouldAbortDownload(res)) return
|
|
86
|
+
// `phases.total` is filled on `end`, which a cancelled request never emits,
|
|
87
|
+
// and the timer does not record the cancel either.
|
|
88
|
+
res.timings.phases.total = Date.now() - res.timings.start
|
|
89
|
+
req.cancel()
|
|
39
90
|
})
|
|
40
91
|
|
|
41
92
|
req.on('redirect', res => {
|
|
@@ -43,34 +94,45 @@ const reachableUrl = async (url, opts) => {
|
|
|
43
94
|
redirectStatusCodes.push(res.statusCode)
|
|
44
95
|
})
|
|
45
96
|
|
|
46
|
-
|
|
97
|
+
let errorResponse
|
|
98
|
+
try {
|
|
99
|
+
await req
|
|
100
|
+
} catch (error) {
|
|
101
|
+
errorResponse = error.response
|
|
102
|
+
}
|
|
47
103
|
|
|
48
|
-
|
|
104
|
+
// A retried or cancelled request settles with the attempt before it, whose body
|
|
105
|
+
// is not this response's, so a response we saw outranks the error's. The error
|
|
106
|
+
// still carries one when nothing else did, as MaxRedirects does.
|
|
107
|
+
const resolvedResponse = toResponse(response ?? errorResponse)
|
|
49
108
|
|
|
50
|
-
if (
|
|
51
|
-
const contentRange =
|
|
109
|
+
if (resolvedResponse.statusCode === 206) {
|
|
110
|
+
const contentRange = resolvedResponse.headers['content-range']
|
|
52
111
|
if (typeof contentRange === 'string') {
|
|
53
112
|
let contentLength = contentRange.split('/')
|
|
54
113
|
if (contentLength.length > 1) {
|
|
55
114
|
contentLength = contentLength[contentLength.length - 1]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
115
|
+
resolvedResponse.statusCode = 200
|
|
116
|
+
resolvedResponse.statusMessage = 'OK'
|
|
117
|
+
resolvedResponse.headers['content-length'] = contentLength
|
|
118
|
+
resolvedResponse.headers['content-range'] = undefined
|
|
60
119
|
}
|
|
61
120
|
}
|
|
62
121
|
}
|
|
63
122
|
|
|
64
123
|
return {
|
|
65
124
|
url,
|
|
66
|
-
...
|
|
125
|
+
...resolvedResponse,
|
|
126
|
+
followRedirect,
|
|
67
127
|
redirectUrls,
|
|
68
128
|
redirectStatusCodes,
|
|
69
129
|
requestUrl: url
|
|
70
130
|
}
|
|
71
131
|
}
|
|
72
132
|
|
|
73
|
-
|
|
133
|
+
// While following, a 3xx is the hop the follow stopped at, never the destination.
|
|
134
|
+
const isReachable = ({ statusCode, followRedirect = true }) =>
|
|
135
|
+
statusCode >= 200 && statusCode < (followRedirect ? 300 : 400)
|
|
74
136
|
|
|
75
137
|
module.exports = async (url, opts) => {
|
|
76
138
|
if (/^\/\//.test(url)) url = `https:${url}`
|
|
@@ -78,3 +140,4 @@ module.exports = async (url, opts) => {
|
|
|
78
140
|
}
|
|
79
141
|
|
|
80
142
|
module.exports.isReachable = isReachable
|
|
143
|
+
module.exports.assertCacheSupport = assertCacheSupport
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reachable-url",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "Resolve a URL as fast as possible and report whether the destination is reachable over HTTP.",
|
|
4
4
|
"homepage": "https://github.com/Kikobeats/reachable-url",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "2.0.0",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"author": {
|
|
8
8
|
"email": "josefrancisco.verdu@gmail.com",
|
|
@@ -18,23 +18,24 @@
|
|
|
18
18
|
"url": "https://github.com/Kikobeats/reachable-url/issues"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"check",
|
|
22
|
+
"health",
|
|
23
|
+
"http",
|
|
24
|
+
"ping",
|
|
25
|
+
"probe",
|
|
23
26
|
"reachability",
|
|
24
27
|
"reachable",
|
|
25
|
-
"
|
|
28
|
+
"redirect",
|
|
26
29
|
"resolve",
|
|
27
30
|
"url"
|
|
28
31
|
],
|
|
29
32
|
"dependencies": {
|
|
30
|
-
"got": "~11.8.0"
|
|
31
|
-
"p-reflect": "~2.1.0"
|
|
33
|
+
"got": "~11.8.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@commitlint/cli": "latest",
|
|
35
37
|
"@commitlint/config-conventional": "latest",
|
|
36
38
|
"@kikobeats/cacheable-request": "latest",
|
|
37
|
-
"@ksmithut/prettier-standard": "latest",
|
|
38
39
|
"async-listen": "latest",
|
|
39
40
|
"ava": "latest",
|
|
40
41
|
"c8": "latest",
|
|
@@ -81,19 +82,13 @@
|
|
|
81
82
|
},
|
|
82
83
|
"nano-staged": {
|
|
83
84
|
"*.js": [
|
|
84
|
-
"prettier-standard",
|
|
85
|
+
"npx @kikobeats/prettier-standard",
|
|
85
86
|
"standard --fix"
|
|
86
87
|
],
|
|
87
88
|
"package.json": [
|
|
88
89
|
"finepack"
|
|
89
90
|
]
|
|
90
91
|
},
|
|
91
|
-
"pnpm": {
|
|
92
|
-
"neverBuiltDependencies": [],
|
|
93
|
-
"overrides": {
|
|
94
|
-
"got>cacheable-request": "npm:@kikobeats/cacheable-request"
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
92
|
"simple-git-hooks": {
|
|
98
93
|
"commit-msg": "npx commitlint --edit",
|
|
99
94
|
"pre-commit": "npx nano-staged"
|