Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/name-unreachable-hosts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'seamless-cli': patch
---

Say what could not be reached when a scaffold's network read fails.

`seamless init` makes three remote reads: the template registry, the templates archive, and
the auth server's `.env.example`. Each of them handles a non-ok HTTP response with a message
naming the status and the URL, but a connection-level failure (offline, DNS, TLS, no route)
rejects with a bare `TypeError: fetch failed` that propagated untouched to the top-level
handler. The whole output was "Error: fetch failed", which named neither the host nor which
of the three reads had failed.

The three call sites now go through a shared helper that turns that rejection into a message
naming the URL, what the CLI wanted from it, and the network as the likely cause, in the
style `login` already uses for an unreachable instance. Non-ok responses keep the messages
they had, and the original error is preserved as the thrown error's `cause`.
8 changes: 4 additions & 4 deletions resources/coverage-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions src/core/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@ describe("fetchEnvExample", () => {
);
});

it("propagates a network failure", async () => {
it("names the URL and the purpose when the connection fails", async () => {
const cause = new TypeError("fetch failed");
vi.stubGlobal(
"fetch",
vi.fn(async () => {
throw new Error("network down");
throw cause;
}),
);

await expect(fetchEnvExample()).rejects.toThrow("network down");
await expect(fetchEnvExample()).rejects.toThrow(
`Could not reach https://raw.githubusercontent.com/fells-code/seamless-auth-api/${SEAMLESS_AUTH_API_VERSION}/.env.example to read the auth server's env.example. Check your network connection.`,
);
await expect(fetchEnvExample()).rejects.toMatchObject({ cause });
});
});
21 changes: 20 additions & 1 deletion src/core/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { SEAMLESS_AUTH_API_VERSION } from "./images.js";

/**
* A GET whose connection-level failure says what it could not reach.
*
* When `fetch` never gets a response at all (offline, DNS, TLS, no route) it rejects
* with a bare `TypeError: fetch failed`, which reaches the top-level handler as
* "Error: fetch failed" and names neither the host nor what the CLI wanted from it.
* A non-ok response is left to the caller, which knows what its status means.
*/
export async function fetchRemote(url: string, purpose: string): Promise<Response> {
try {
return await fetch(url);
} catch (err) {
throw new Error(
`Could not reach ${url} to ${purpose}. Check your network connection.`,
{ cause: err },
);
}
}

export async function fetchEnvExample(): Promise<string> {
const url =
`https://raw.githubusercontent.com/fells-code/seamless-auth-api/${SEAMLESS_AUTH_API_VERSION}/.env.example`;

const res = await fetch(url);
const res = await fetchRemote(url, "read the auth server's env.example");

if (!res.ok) {
throw new Error("Failed to fetch auth env.example");
Expand Down
31 changes: 31 additions & 0 deletions src/core/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,18 @@ describe("openTemplateSource (remote)", () => {
);
});

it("names the registry URL when the connection fails", async () => {
mockFetchByUrl({
"registry.json": () => {
throw new TypeError("fetch failed");
},
});

await expect(openTemplateSource()).rejects.toThrow(
`Could not reach https://raw.githubusercontent.com/${SEAMLESS_TEMPLATES_REPO}/${SEAMLESS_TEMPLATES_REF}/registry.json to read the template registry. Check your network connection.`,
);
});

it("throws when the fetched registry is malformed", async () => {
mockFetchByUrl({
"registry.json": () => ({ ok: true, text: () => JSON.stringify({}) }),
Expand All @@ -271,6 +283,25 @@ describe("openTemplateSource (remote)", () => {
).rejects.toThrow(/Failed to download templates \(503\)/);
});

it("names the archive URL when the connection fails", async () => {
mockFetchByUrl({
"registry.json": () => ({
ok: true,
text: () => JSON.stringify(registryPayload),
}),
".zip": () => {
throw new TypeError("fetch failed");
},
});

const source = await openTemplateSource();
await expect(
source.readManifest(registryPayload.templates[0] as RegistryEntry),
).rejects.toThrow(
`Could not reach https://github.com/${SEAMLESS_TEMPLATES_REPO}/archive/${SEAMLESS_TEMPLATES_REF}.zip to download the project templates. Check your network connection.`,
);
});

it("throws when the downloaded archive has no entries", async () => {
mockFetchByUrl({
"registry.json": () => ({
Expand Down
5 changes: 3 additions & 2 deletions src/core/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AdmZip from "adm-zip";

import { VERSION } from "../index.js";
import { parseEnvString, writeEnv } from "./env.js";
import { fetchRemote } from "./fetch.js";
import { generateSecret } from "./secrets.js";
import { SEAMLESS_TEMPLATES_REF, SEAMLESS_TEMPLATES_REPO } from "./images.js";

Expand Down Expand Up @@ -140,7 +141,7 @@ function openLocalSource(dir: string): TemplateSource {

async function openRemoteSource(repo: string, ref: string): Promise<TemplateSource> {
const registryUrl = `https://raw.githubusercontent.com/${repo}/${ref}/registry.json`;
const res = await fetch(registryUrl);
const res = await fetchRemote(registryUrl, "read the template registry");
if (!res.ok) {
throw new Error(
`Failed to fetch the template registry (${res.status}) from ${registryUrl}.`,
Expand All @@ -154,7 +155,7 @@ async function openRemoteSource(repo: string, ref: string): Promise<TemplateSour
const ensureArchive = async () => {
if (archive) return archive;
const url = `https://github.com/${repo}/archive/${ref}.zip`;
const zipRes = await fetch(url);
const zipRes = await fetchRemote(url, "download the project templates");
if (!zipRes.ok) {
throw new Error(`Failed to download templates (${zipRes.status}) from ${url}.`);
}
Expand Down
Loading