dsh-moli 1.1.13 → 1.1.14
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 +62 -2
- package/README.zh.md +56 -2
- package/index.js +25 -0
- package/lib/moli.js +123 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -210,8 +210,9 @@ An unresolvable bundle is **skipped silently** apart from one stderr line
|
|
|
210
210
|
## Usage
|
|
211
211
|
|
|
212
212
|
`moli_fetch` parameters: `url` (required), `dump`, `writeTo`, `eval`, `scroll`, `wait`,
|
|
213
|
-
`waitSelector`, `
|
|
214
|
-
`
|
|
213
|
+
`waitSelector`, `waitScript`, `waitResponseUrl`, `waitResponseJson`, `delayMs`, `timeoutMs`,
|
|
214
|
+
`maxChars`, `headers`, `userAgent`, `layout`, `scrollbars`, `noJs`, `noCss`, `resources`,
|
|
215
|
+
`withFrames`, `insecureTls`, `failOnHttpError`, `profileDir`, `cookieFile`, `proxy`.
|
|
215
216
|
|
|
216
217
|
```
|
|
217
218
|
moli_fetch url=https://example.com dump=markdown # token-efficient text
|
|
@@ -244,6 +245,43 @@ scrolled `dump=screenshot` shows that part of the page.
|
|
|
244
245
|
It runs as part of the engine's own pre-capture evaluation and therefore composes with `eval`: the
|
|
245
246
|
scroll happens first, and the expression still returns its own value.
|
|
246
247
|
|
|
248
|
+
### `failOnHttpError` — stop reading an error page as content
|
|
249
|
+
|
|
250
|
+
By default a 404 is returned like any other page, so a failed fetch looks successful. Setting
|
|
251
|
+
`failOnHttpError` turns a final status of 400 or above into `ok: false` with the status named — and
|
|
252
|
+
**keeps the body**, because an error page is often the thing you need to read.
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
moli_fetch url=https://example.com/nope dump=markdown failOnHttpError=true
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| | default | `failOnHttpError: true` |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| a 404 | `ok: true`, the error page as content | `ok: false`, `status: 404`, body still returned |
|
|
261
|
+
| a screenshot of a 404 | written as a normal image | still written, and reported as a failure |
|
|
262
|
+
|
|
263
|
+
It is off by default so existing calls keep their behaviour. A JSON dump already reported the status
|
|
264
|
+
on its own; markdown and HTML dumps did not, and that is the gap this closes.
|
|
265
|
+
|
|
266
|
+
### Waiting for what the page actually fetches
|
|
267
|
+
|
|
268
|
+
`wait: "networkidle"` plus `delayMs` is a guess about how long a list takes to arrive. These wait for
|
|
269
|
+
the request itself:
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
# the response that carries the data
|
|
273
|
+
moli_fetch url=https://example.com/feed dump=markdown waitResponseUrl="/api/feed"
|
|
274
|
+
|
|
275
|
+
# the payload, when the URL is not distinctive
|
|
276
|
+
moli_fetch url=https://example.com/feed dump=markdown waitResponseJson="data.list.0.id=42"
|
|
277
|
+
|
|
278
|
+
# the page's own idea of readiness
|
|
279
|
+
moli_fetch url=https://example.com/app dump=markdown waitScript="window.appReady === true"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`waitResponseJson` takes `PATH=VALUE`, where `PATH` addresses the field and `VALUE` is compared
|
|
283
|
+
literally. Use these when a fixed delay either wastes time or reads the page too early.
|
|
284
|
+
|
|
247
285
|
## Relationship to Moli's own agent skills
|
|
248
286
|
|
|
249
287
|
Moli ships official agent skills in its repository — [`moli-webfetch`](https://github.com/lexmount/moli/tree/main/skills/moli-webfetch),
|
|
@@ -403,6 +441,13 @@ Four suites live in [`verify/`](verify/), and all are runnable against a working
|
|
|
403
441
|
`bottom`, nonsense rejected, the caller's `eval` value preserved) and then, when an engine is
|
|
404
442
|
present, fetches a page whose `scroll` handler fills a slot and asserts the content appears with
|
|
405
443
|
`scroll` and the placeholder appears without it — a property of the output, not of an internal call.
|
|
444
|
+
- `http-verify.mjs` asserts the parameter-to-flag mapping directly, because a parameter that never
|
|
445
|
+
reaches the engine is indistinguishable from a feature that does not work. It then fetches a 404 and
|
|
446
|
+
checks that `failOnHttpError` reports it while keeping the body, that a healthy page is unaffected,
|
|
447
|
+
and that a screenshot of an error page is still written as a valid PNG rather than decoded as text.
|
|
448
|
+
The end-to-end half needs an HTTP error from a real server: loopback is unreachable from inside this
|
|
449
|
+
sandbox — `curl.exe` times out on `127.0.0.1` while the Node host reaches it — so it uses a path
|
|
450
|
+
under `example.com` and skips when the network is away.
|
|
406
451
|
|
|
407
452
|
`.github/workflows/verify.yml` runs `schema-verify`, `empty-verify` and `scroll-verify` on Linux,
|
|
408
453
|
Windows and macOS (they need no engine and no network) and attempts the engine-dependent suites on
|
|
@@ -467,6 +512,21 @@ which reads as a missing package and is actually an authorisation failure. Run
|
|
|
467
512
|
`node scripts/preflight-publish.mjs` before any local publish: it reports whether the credential can
|
|
468
513
|
authenticate and names that misleading 404 when it cannot.
|
|
469
514
|
|
|
515
|
+
### Deprecating a version: do it on the website
|
|
516
|
+
|
|
517
|
+
Publishing is scripted; deprecating deliberately is not. Use the package's page on npmjs.com —
|
|
518
|
+
**package → Settings → Deprecate** — and name the versions and the reason.
|
|
519
|
+
|
|
520
|
+
`npm deprecate dsh-moli@1.1.11 "…"` produces the same misleading failure as above when the local
|
|
521
|
+
credential is dead, and there is a second reason not to reach for a token here. npm restricted
|
|
522
|
+
2FA-bypass granular tokens from package-management operations on 31 July 2026, so the kind of token
|
|
523
|
+
that used to make `deprecate` scriptable is being withdrawn, while `publish` has a supported
|
|
524
|
+
token-free path through OIDC. A release process that needs a token only for the rare act of
|
|
525
|
+
deprecating is a process that will be broken the next time it is needed, which is exactly what
|
|
526
|
+
happened with 1.1.11 and 1.1.12.
|
|
527
|
+
|
|
528
|
+
The 1.0.0 and 1.0.1 deprecations were done this way, from the website.
|
|
529
|
+
|
|
470
530
|
## License
|
|
471
531
|
|
|
472
532
|
MIT. See [LICENSE](LICENSE).
|
package/README.zh.md
CHANGED
|
@@ -196,8 +196,9 @@ profile 插件需要两半都齐:依赖边(`dependencies`)+ `dsh.profile.b
|
|
|
196
196
|
## 用法
|
|
197
197
|
|
|
198
198
|
`moli_fetch` 参数:`url`(必填)、`dump`、`writeTo`、`eval`、`scroll`、`wait`、`waitSelector`、
|
|
199
|
-
`delayMs`、`timeoutMs`、`maxChars`、`headers
|
|
200
|
-
`noCss`、`resources`、`withFrames`、`insecureTls
|
|
199
|
+
`waitScript`、`waitResponseUrl`、`waitResponseJson`、`delayMs`、`timeoutMs`、`maxChars`、`headers`、
|
|
200
|
+
`userAgent`、`layout`、`scrollbars`、`noJs`、`noCss`、`resources`、`withFrames`、`insecureTls`、
|
|
201
|
+
`failOnHttpError`、`profileDir`、`cookieFile`、`proxy`。
|
|
201
202
|
|
|
202
203
|
```
|
|
203
204
|
moli_fetch url=https://example.com dump=markdown # 省 token 的正文
|
|
@@ -229,6 +230,41 @@ moli_fetch url=https://example.com/feed dump=markdown scroll=-400 # 往回滚
|
|
|
229
230
|
它作为引擎自身"抓取前求值"的一部分执行,因此与 `eval` 可组合:**先滚动,表达式再读取页面,并且仍然返回
|
|
230
231
|
它自己的值**。
|
|
231
232
|
|
|
233
|
+
### `failOnHttpError` —— 别再把错误页当正文读
|
|
234
|
+
|
|
235
|
+
默认情况下 404 和普通页面一样被返回,于是**抓取失败看起来是成功的**。加上 `failOnHttpError` 后,最终状态码
|
|
236
|
+
400 及以上会变成 `ok: false` 并报出状态码——**而且保留响应体**,因为错误页往往正是你需要读的东西。
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
moli_fetch url=https://example.com/nope dump=markdown failOnHttpError=true
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| | 默认 | `failOnHttpError: true` |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| 一个 404 | `ok: true`,错误页作为正文 | `ok: false`、`status: 404`,响应体仍然返回 |
|
|
245
|
+
| 404 的截图 | 当普通图片写出 | 仍然写出,但报告为失败 |
|
|
246
|
+
|
|
247
|
+
默认关闭,因此现有调用行为不变。JSON dump 本来就能自己报出状态码;markdown 与 HTML 不能,这个开关补的正是
|
|
248
|
+
这个缺口。
|
|
249
|
+
|
|
250
|
+
### 等待页面真正取回的东西
|
|
251
|
+
|
|
252
|
+
`wait: "networkidle"` 加 `delayMs` 是在**猜**列表要多久才到。下面这几个等的是请求本身:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
# 等待承载数据的那次响应
|
|
256
|
+
moli_fetch url=https://example.com/feed dump=markdown waitResponseUrl="/api/feed"
|
|
257
|
+
|
|
258
|
+
# URL 不够有特征时,按载荷内容等
|
|
259
|
+
moli_fetch url=https://example.com/feed dump=markdown waitResponseJson="data.list.0.id=42"
|
|
260
|
+
|
|
261
|
+
# 让页面用自己的方式表示"就绪"
|
|
262
|
+
moli_fetch url=https://example.com/app dump=markdown waitScript="window.appReady === true"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`waitResponseJson` 接收 `PATH=VALUE`:`PATH` 定位字段,`VALUE` 按字面比较。当固定延时不是浪费时间就是读得太
|
|
266
|
+
早时,用这几个。
|
|
267
|
+
|
|
232
268
|
## 与 Moli 官方 agent skill 的关系
|
|
233
269
|
|
|
234
270
|
Moli 仓库自带官方 agent skill:[`moli-webfetch`](https://github.com/lexmount/moli/tree/main/skills/moli-webfetch)、
|
|
@@ -358,6 +394,11 @@ profile。无论生效的是哪个代理,loopback 地址一律绕过它。
|
|
|
358
394
|
- `scroll-verify.mjs` 在无引擎时校验表达式构造(像素值、负数、`top`、`bottom`、无效输入被拒、调用方的 `eval`
|
|
359
395
|
值被保留),有引擎时抓取一个"滚动处理器填充槽位"的页面,断言带 `scroll` 时内容出现、不带时只有占位符——
|
|
360
396
|
断言的是**输出**的性质,而不是内部调用。
|
|
397
|
+
- `http-verify.mjs` 直接断言"参数 → 引擎选项"的映射,因为**参数没送到引擎和功能不生效,从外面看是一样的**。
|
|
398
|
+
随后抓一个 404,验证 `failOnHttpError` 会报告失败同时保留响应体、健康页面不受影响、以及错误页的截图仍然被
|
|
399
|
+
写为合法 PNG 而不是被当文本解码。端到端部分需要一个真实服务器的 HTTP 错误:**本沙箱内无法访问 loopback**
|
|
400
|
+
——`curl.exe` 连 `127.0.0.1` 会超时,而 Node 宿主进程可以——所以它用 `example.com` 下的一个路径,网络不可用
|
|
401
|
+
时跳过。
|
|
361
402
|
|
|
362
403
|
`.github/workflows/verify.yml` 在 Linux、Windows、macOS 上跑 `schema-verify`、`empty-verify` 与
|
|
363
404
|
`scroll-verify`(无需引擎、无需网络),在 Linux 与 Windows 上尝试依赖引擎的几套;若 runner 上没有引擎,
|
|
@@ -414,6 +455,19 @@ E404 Not Found - PUT https://registry.npmjs.org/dsh-moli
|
|
|
414
455
|
**这看起来像"包不存在",实际是认证失败。** 任何本地发布之前先跑
|
|
415
456
|
`node scripts/preflight-publish.mjs`:它会告诉你凭据能否认证,不能时明确点出这个误导性的 404。
|
|
416
457
|
|
|
458
|
+
### 废弃某个版本:去网站操作,别用命令行
|
|
459
|
+
|
|
460
|
+
发布是脚本化的,**废弃则刻意不做成脚本**。用 npmjs.com 上的包页面——**package → Settings →
|
|
461
|
+
Deprecate**——选版本、写理由。
|
|
462
|
+
|
|
463
|
+
`npm deprecate dsh-moli@1.1.11 "…"` 在本机凭据失效时会给出上面那个同样误导的失败,而且这里还有第二个
|
|
464
|
+
理由不要依赖 token:npm 在 **2026 年 7 月 31 日**起禁止 2FA-bypass 类型的 granular token 执行**包管理**
|
|
465
|
+
操作,也就是说"以前能让 `deprecate` 脚本化"的那类凭据正在被取消;而 `publish` 有受支持的、免 token 的
|
|
466
|
+
OIDC 路径。**一个只在"偶尔废弃版本"时才需要 token 的流程,注定在下次需要它时是坏的**——1.1.11 与 1.1.12
|
|
467
|
+
遇到的正是这种情况。
|
|
468
|
+
|
|
469
|
+
1.0.0 与 1.0.1 的废弃就是通过网站完成的。
|
|
470
|
+
|
|
417
471
|
## 许可
|
|
418
472
|
|
|
419
473
|
MIT,见 [LICENSE](LICENSE)。
|
package/index.js
CHANGED
|
@@ -77,6 +77,22 @@ const FETCH_PARAMETERS = {
|
|
|
77
77
|
description: 'Readiness stage to wait for. Defaults to `done`.',
|
|
78
78
|
},
|
|
79
79
|
waitSelector: { type: 'string', description: 'Wait until this CSS selector matches.' },
|
|
80
|
+
waitScript: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Wait until this JavaScript expression is truthy; a function result is called as the predicate on each poll. More precise than a fixed delay when the page signals readiness in its own terms.',
|
|
83
|
+
},
|
|
84
|
+
waitResponseUrl: {
|
|
85
|
+
type: 'string',
|
|
86
|
+
description: 'Wait for a response whose original or final URL contains this substring. This is the reliable way to await an AJAX-built list: `wait: "networkidle"` plus a delay is a guess, whereas this waits for the request that actually carries the data.',
|
|
87
|
+
},
|
|
88
|
+
waitResponseJson: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Wait for a response whose JSON field equals a value, written as PATH=VALUE (for example "data.list.0.id=42"). The counterpart to waitResponseUrl when the condition is in the payload rather than the URL.',
|
|
91
|
+
},
|
|
92
|
+
failOnHttpError: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description: 'Treat a final main-document HTTP status of 400 or above as a failure. Without it an error page comes back as ordinary content and a 404 reads like a successful fetch; with it the result is ok:false and reports the status, while still carrying the rendered body so the error page can be read. Off by default, so existing calls keep their behaviour.',
|
|
95
|
+
},
|
|
80
96
|
delayMs: { type: 'integer', description: 'Extra milliseconds to wait after readiness.' },
|
|
81
97
|
timeoutMs: { type: 'integer', description: 'Total readiness deadline in milliseconds. Defaults to the plugin configuration.' },
|
|
82
98
|
maxChars: { type: 'integer', description: 'Truncate inline text output to this many characters. Defaults to the plugin configuration.' },
|
|
@@ -125,6 +141,7 @@ const FETCH_OUTPUT_SCHEMA = {
|
|
|
125
141
|
error: { type: 'string' },
|
|
126
142
|
hint: { type: 'string' },
|
|
127
143
|
status: { type: 'integer' },
|
|
144
|
+
httpError: { type: 'boolean' },
|
|
128
145
|
sandboxTlsBlock: { type: 'boolean' },
|
|
129
146
|
unreachable: { type: 'boolean' },
|
|
130
147
|
possiblyUnreachable: { type: 'boolean' },
|
|
@@ -172,6 +189,14 @@ function renderFetch(_args, value) {
|
|
|
172
189
|
if (value.unreachable === true) lines.push('Cause: the host did not answer; a local forward proxy is the usual fix on this network.');
|
|
173
190
|
else if (value.possiblyUnreachable === true) lines.push('Cause: the host did not answer in time; it may be blocked, or the page may simply be slow.');
|
|
174
191
|
if (typeof value.hint === 'string' && value.hint !== '') lines.push(`Next: ${value.hint}`);
|
|
192
|
+
// An HTTP error keeps its body under `failOnHttpError`. Reporting the failure
|
|
193
|
+
// without the body would throw away the error page, which is the part worth
|
|
194
|
+
// reading -- and for a binary dump the body is the file that was written.
|
|
195
|
+
if (value.kind === 'binary' && typeof value.path === 'string') {
|
|
196
|
+
lines.push(`The error page was still written to ${value.path} (${String(value.bytes)} bytes).`);
|
|
197
|
+
} else if (typeof value.content === 'string' && value.content !== '') {
|
|
198
|
+
lines.push('', 'The server\'s response body:', '', value.content);
|
|
199
|
+
}
|
|
175
200
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
176
201
|
}
|
|
177
202
|
if (value.kind === 'binary') {
|
package/lib/moli.js
CHANGED
|
@@ -31,6 +31,13 @@ const TEXT_DUMPS = new Set([undefined, 'html', 'markdown', 'json', 'semantic_tre
|
|
|
31
31
|
/** Accepted `--wait` lifecycle stages. */
|
|
32
32
|
const WAIT_STAGES = new Set(['domcontentloaded', 'load', 'networkidle', 'domstable', 'done']);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The exit code the engine uses for "the final main-document status was >= 400".
|
|
36
|
+
* Its help states it for `--fail` and `--fail-with-body`; treating it specially is
|
|
37
|
+
* what lets an HTTP error be reported without discarding the body.
|
|
38
|
+
*/
|
|
39
|
+
const ENGINE_HTTP_ERROR_STATUS = 22;
|
|
40
|
+
|
|
34
41
|
/** `scroll` keywords, mapped to what they mean in the page. */
|
|
35
42
|
const SCROLL_TOKENS = {
|
|
36
43
|
top: '0',
|
|
@@ -300,7 +307,18 @@ export function fetchOnce(options) {
|
|
|
300
307
|
* @param args - validated tool arguments.
|
|
301
308
|
* @returns the argv tail (everything after the executable).
|
|
302
309
|
*/
|
|
303
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Build the `moli fetch` argument vector for one call.
|
|
312
|
+
*
|
|
313
|
+
* Exported so the mapping from tool parameters to engine flags can be asserted
|
|
314
|
+
* directly. That matters more than it sounds: a parameter that silently fails to
|
|
315
|
+
* reach the engine is indistinguishable from a feature that does not work, and the
|
|
316
|
+
* only fast, deterministic way to tell them apart is to check the vector.
|
|
317
|
+
*
|
|
318
|
+
* @param args - tool arguments.
|
|
319
|
+
* @returns the argv tail (without the executable).
|
|
320
|
+
*/
|
|
321
|
+
export function buildArgs(args) {
|
|
304
322
|
if (args.dump !== undefined && !TEXT_DUMPS.has(args.dump) && !BINARY_DUMPS.has(args.dump)) {
|
|
305
323
|
throw new Error(`unsupported dump: ${String(args.dump)}`);
|
|
306
324
|
}
|
|
@@ -320,6 +338,16 @@ function buildArgs(args) {
|
|
|
320
338
|
if (evaluation !== undefined) argv.push('--eval', evaluation);
|
|
321
339
|
if (args.wait !== undefined && args.wait !== '') argv.push('--wait', args.wait);
|
|
322
340
|
if (args.waitSelector !== undefined && args.waitSelector !== '') argv.push('--wait-selector', args.waitSelector);
|
|
341
|
+
if (args.waitScript !== undefined && args.waitScript !== '') argv.push('--wait-script', args.waitScript);
|
|
342
|
+
// Response waits: the right way to await an AJAX list, where a fixed delay is a
|
|
343
|
+
// guess. The engine takes one of each, and a caller supplying several is asking
|
|
344
|
+
// for "any of these", which is the engine's own semantics for multiple waits.
|
|
345
|
+
if (args.waitResponseUrl !== undefined && args.waitResponseUrl !== '') argv.push('--wait-response-url', args.waitResponseUrl);
|
|
346
|
+
if (args.waitResponseJson !== undefined && args.waitResponseJson !== '') argv.push('--wait-response-json', args.waitResponseJson);
|
|
347
|
+
// The engine's own switch, but only when the caller asked for a failure to be
|
|
348
|
+
// surfaced: adding it unconditionally would turn every 404 into `ok: false` and
|
|
349
|
+
// change what existing calls do.
|
|
350
|
+
if (args.failOnHttpError === true) argv.push('--fail-with-body');
|
|
323
351
|
if (args.delayMs !== undefined) argv.push('--delay-ms', String(args.delayMs));
|
|
324
352
|
if (args.noJs === true) argv.push('--disable-js');
|
|
325
353
|
if (args.noCss === true) argv.push('--disable-css');
|
|
@@ -413,6 +441,54 @@ export function runFetch(options) {
|
|
|
413
441
|
};
|
|
414
442
|
}
|
|
415
443
|
|
|
444
|
+
// The engine signals an HTTP error with exit code 22 and, under
|
|
445
|
+
// `--fail-with-body`, keeps whatever the output format produced. That is why the
|
|
446
|
+
// body is still here: the caller is told the fetch failed *and* gets to read what
|
|
447
|
+
// came back, which is what an error page is for.
|
|
448
|
+
//
|
|
449
|
+
// This branch is reached for binary dumps too -- a screenshot of an error page is
|
|
450
|
+
// still a screenshot -- so it must not decode stdout as text.
|
|
451
|
+
if (outcome.status === ENGINE_HTTP_ERROR_STATUS && bytes > 0) {
|
|
452
|
+
// The status is in the stderr reason. A JSON dump also carries it in the body,
|
|
453
|
+
// which is the fallback when the reason wording changes.
|
|
454
|
+
const reported = httpStatusFrom(stderr);
|
|
455
|
+
if (isBinary) {
|
|
456
|
+
const failure = ensureWritable(args.writeTo);
|
|
457
|
+
if (failure !== undefined) {
|
|
458
|
+
return { ok: false, url: args.url, httpError: true, error: failure, hint: 'Pass an absolute path in an existing directory.' };
|
|
459
|
+
}
|
|
460
|
+
writeFileSync(args.writeTo, outcome.stdout);
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
url: args.url,
|
|
464
|
+
kind: 'binary',
|
|
465
|
+
dump: args.dump,
|
|
466
|
+
path: args.writeTo,
|
|
467
|
+
bytes,
|
|
468
|
+
httpError: true,
|
|
469
|
+
...(reported === undefined ? {} : { status: reported }),
|
|
470
|
+
error: stderr.trim() === '' ? 'the server answered with an HTTP error status' : stderr.trim(),
|
|
471
|
+
hint: `The image the server returned was written to ${String(args.writeTo)}. Drop \`failOnHttpError\` to treat this as ordinary output.`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const text = outcome.stdout.toString('utf8');
|
|
475
|
+
const status = reported
|
|
476
|
+
?? (args.dump === 'json' && text.trimStart().startsWith('{') ? parseStatus(text) : undefined);
|
|
477
|
+
return {
|
|
478
|
+
ok: false,
|
|
479
|
+
url: args.url,
|
|
480
|
+
httpError: true,
|
|
481
|
+
...(status === undefined ? {} : { status }),
|
|
482
|
+
error: stderr.trim() === ''
|
|
483
|
+
? `the server answered with an HTTP error status${status === undefined ? '' : ` (${String(status)})`}`
|
|
484
|
+
: stderr.trim(),
|
|
485
|
+
content: text,
|
|
486
|
+
chars: text.length,
|
|
487
|
+
hint: 'The request reached a server and it refused. The content above is the error page as the engine rendered it, '
|
|
488
|
+
+ 'so it is often worth reading. Drop `failOnHttpError` to treat this as ordinary content instead.',
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
416
492
|
if (outcome.status !== 0) {
|
|
417
493
|
const sandboxTls = isSandboxTlsFailure(stderr);
|
|
418
494
|
// A blocked host fails at connect time ("Failed to connect" / RST) or, when
|
|
@@ -426,16 +502,24 @@ export function runFetch(options) {
|
|
|
426
502
|
// through to the generic hint below used to tell the caller to re-check the
|
|
427
503
|
// installation with moli_probe, which cannot possibly help.
|
|
428
504
|
const evalFailure = args.eval !== undefined && /JavaScript evaluation failed/u.test(stderr);
|
|
505
|
+
// A binary dump that failed produced no file at all. Saying only "exited with
|
|
506
|
+
// status 1" leaves the caller to guess whether an image exists; the useful fact
|
|
507
|
+
// is that it does not, and that a text dump would have captured this page.
|
|
508
|
+
const binaryFailure = isBinary && httpStatusFrom(stderr) !== undefined;
|
|
429
509
|
const hint = evalFailure
|
|
430
510
|
? 'The eval expression threw. Check that the page has reached the state you expect (`wait` or `waitSelector`), that the selector matches an element, and that the expression is valid JavaScript. The engine error names the type and position.'
|
|
431
|
-
:
|
|
432
|
-
? 'The
|
|
433
|
-
:
|
|
434
|
-
? 'The
|
|
435
|
-
:
|
|
511
|
+
: binaryFailure
|
|
512
|
+
? 'The engine could not render this page, so no file was written. An HTTP error still produces a body for a text dump (markdown, html or json), so ask for one of those if you need to see what the server returned.'
|
|
513
|
+
: sandboxTls
|
|
514
|
+
? 'The DSH file sandbox blocks Schannel client credentials, so moli cannot do TLS at low integrity. Retry the same call with sandbox_permissions "danger-full-access" (with a justification), use an http:// URL, or pass proxy to reach a local forward proxy.'
|
|
515
|
+
: possiblyUnreachable
|
|
516
|
+
? 'The host did not answer in time. Reachability to some hosts varies on this network, so retrying may succeed; if a local forward proxy is available, setting the plugin config field `proxy` is the other option. A certificate problem would report a schannel error instead of a timeout.'
|
|
517
|
+
: 'Run moli_probe to confirm the install, then retry; use waitSelector or eval for a page that renders late.';
|
|
518
|
+
const failureStatus = httpStatusFrom(stderr);
|
|
436
519
|
return {
|
|
437
520
|
ok: false,
|
|
438
521
|
url: args.url,
|
|
522
|
+
...(failureStatus === undefined ? {} : { status: failureStatus, httpError: true }),
|
|
439
523
|
error: stderr === '' ? `moli exited with status ${String(outcome.status)} and no output` : stderr,
|
|
440
524
|
sandboxTlsBlock: sandboxTls,
|
|
441
525
|
unreachable,
|
|
@@ -450,6 +534,20 @@ export function runFetch(options) {
|
|
|
450
534
|
if (failure !== undefined) {
|
|
451
535
|
return { ok: false, url: args.url, error: failure, sandboxTlsBlock: false, hint: 'Pass an absolute path in an existing directory.' };
|
|
452
536
|
}
|
|
537
|
+
// A binary dump produces nothing for an HTTP error: the engine exits 1 before
|
|
538
|
+
// rendering anything, unlike the text dumps which exit 22 with the body kept.
|
|
539
|
+
// Nothing is written, so say that rather than leaving an empty file behind.
|
|
540
|
+
if (outcome.status !== 0) {
|
|
541
|
+
const status = httpStatusFrom(stderr);
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
url: args.url,
|
|
545
|
+
...(status === undefined ? {} : { status }),
|
|
546
|
+
httpError: outcome.status === ENGINE_HTTP_ERROR_STATUS || status !== undefined,
|
|
547
|
+
error: stderr.trim() === '' ? `moli exited with status ${String(outcome.status)} before producing ${String(args.dump)} output` : stderr.trim(),
|
|
548
|
+
hint: 'The engine could not render this page, so no file was written. A text dump (markdown, html or json) still captures an HTTP error body; binary output does not.',
|
|
549
|
+
};
|
|
550
|
+
}
|
|
453
551
|
writeFileSync(args.writeTo, outcome.stdout);
|
|
454
552
|
return {
|
|
455
553
|
ok: true,
|
|
@@ -532,6 +630,25 @@ export function runFetch(options) {
|
|
|
532
630
|
};
|
|
533
631
|
}
|
|
534
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Read the HTTP status out of the engine's stderr reason.
|
|
635
|
+
*
|
|
636
|
+
* The engine reports it there, in the form
|
|
637
|
+
* `Reason: final main document returned HTTP status 404 at ...`
|
|
638
|
+
* for `--fail` and `--fail-with-body`, whatever the dump format. That makes stderr
|
|
639
|
+
* the reliable source: a JSON dump also carries the status in its body, but a
|
|
640
|
+
* markdown or HTML dump does not, and this branch has to name the status for those
|
|
641
|
+
* too. Matching narrowly keeps an unrelated number in the text from being read as a
|
|
642
|
+
* status.
|
|
643
|
+
*
|
|
644
|
+
* @param stderr - the engine's stderr text.
|
|
645
|
+
* @returns the numeric status, or undefined when it is not reported.
|
|
646
|
+
*/
|
|
647
|
+
function httpStatusFrom(stderr) {
|
|
648
|
+
const match = /HTTP status (\d{3})\b/u.exec(String(stderr));
|
|
649
|
+
return match === null ? undefined : Number(match[1]);
|
|
650
|
+
}
|
|
651
|
+
|
|
535
652
|
/**
|
|
536
653
|
* Read the HTTP status out of a JSON dump without trusting its shape.
|
|
537
654
|
*
|
package/package.json
CHANGED