Skip to content

Commit 02e50b7

Browse files
committed
Respect Content-Signal: ai-input=no
This signal means the site insists we must not feed the data into an AI agent. Not sure why a site would do this but we need to follow the rules that Cloudflare is endorsing.
1 parent b7a46e5 commit 02e50b7

1 file changed

Lines changed: 27 additions & 0 deletions

File tree

packages/workshop-backend/src/web-fetch.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,19 @@ async function followRedirects(
279279
throw new Error(`Too many redirects (>${MAX_REDIRECTS})`);
280280
}
281281

282+
// Parse the Content-Signal response header (https://contentsignals.org/) and check whether
283+
// a specific signal is present and set to "no". The header is a comma-separated list of
284+
// key=value pairs, e.g. `ai-train=yes, search=yes, ai-input=no`.
285+
function contentSignalDenies(response: Response, signal: string): boolean {
286+
const header = response.headers.get("content-signal");
287+
if (!header) return false;
288+
for (const part of header.split(",")) {
289+
const [key, value] = part.split("=").map((s) => s.trim().toLowerCase());
290+
if (key === signal && value === "no") return true;
291+
}
292+
return false;
293+
}
294+
282295
// Format a `WebFetchResult` as a single string for the agent: a small YAML frontmatter
283296
// header followed by `---` then the body. This is friendlier to LLMs than a JSON-wrapped
284297
// object, since the body lives inline rather than as an escaped JSON string.
@@ -336,6 +349,20 @@ export async function webFetch(
336349
const finalUrl = response.url ? new URL(response.url) : postRedirectUrl;
337350
const contentType = response.headers.get("content-type") ?? "";
338351

352+
// Respect the Content-Signal header (https://contentsignals.org/). If the site
353+
// explicitly sets `ai-input=no`, we must not feed its content to the AI agent.
354+
if (contentSignalDenies(response, "ai-input")) {
355+
try {
356+
await response.body?.cancel();
357+
} catch {
358+
// Ignore.
359+
}
360+
throw new Error(
361+
`The site at ${finalUrl} sets Content-Signal: ai-input=no, indicating that ` +
362+
`it does not permit its content to be used as AI input.`,
363+
);
364+
}
365+
339366
const { bytes, truncated } = await readBodyCapped(response, maxBytes);
340367

341368
let body: string;

0 commit comments

Comments
 (0)