From 7d7806df30d83be6fd21778c8ab19e151f442749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Thu, 8 May 2025 18:57:50 +0200 Subject: [PATCH] Passthrough SVG images in image resizing (#3224) --- .changeset/two-lions-tickle.md | 6 ++++ .../src/lib/images/checkIsSizableImageURL.ts | 30 +++++++++++-------- .../src/lib/images/createImageResizer.ts | 6 ++-- .../src/lib/images/resizer/resizeImage.ts | 18 ++++++----- packages/gitbook/src/routes/image.ts | 3 +- 5 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 .changeset/two-lions-tickle.md diff --git a/.changeset/two-lions-tickle.md b/.changeset/two-lions-tickle.md new file mode 100644 index 000000000..b9fa67603 --- /dev/null +++ b/.changeset/two-lions-tickle.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": minor +"gitbook": minor +--- + +Pass SVG images through image resizing without resizing them to serve them from optimal host. diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts index 45754af50..61e3ebbc2 100644 --- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts @@ -1,4 +1,8 @@ -import { checkIsHttpURL } from '@/lib/urls'; +export enum SizableImageAction { + Resize = 'resize', + Skip = 'skip', + Passthrough = 'passthrough', +} /** * Check if an image URL is resizable. @@ -6,22 +10,24 @@ import { checkIsHttpURL } from '@/lib/urls'; * Skip it for SVGs. * Skip it for GitBook images (to avoid recursion). */ -export function checkIsSizableImageURL(input: string): boolean { +export function checkIsSizableImageURL(input: string): SizableImageAction { if (!URL.canParse(input)) { - return false; - } - - if (input.includes('/~gitbook/image')) { - return false; + return SizableImageAction.Skip; } const parsed = new URL(input); - if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) { - return false; + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return SizableImageAction.Skip; } - if (!checkIsHttpURL(parsed)) { - return false; + if (parsed.hostname === 'localhost') { + return SizableImageAction.Skip; + } + if (parsed.pathname.includes('/~gitbook/image')) { + return SizableImageAction.Skip; + } + if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) { + return SizableImageAction.Passthrough; } - return true; + return SizableImageAction.Resize; } diff --git a/packages/gitbook-v2/src/lib/images/createImageResizer.ts b/packages/gitbook-v2/src/lib/images/createImageResizer.ts index 703fdadd6..8507a7aef 100644 --- a/packages/gitbook-v2/src/lib/images/createImageResizer.ts +++ b/packages/gitbook-v2/src/lib/images/createImageResizer.ts @@ -1,7 +1,7 @@ import 'server-only'; import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env'; import type { GitBookLinker } from '../links'; -import { checkIsSizableImageURL } from './checkIsSizableImageURL'; +import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL'; import { getImageSize } from './resizer'; import { type SignatureVersion, generateImageSignature } from './signatures'; import type { ImageResizer } from './types'; @@ -24,7 +24,7 @@ export function createImageResizer({ return { getResizedImageURL: (urlInput) => { - if (!checkIsSizableImageURL(urlInput)) { + if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) { return null; } @@ -64,7 +64,7 @@ export function createImageResizer({ }, getImageSize: async (input, options) => { - if (!checkIsSizableImageURL(input)) { + if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) { return null; } diff --git a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts index 4ed81dcd2..8e656f3b7 100644 --- a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts +++ b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts @@ -1,7 +1,7 @@ import 'server-only'; import assertNever from 'assert-never'; import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env'; -import { checkIsSizableImageURL } from '../checkIsSizableImageURL'; +import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL'; import { resizeImageWithCDNCgi } from './cdn-cgi'; import { resizeImageWithCFFetch } from './cf-fetch'; import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types'; @@ -13,7 +13,7 @@ export async function getImageSize( input: string, defaultSize: Partial = {} ): Promise<{ width: number; height: number } | null> { - if (!checkIsSizableImageURL(input)) { + if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) { return null; } @@ -48,13 +48,17 @@ export async function resizeImage( signal?: AbortSignal; } ): Promise { - const parsed = new URL(input); - if (parsed.protocol === 'data:') { - throw new Error('Cannot resize data: URLs'); + const action = checkIsSizableImageURL(input); + if (action === SizableImageAction.Skip) { + throw new Error( + 'Cannot resize this image, this function should have never been called on this url' + ); } - if (parsed.hostname === 'localhost') { - throw new Error('Cannot resize localhost URLs'); + if (action === SizableImageAction.Passthrough) { + return fetch(input, { + signal: options.signal, + }); } switch (GITBOOK_IMAGE_RESIZE_MODE) { diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts index 45d590f31..3bc946ed9 100644 --- a/packages/gitbook/src/routes/image.ts +++ b/packages/gitbook/src/routes/image.ts @@ -2,6 +2,7 @@ import { CURRENT_SIGNATURE_VERSION, type CloudflareImageOptions, type SignatureVersion, + SizableImageAction, checkIsSizableImageURL, isSignatureVersion, parseImageAPIURL, @@ -40,7 +41,7 @@ export async function serveResizedImage( // Check again if the image can be sized, even though we checked when rendering the Image component // Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain // Also prevent infinite redirects. - if (!checkIsSizableImageURL(url)) { + if (checkIsSizableImageURL(url) === SizableImageAction.Skip) { return new Response('Invalid url parameter', { status: 400 }); }