paperclipai/paperclip · warning · GitHubAttachmentUnavailableError

github_attachment_unsafe_redirect

github_attachment_unsafe_redirect

Error message

github_attachment_unsafe_redirect

What it means

prepareGitHubPublicAttachment downloads public GitHub attachment URLs with redirects handled manually (redirect: "manual"). When a response is a redirect (3xx in REDIRECT_STATUSES) and the Location header is missing or fails the SSRF-safe allowedRedirect check, or the redirect budget (4 hops) is exhausted, the service throws GitHubAttachmentUnavailableError with code "github_attachment_unsafe_redirect". This is thrown at chat-github-attachments.ts:736, inside the redirect loop.

Source

Thrown at server/src/services/chat-github-attachments.ts:736

        },
        {
          allowPrivateNetwork: false,
          connectTimeoutMs: 5000,
          responseTimeoutMs: DOWNLOAD_TIMEOUT_MS,
          error: () =>
            new GitHubAttachmentUnavailableError(
              "github_attachment_download_failed",
            ),
        },
      );
      if (REDIRECT_STATUSES.has(response.status)) {
        const target = allowedRedirect(
          response.headers.get("location") ?? "",
          url.href,
        );
        await response.body?.cancel();
        if (!target || redirects === 3)
          throw new GitHubAttachmentUnavailableError(
            "github_attachment_unsafe_redirect",
          );
        url = target;
        continue;
      }
      const rejectResponse = async (
        code: GitHubAttachmentUnavailableError["code"],
      ): Promise<never> => {
        await response.body?.cancel();
        throw new GitHubAttachmentUnavailableError(code);
      };
      if (
        response.status === 401 ||
        response.status === 403 ||
        response.status === 404
      ) {
        const commentRequest = githubAttachmentCommentRequest(attachment);
        if (!resolvedCanonicalComment && resolveComment && commentRequest) {

View on GitHub (pinned to 01ad858492)

Solutions

  1. Confirm the attachment URL is a current, public GitHub attachment URL that resolves directly or within 3 redirects
  2. Check that the redirect target host is publicly routable (no private IPs / localhost / internal domains) and permitted by allowedRedirect
  3. Regenerate or re-upload the attachment so it uses a fresh non-redirecting URL
  4. If behind a proxy, ensure it does not rewrite GitHub attachment URLs to other hosts

Example fix

// before: attachment URL redirects beyond allowed hosts/depth
const attachment = { url: "https://github.com/user-attachments/assets/expired-id" };
// after: use a resolvable public URL or re-fetch via the GitHub API asset URL
const attachment = { url: await getFreshPublicAttachmentUrl(repo, attachmentId) };
Defensive patterns

Strategy: try-catch

Validate before calling

const u = new URL(attachmentUrl); if (u.protocol !== "https:" || !/(^|\.)github\.com$|(^|\.)githubusercontent\.com$/.test(u.hostname)) reject(attachmentUrl);

Type guard

function isPublicHttpsUrl(u) { try { const p = new URL(u); return (p.protocol === "https:" && !/^\d+\.\d+\.\d+\.\d+$/.test(p.hostname) && p.hostname !== "localhost"); } catch { return false; } }

Try / catch

try { return await prepareGitHubPublicAttachment(attachment, signal); } catch (e) { if (e instanceof GitHubAttachmentUnavailableError && e.code === "github_attachment_unsafe_redirect") { markAttachmentUnavailable(attachment, e.code); return null; } throw e; }

Prevention

When it happens

Trigger: A 3xx response whose Location header is empty; a Location that points to a private-network or otherwise disallowed host (allowedRedirect returns null); or a chain requiring a 5th redirect (redirects === 3).

Common situations: GitHub media URLs (github.com/user-attachments/...) re-pointing to a host not on the allowed redirect list; expired media links redirecting to a login/error page on a different domain; misconfigured proxies injecting redirects to internal hosts; deeply chained CDN redirects exceeding 3 hops.

Understand the failure class

Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/1c2debed9393ebb6. Report an issue: GitHub.