diff --git a/lib/api/image-api.test.ts b/lib/api/image-api.test.ts index a9b97eb..e0512f9 100644 --- a/lib/api/image-api.test.ts +++ b/lib/api/image-api.test.ts @@ -19,6 +19,12 @@ vi.mock("@/lib/pollinations-api", () => ({ }, })) +vi.mock("@/lib/config/api.config", () => ({ + API_CONFIG: { + baseUrl: "https://gen.pollinations.ai", + }, +})) + const mockFetch = vi.fn() describe("image-api", () => { @@ -169,19 +175,35 @@ describe("image-api", () => { blob: async () => mockBlob, }) - const result = await downloadImage("https://example.com/img.png") + const pollinationsUrl = "https://gen.pollinations.ai/image/test.png" + const result = await downloadImage(pollinationsUrl) expect(result).toBe(mockBlob) expect(mockFetch).toHaveBeenCalledWith( - "https://example.com/img.png", + pollinationsUrl, expect.objectContaining({ headers: { "Authorization": "Bearer test-token" }, }) ) }) + it("skips auth headers for non-pollinations URLs", async () => { + const mockBlob = new Blob(["data"], { type: "image/png" }) + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + blob: async () => mockBlob, + }) + + const result = await downloadImage("https://example.com/img.png") + + expect(result).toBe(mockBlob) + expect(mockFetch).toHaveBeenCalledWith("https://example.com/img.png", undefined) + }) + it("falls back to no-header fetch on initial failure", async () => { const mockBlob = new Blob(["data"], { type: "image/png" }) + const pollinationsUrl = "https://gen.pollinations.ai/image/test.png" // First call fails (e.g. 403 Forbidden with headers) mockFetch.mockResolvedValueOnce({ @@ -196,7 +218,7 @@ describe("image-api", () => { blob: async () => mockBlob, }) - const result = await downloadImage("https://example.com/img.png") + const result = await downloadImage(pollinationsUrl) expect(result).toBe(mockBlob) // Verify both calls were made @@ -204,7 +226,7 @@ describe("image-api", () => { // First with headers expect(mockFetch).toHaveBeenNthCalledWith( 1, - "https://example.com/img.png", + pollinationsUrl, expect.objectContaining({ headers: { "Authorization": "Bearer test-token" }, }) @@ -212,7 +234,7 @@ describe("image-api", () => { // Second without headers (undefined or empty options) expect(mockFetch).toHaveBeenNthCalledWith( 2, - "https://example.com/img.png" + pollinationsUrl ) }) diff --git a/lib/api/image-api.ts b/lib/api/image-api.ts index a883afc..725b7e9 100644 --- a/lib/api/image-api.ts +++ b/lib/api/image-api.ts @@ -13,6 +13,7 @@ import { PollinationsApiError, isPollinationsApiError, } from "@/lib/errors" +import { API_CONFIG } from "@/lib/config/api.config" import { PollinationsAPI } from "@/lib/pollinations-api" import { GeneratedImageSchema, @@ -77,6 +78,16 @@ export async function generateImage( } } +/** + * Returns true when the URL points at the Pollinations API origin. + * R2/public URLs should skip auth headers to avoid CORS/preflight failures. + */ +function shouldUsePollinationsHeaders(imageUrl: string): boolean { + const { origin } = new URL(imageUrl) + const pollinationsOrigin = new URL(API_CONFIG.baseUrl).origin + return origin === pollinationsOrigin +} + /** * Downloads an image as a blob. * @@ -85,10 +96,15 @@ export async function generateImage( */ export async function downloadImage(imageUrl: string): Promise { try { - // Try with auth headers first - const response = await fetch(imageUrl, { - headers: PollinationsAPI.getHeaders(), - }) + const headers = shouldUsePollinationsHeaders(imageUrl) + ? PollinationsAPI.getHeaders() + : undefined + + // Only attach auth headers for Pollinations API URLs to avoid CORS errors on R2. + const response = await fetch( + imageUrl, + headers ? { headers } : undefined + ) if (response.ok) { return response.blob()