6. Reaching the outside
preview calls api.microlink.io — functions have no network by default; `network:` is the whole allowlist.
Functions run in a sandbox with no network by default — fetch to anywhere is refused before a packet leaves. A function that needs the outside declares its hosts in function.yaml, and every request exits through the egress proxy that enforces the list.
preview
Pasting a link into the new-asset form is a chore; preview fetches the page’s title and description so the form can prefill itself:
examples/config/shelf/main/_functions/preview/function.yaml
# Fetches a link's title and description for the new-asset form. This is the
# first function that reaches outside: `network` is the whole allowlist —
# api.microlink.io answers, every other host is refused at the egress. It
# touches no collection, so no `admin:` either.
invoke: "user != nil"
network: [api.microlink.io]Two things to notice:
network: [api.microlink.io]is the whole allowlist — a fetch to any other host is refused at the egress, whatever the code says (Outbound network).- No
admin:— the function touches no collection, so it gets no database access at all. Declare only what the function needs.
examples/config/shelf/main/_functions/preview/index.ts
// preview: given a URL, ask api.microlink.io for the page's title and
// description. `network: [api.microlink.io]` is the whole allowlist — fetch
// anywhere else and the egress refuses the connection; that's the sandbox
// doing the work, not the code.
import type { Context } from "../lib/types.ts";
export default async function handler(ctx: Context) {
const { url } = ctx.input as { url: string };
const res = await fetch(`https://api.microlink.io?url=${encodeURIComponent(url)}`, {
headers: { accept: "application/json" },
});
if (!res.ok) {
// ctx.error is the caller's answer, so only 4xx exists here; 424 says
// "the dependency failed", not "the function crashed".
throw ctx.error(424, "preview_failed", `the preview service answered ${res.status}`);
}
const payload = await res.json();
if (payload.status !== "success") {
throw ctx.error(424, "preview_failed", payload.error?.message ?? "no metadata for that page");
}
const { title, description } = payload.data ?? {};
return { title: title ?? null, description: description ?? null };
}The egress does the sandboxing, so the handler is ordinary fetch. ctx.error answers the caller, so it only takes 4xx statuses — the function’s own crash is 500 function_error, never yours to emit. 424 preview_failed says “the dependency failed”; callers shouldn’t see transport errors.
Testing without a network
network: lives outside the function, so a unit test can’t observe it — what it can do is replace globalThis.fetch and check the code around the call:
examples/config/shelf/main/_functions/preview/index.test.ts
// Run with `make functions-testing-test` (Deno; no MongoDB, backd or network).
//
// `network:` is enforced at the egress, outside the function — a unit test
// can't see it. What the test can do is replace globalThis.fetch and check
// the code that surrounds the call.
import { createContext, FunctionError } from "../../../../../../clients/functions-testing/src/index.js";
import handler from "./index.ts";
async function withFetch(fake: (url: string) => Promise<Response>, run: () => Promise<void>) {
const real = globalThis.fetch;
globalThis.fetch = fake as typeof fetch;
try {
await run();
} finally {
globalThis.fetch = real;
}
}
Deno.test("returns title and description, sending the url to the allowlisted host", async () => {
let asked = "";
await withFetch(async (url) => {
asked = url;
return new Response(JSON.stringify({ status: "success", data: { title: "backd docs", description: "the manual" } }), { status: 200 });
}, async () => {
const { ctx } = createContext({ input: { url: "https://backd.example/docs" } });
const out = (await handler(ctx as never)) as { title: string | null; description: string | null };
if (out.title !== "backd docs" || out.description !== "the manual") throw new Error(JSON.stringify(out));
});
if (!asked.startsWith("https://api.microlink.io?url=") || !asked.includes(encodeURIComponent("https://backd.example/docs"))) {
throw new Error(`fetched ${asked}`);
}
});
Deno.test("a failed fetch becomes 424 preview_failed (ctx.error is 4xx only)", async () => {
await withFetch(async () => new Response("down", { status: 503 }), async () => {
const { ctx } = createContext({ input: { url: "https://anything.example" } });
try {
await handler(ctx as never);
throw new Error("should have refused");
} catch (e) {
const err = e as FunctionError;
if (err.status !== 424 || err.code !== "preview_failed") throw e;
}
});
});Try it
Create the four files (the listings above; or fetch them) and rebuild:
fetch the chapter's files (optional — the listings above are the same files)
for f in main/_functions/preview/function.yaml main/_functions/preview/index.ts main/_functions/preview/input.schema.json main/_functions/preview/index.test.ts; do
mkdir -p "config/shelf/$(dirname "$f")"
curl -fsSL "https://fernandezvara.github.io/backd/tutorial/config/shelf/$f" -o "config/shelf/$f"
donedocker compose run --rm functions-build && docker compose restart backd
The tests run as in chapter 5 (docker run … deno test config/shelf/main/_functions/preview/).
In the app, the New asset URL field gains a Preview button: paste a link, click — title and notes fill from the page’s metadata. The client call is db.fn('preview', { url }), same as chapter 5.
You should see
deno testpasses the fetch-stub tests.- Preview on
https://fernandezvara.github.io/backd/fills title and notes. - A
previewcall for a page that fails answers424 preview_failed, not a stack trace. - See the allowlist work: change the
fetchURL inpreview/index.tsto another host (sayhttps://example.com/), rebuild, restart and call it: the answer is500 function_failed, anddocker compose logs backdshowsNotCapable: Requires net access to "example.com:443"— the function’s code can say anything, the allowlist infunction.yamldecides. Put it back and rebuild.
Next: chapter 7 — functions calling functions: notify becomes an internal: building block.