fix(files): align upload route image extensions with picker (#4423)

* fix(files): align upload route image extensions with picker

* fix(files): widen image mime mapping for chat/profile/logo uploads

* fix(files): fall back to extension when MIME is generic for image-only uploads

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(files): add new image extensions to EXTENSION_TO_MIME map

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(files): skip non-Claude image MIMEs in createFileContent

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(files): resolve content type for image-only contexts when MIME is generic

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(files): cover image/jpg in Claude gate, reverse MIME map, audit metadata

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-05-03 03:32:17 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 14c93f6305
commit 0f09310f90
2 changed files with 51 additions and 10 deletions
+18 -10
View File
@@ -13,11 +13,12 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
import type { StorageContext } from '@/lib/uploads/config'
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import { isImageFileType } from '@/lib/uploads/utils/file-utils'
import { isImageFileType, resolveFileType } from '@/lib/uploads/utils/file-utils'
import {
SUPPORTED_AUDIO_EXTENSIONS,
SUPPORTED_CODE_EXTENSIONS,
SUPPORTED_DOCUMENT_EXTENSIONS,
SUPPORTED_IMAGE_EXTENSIONS,
SUPPORTED_VIDEO_EXTENSIONS,
validateFileType,
} from '@/lib/uploads/utils/validation'
@@ -28,12 +29,10 @@ import {
InvalidRequestError,
} from '@/app/api/files/utils'
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'] as const
const ALLOWED_EXTENSIONS = new Set<string>([
...SUPPORTED_DOCUMENT_EXTENSIONS,
...SUPPORTED_CODE_EXTENSIONS,
...IMAGE_EXTENSIONS,
...SUPPORTED_IMAGE_EXTENSIONS,
...SUPPORTED_AUDIO_EXTENSIONS,
...SUPPORTED_VIDEO_EXTENSIONS,
])
@@ -305,10 +304,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
context === 'profile-pictures' ||
context === 'workspace-logos'
) {
if (context !== 'copilot' && !isImageFileType(file.type)) {
throw new InvalidRequestError(
`Only image files (JPEG, PNG, GIF, WebP, SVG) are allowed for ${context} uploads`
if (context !== 'copilot') {
const mimeType = file.type
const isGenericMime = !mimeType || mimeType === 'application/octet-stream'
const extension = originalName.split('.').pop()?.toLowerCase() ?? ''
const extensionIsImage = (SUPPORTED_IMAGE_EXTENSIONS as readonly string[]).includes(
extension
)
const isImage = isGenericMime ? extensionIsImage : isImageFileType(mimeType)
if (!isImage) {
throw new InvalidRequestError(`Only image files are allowed for ${context} uploads`)
}
}
if (context === 'workspace-logos') {
@@ -344,6 +350,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
logger.info(`Uploading ${context} file: ${originalName}`)
const resolvedContentType = resolveFileType({ type: file.type, name: originalName })
const timestamp = Date.now()
const safeFileName = sanitizeFileName(originalName)
const storageKey = `${context}/${timestamp}-${safeFileName}`
@@ -362,7 +370,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const fileInfo = await storageService.uploadFile({
file: buffer,
fileName: storageKey,
contentType: file.type,
contentType: resolvedContentType,
context,
preserveKey: true,
customKey: storageKey,
@@ -379,7 +387,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
key: fileInfo.key,
name: originalName,
size: buffer.length,
type: file.type,
type: resolvedContentType,
},
directUploadSupported: false,
}
@@ -400,7 +408,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
fileName: originalName,
fileKey: fileInfo.key,
fileSize: buffer.length,
fileType: file.type,
fileType: resolvedContentType,
},
request,
})
+33
View File
@@ -33,6 +33,13 @@ export const MIME_TYPE_MAPPING: Record<string, 'image' | 'document' | 'audio' |
'image/gif': 'image',
'image/webp': 'image',
'image/svg+xml': 'image', // SVG upload is allowed; createFileContent handles it separately for Claude API
'image/bmp': 'image',
'image/tiff': 'image',
'image/heic': 'image',
'image/heif': 'image',
'image/avif': 'image',
'image/x-icon': 'image',
'image/vnd.microsoft.icon': 'image',
// Documents
'application/pdf': 'document',
@@ -158,6 +165,10 @@ export function createFileContent(fileBuffer: Buffer, mimeType: string): Message
return null
}
if (contentType === 'image' && !CLAUDE_SUPPORTED_IMAGE_MIME_TYPES.has(mimeType.toLowerCase())) {
return null
}
return {
type: contentType,
source: {
@@ -168,6 +179,14 @@ export function createFileContent(fileBuffer: Buffer, mimeType: string): Message
}
}
const CLAUDE_SUPPORTED_IMAGE_MIME_TYPES = new Set([
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp',
])
/**
* Extract file extension from filename
*/
@@ -184,6 +203,13 @@ const EXTENSION_TO_MIME: Record<string, string> = {
gif: 'image/gif',
webp: 'image/webp',
svg: 'image/svg+xml',
bmp: 'image/bmp',
tif: 'image/tiff',
tiff: 'image/tiff',
heic: 'image/heic',
heif: 'image/heif',
avif: 'image/avif',
ico: 'image/x-icon',
// Documents
pdf: 'application/pdf',
@@ -339,6 +365,13 @@ const MIME_TO_EXTENSION: Record<string, string> = {
'image/gif': 'gif',
'image/webp': 'webp',
'image/svg+xml': 'svg',
'image/bmp': 'bmp',
'image/tiff': 'tiff',
'image/heic': 'heic',
'image/heif': 'heif',
'image/avif': 'avif',
'image/x-icon': 'ico',
'image/vnd.microsoft.icon': 'ico',
// Documents
'application/pdf': 'pdf',