mastra-ai/mastra · error · Error

Failed to upload artifact: ${uploadResp.statusText}

Error message

Failed to upload artifact: ${uploadResp.statusText}

What it means

After creating the deploy record, uploadToEnvironment PUTs/POSTs the zip artifact to the returned uploadUrl. A non-ok response here throws with only the HTTP statusText (this endpoint is a raw upload sink that typically returns no JSON body). It means the storage upload itself failed — network, credentials, or an expired presigned URL.

Source

Thrown at packages/cli/src/commands/deploy/index.ts:380

  if (!createResp.ok) {
    const err = await createResp.json().catch(() => ({}));
    throw new Error(`Failed to create deploy: ${(err as { detail?: string }).detail || createResp.statusText}`);
  }

  const { deploy } = (await createResp.json()) as { deploy: { id: string; uploadUrl: string } };

  // Upload artifact
  const uploadResp = await fetch(deploy.uploadUrl, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/zip',
    },
    body: zipBuffer,
  });

  if (!uploadResp.ok) {
    throw new Error(`Failed to upload artifact: ${uploadResp.statusText}`);
  }

  // Signal upload complete — uses net-new env-scoped endpoint so the
  // unified-runtime CLI never touches /v1/studio/*.
  const completeResp = await fetch(
    `${apiUrl}/v1/projects/${projectId}/environments/${environmentId}/deploys/${deploy.id}/upload-complete`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'x-organization-id': orgId,
      },
    },
  );

  if (!completeResp.ok) {
    const err = await completeResp.json().catch(() => ({}));
    throw new Error(`Failed to complete upload: ${(err as { detail?: string }).detail || completeResp.statusText}`);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Retry the deploy — presigned-URL or transient storage failures usually clear on retry
  2. Reduce artifact size (exclude node_modules/dev assets from the zip) if statusText suggests a size limit (413)
  3. Deploy again promptly after deploy creation so the signed upload URL doesn't expire
  4. Check network/proxy egress to the storage host from your CI environment

Example fix

// before
// zip includes node_modules -> 413 Payload Too Large
// after
// build step: zip only dist/ + package.json, then retry deploy
Defensive patterns

Strategy: retry

Validate before calling

// Check artifact size before upload (guard against 413)
const MAX_ZIP_BYTES = 100 * 1024 * 1024;
if (zipBuffer.byteLength > MAX_ZIP_BYTES) {
  throw new Error('Artifact ' + zipBuffer.byteLength + ' bytes exceeds upload limit');
}

Type guard

function isUploadFailure(err: unknown): err is Error {
  return err instanceof Error && err.message.startsWith('Failed to upload artifact:');
}

Try / catch

try {
  const deploy = await uploadToEnvironment(args);
} catch (err) {
  if (isUploadFailure(err) && isRetryableStatus(err.message)) {
    await uploadToEnvironment(args); // one retry for transient storage/network errors
  } else throw err;
}

Prevention

When it happens

Trigger: The artifact upload fetch to deploy.uploadUrl returns a non-2xx status — e.g., 403 on an expired signed URL, 413 for an oversized zip, 5xx from storage, or an interrupted/unreachable connection reported via statusText.

Common situations: Slow CI networks or large bundles causing storage timeouts; zip exceeding upload size limits; long delay between deploy creation and upload letting the presigned URL lapse; proxy/firewall blocking the storage host.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/f9e8894d40dfcf24. Report an issue: GitHub.