go-gitea/gitea · error

Invalid server response: ${response.status}

Error message

Invalid server response: ${response.status}

What it means

This error is thrown in Gitea's client-side archive download flow (onDownloadArchive). After the user clicks an archive link (ZIP/tar download on the code page or release list), the browser POSTs/el.href polls the server's archive-generation endpoint and expects an HTTP 2xx on every poll. Any non-ok status aborts the whole download with this message, where the number is the HTTP status code returned by the Gitea server.

Source

Thrown at web_src/js/features/repo-common.ts:21

import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {sleep} from '../utils.ts';
import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
import {createApp} from 'vue';
import {createTippy} from '../modules/tippy.ts';
import {localUserSettings} from '../modules/user-settings.ts';
import {registerGlobalInitFunc} from '../modules/observer.ts';

async function onDownloadArchive(e: Event) {
  e.preventDefault();
  // there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list
  const el = (e.target as HTMLElement).closest<HTMLAnchorElement>('a.archive-link[href]')!;
  const targetLoading = el.closest('.ui.dropdown') ?? el;
  targetLoading.classList.add('is-loading', 'loading-icon-2px');
  try {
    for (let tryCount = 0; ;tryCount++) {
      const response = await POST(el.href);
      if (!response.ok) throw new Error(`Invalid server response: ${response.status}`);

      const data = await response.json();
      if (data.complete) break;
      await sleep(Math.min((tryCount + 1) * 750, 2000));
    }
    window.location.assign(el.href); // the archive is ready, start real downloading
  } catch (e) {
    console.error(e);
    showErrorToast(`Failed to download the archive: ${errorMessage(e)}`, {duration: 2500});
  } finally {
    targetLoading.classList.remove('is-loading', 'loading-icon-2px');
  }
}

export function initRepoArchiveLinks() {
  queryElems(document, 'a.archive-link[href]', (el) => el.addEventListener('click', onDownloadArchive));
}

View on GitHub (pinned to 43ace7cc8a)

Solutions

  1. Check the number in the message: 404 => the ref no longer exists (update the page and retry); 500 => inspect the Gitea server log for the archive-generation failure; 502/504 => raise the reverse-proxy read timeout for archive endpoints
  2. Verify the repository can produce an archive at all: git archive works locally against the same ref, and the repo is not empty/broken
  3. Retry the download after a hard refresh so the link href and CSRF token are current
  4. If it is proxy-timeout related, generate the archive server-side with a longer timeout or download the repository as a git clone instead

Example fix

// before
const response = await POST(el.href);
if (!response.ok) throw new Error(`Invalid server response: ${response.status}`);

// after (surface the server's own error message when present)
const response = await POST(el.href);
if (!response.ok) {
  const data = await response.json().catch(() => null);
  throw new Error(`Invalid server response: ${response.status}${data?.errorMessage ? `: ${data.errorMessage}` : ''}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Only start the polling flow if the archive link is well-formed
const el = (e.target as HTMLElement).closest<HTMLAnchorElement>('a.archive-link[href]');
if (!el || !el.getAttribute('href')?.startsWith('/')) return; // let the browser handle it natively

Type guard

const isArchiveLink = (el: Element | null): el is HTMLAnchorElement =>
  el instanceof HTMLAnchorElement && !!el.getAttribute('href');

Try / catch

try {
  for (let tryCount = 0; ;tryCount++) {
    const response = await POST(el.href);
    if (!response.ok) throw new Error(`Invalid server response: ${response.status}`);
    const data = await response.json();
    if (data.complete) break;
    await sleep(Math.min((tryCount + 1) * 750, 2000));
  }
  window.location.assign(el.href);
} catch (e) {
  // already the shipped pattern: console.error + showErrorToast, finally removes loading state
  showErrorToast(`Failed to download the archive: ${errorMessage(e)}`, {duration: 2500});
} finally {
  targetLoading.classList.remove('is-loading', 'loading-icon-2px');
}

Prevention

When it happens

Trigger: POST(el.href) returns a non-2xx status while waiting for the server to finish building the archive: 500 when archive generation fails server-side (e.g., repository is empty, git archive unsupported for the ref, worker timeout), 404 when the ref/path in the link no longer exists, or 403 when the user lacks permission or the request is rejected (e.g., CSRF/session expiry).

Common situations: Downloading an archive for a commit/branch that was force-pushed away or deleted mid-poll; very large repositories where server-side generation times out; mirrored repos whose objects are incomplete; expired session while the polling loop (up to 2s sleeps) is still running; reverse proxy (nginx/traefik) returning 502/504 because the archive request exceeds its timeout.

Related errors


AI-assisted analysis of go-gitea/gitea@43ace7cc8a (2026-08-15). Data as JSON: /api/errors/994a0aa3c5325b4c. Report an issue: GitHub.