Skip to content

Commit f00210c

Browse files
committed
Improve lookup hints for private-address validation failures
Detect private/localhost URL validation failures and show actionable hints instead of always suggesting authorized fetch. - In lookup/traverse fetch failures, suggest -p/--allow-private-address when the failure is a private-address UrlError. - In recurse step failures, explain that recursive fetches always block private/localhost targets and suggest best-effort suppression or explicit non-recursive fetches. - Keep existing -a/--authorized-fetch guidance for non-UrlError cases. Also adds unit tests for the hint classification helper. #608 (comment) #608 (comment) #608 (comment)
1 parent aab372f commit f00210c

2 files changed

Lines changed: 77 additions & 17 deletions

File tree

packages/cli/src/lookup.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Activity, Note } from "@fedify/vocab";
22
import { clearActiveConfig, setActiveConfig } from "@optique/config";
33
import { runWithConfig } from "@optique/config/run";
44
import { parse } from "@optique/core/parser";
5+
import { UrlError } from "@fedify/vocab-runtime";
56
import assert from "node:assert/strict";
67
import { Buffer } from "node:buffer";
78
import { createWriteStream } from "node:fs";
@@ -16,6 +17,7 @@ import {
1617
clearTimeoutSignal,
1718
collectRecursiveObjects,
1819
createTimeoutSignal,
20+
getLookupFailureHint,
1921
getRecursiveTargetId,
2022
lookupCommand,
2123
RecursiveLookupError,
@@ -618,6 +620,29 @@ test("getRecursiveTargetId - returns null for unknown recurse property", () => {
618620
);
619621
});
620622

623+
test("getLookupFailureHint - suggests private-address for UrlError", () => {
624+
assert.equal(
625+
getLookupFailureHint(new UrlError("Localhost is not allowed")),
626+
"private-address",
627+
);
628+
});
629+
630+
test("getLookupFailureHint - suggests recursive-private-address in recurse mode", () => {
631+
assert.equal(
632+
getLookupFailureHint(new UrlError("Invalid or private address"), {
633+
recursive: true,
634+
}),
635+
"recursive-private-address",
636+
);
637+
});
638+
639+
test("getLookupFailureHint - suggests authorized-fetch for non-URL errors", () => {
640+
assert.equal(
641+
getLookupFailureHint(new Error("401 Unauthorized")),
642+
"authorized-fetch",
643+
);
644+
});
645+
621646
test("collectRecursiveObjects - follows chain up to depth limit", async () => {
622647
const note1 = new Note({
623648
id: new URL("https://example.com/notes/1"),

packages/cli/src/lookup.ts

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Object as APObject,
1313
traverseCollection,
1414
} from "@fedify/vocab";
15-
import type { DocumentLoader } from "@fedify/vocab-runtime";
15+
import { type DocumentLoader, UrlError } from "@fedify/vocab-runtime";
1616
import type { ResourceDescriptor } from "@fedify/webfinger";
1717
import { getLogger } from "@logtape/logtape";
1818
import { bindConfig } from "@optique/config";
@@ -491,6 +491,53 @@ function handleTimeoutError(
491491
);
492492
}
493493

494+
function isPrivateAddressError(error: unknown): boolean {
495+
if (error instanceof UrlError) return true;
496+
const errorMessage = error instanceof Error ? error.message : String(error);
497+
const lowerMessage = errorMessage.toLowerCase();
498+
return (
499+
lowerMessage.includes("private address") ||
500+
lowerMessage.includes("private ip") ||
501+
lowerMessage.includes("localhost") ||
502+
lowerMessage.includes("loopback")
503+
);
504+
}
505+
506+
export function getLookupFailureHint(
507+
error: unknown,
508+
options: { recursive?: boolean } = {},
509+
): "private-address" | "recursive-private-address" | "authorized-fetch" {
510+
if (isPrivateAddressError(error)) {
511+
return options.recursive ? "recursive-private-address" : "private-address";
512+
}
513+
return "authorized-fetch";
514+
}
515+
516+
function printLookupFailureHint(
517+
authLoader: DocumentLoader | undefined,
518+
error: unknown,
519+
options: { recursive?: boolean } = {},
520+
): void {
521+
if (authLoader != null) return;
522+
switch (getLookupFailureHint(error, options)) {
523+
case "private-address":
524+
printError(
525+
message`The URL appears to be private or localhost. Try with -p/--allow-private-address.`,
526+
);
527+
return;
528+
case "recursive-private-address":
529+
printError(
530+
message`Recursive fetches do not allow private/localhost URLs. Use -S/--suppress-errors to skip blocked steps, or fetch those targets explicitly without --recurse.`,
531+
);
532+
return;
533+
case "authorized-fetch":
534+
printError(
535+
message`It may be a private object. Try with -a/--authorized-fetch.`,
536+
);
537+
return;
538+
}
539+
}
540+
494541
/**
495542
* Gets the next recursion target URL from an ActivityPub object.
496543
*/
@@ -796,11 +843,7 @@ export async function runLookup(
796843
handleTimeoutError(spinner, command.timeout, url);
797844
} else {
798845
spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
799-
if (authLoader == null) {
800-
printError(
801-
message`It may be a private object. Try with -a/--authorized-fetch.`,
802-
);
803-
}
846+
printLookupFailureHint(authLoader, error);
804847
}
805848
await finalizeAndExit(1);
806849
return;
@@ -874,9 +917,7 @@ export async function runLookup(
874917
} else {
875918
spinner.fail("Failed to recursively fetch object.");
876919
if (authLoader == null) {
877-
printError(
878-
message`It may be a private object. Try with -a/--authorized-fetch.`,
879-
);
920+
printLookupFailureHint(authLoader, error, { recursive: true });
880921
} else {
881922
printError(
882923
message`Use the -S/--suppress-errors option to suppress partial errors.`,
@@ -936,11 +977,7 @@ export async function runLookup(
936977
handleTimeoutError(spinner, command.timeout, url);
937978
} else {
938979
spinner.fail(`Failed to fetch object: ${colors.red(url)}.`);
939-
if (authLoader == null) {
940-
printError(
941-
message`It may be a private object. Try with -a/--authorized-fetch.`,
942-
);
943-
}
980+
printLookupFailureHint(authLoader, error);
944981
}
945982
await finalizeAndExit(1);
946983
return;
@@ -1009,9 +1046,7 @@ export async function runLookup(
10091046
`Failed to complete the traversal for: ${colors.red(url)}.`,
10101047
);
10111048
if (authLoader == null) {
1012-
printError(
1013-
message`It may be a private object. Try with -a/--authorized-fetch.`,
1014-
);
1049+
printLookupFailureHint(authLoader, error);
10151050
} else {
10161051
printError(
10171052
message`Use the -S/--suppress-errors option to suppress partial errors.`,

0 commit comments

Comments
 (0)