From daa8f0ea6b5e998c125e062cb4e96e592e0eefaa Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 9 Apr 2026 17:43:53 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=82=20fix:=20Respect=20`supportedMimeT?= =?UTF-8?q?ypes`=20Config=20in=20File=20Picker=20Accept=20Filter=20(#12596?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: respect supportedMimeTypes config in file picker accept filter The browser file picker's accept attribute was hardcoded by provider identity, ignoring the endpoint's supportedMimeTypes from fileConfig. Users who configured permissive MIME types (e.g., '.*') still saw a restrictive filter in the upload dialog. Add isPermissiveMimeConfig utility that detects wildcard patterns in the endpoint's supportedMimeTypes. When permissive, the file picker accept attribute is set to empty (unrestricted). Non-permissive configs retain the existing provider-based defaults. Closes #12589 * fix: address review findings for isPermissiveMimeConfig - Use non-standard MIME namespace probe (x-librechat/x-probe) so category-wildcard patterns like ^application\/.*$ no longer false-positive as permissive - Add single-line JSDoc to isPermissiveMimeConfig - Use !== undefined instead of != null (fileType is never null) - Add endpointFileConfig to dropdownItems useMemo deps to prevent stale closure when config changes without endpoint change - Add tests for broad application and multi-category patterns * fix: wrap handleUploadClick in useCallback to satisfy exhaustive-deps handleUploadClick is captured inside the dropdownItems useMemo but was not in its dependency array. Wrap it in useCallback with endpointFileConfig.supportedMimeTypes as the sole dependency, then reference the stable callback in the useMemo deps. --- .../Chat/Input/Files/AttachFileMenu.tsx | 52 +++++++++------- .../data-provider/src/file-config.spec.ts | 61 +++++++++++++++++++ packages/data-provider/src/file-config.ts | 8 +++ 3 files changed, 100 insertions(+), 21 deletions(-) diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 181d219c08..0b10e6c712 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useMemo } from 'react'; +import React, { useRef, useState, useMemo, useCallback } from 'react'; import { useRecoilState } from 'recoil'; import * as Ariakit from '@ariakit/react'; import { @@ -19,6 +19,7 @@ import { Providers, EToolResources, EModelEndpoint, + isPermissiveMimeConfig, defaultAgentCapabilities, bedrockDocumentExtensions, isDocumentSupportedProvider, @@ -110,27 +111,35 @@ const AttachFileMenu = ({ ephemeralAgent, ); - const handleUploadClick = (fileType?: FileUploadType) => { - if (!inputRef.current) { - return; - } - inputRef.current.value = ''; - if (fileType === 'image') { - inputRef.current.accept = 'image/*,.heif,.heic'; - } else if (fileType === 'document') { - inputRef.current.accept = '.pdf,application/pdf'; - } else if (fileType === 'image_document') { - inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf'; - } else if (fileType === 'image_document_extended') { - inputRef.current.accept = `image/*,.heif,.heic,${bedrockDocumentExtensions}`; - } else if (fileType === 'image_document_video_audio') { - inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*'; - } else { + const handleUploadClick = useCallback( + (fileType?: FileUploadType) => { + if (!inputRef.current) { + return; + } + inputRef.current.value = ''; + if ( + fileType !== undefined && + isPermissiveMimeConfig(endpointFileConfig?.supportedMimeTypes) + ) { + inputRef.current.accept = ''; + } else if (fileType === 'image') { + inputRef.current.accept = 'image/*,.heif,.heic'; + } else if (fileType === 'document') { + inputRef.current.accept = '.pdf,application/pdf'; + } else if (fileType === 'image_document') { + inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf'; + } else if (fileType === 'image_document_extended') { + inputRef.current.accept = `image/*,.heif,.heic,${bedrockDocumentExtensions}`; + } else if (fileType === 'image_document_video_audio') { + inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*'; + } else { + inputRef.current.accept = ''; + } + inputRef.current.click(); inputRef.current.accept = ''; - } - inputRef.current.click(); - inputRef.current.accept = ''; - }; + }, + [endpointFileConfig?.supportedMimeTypes], + ); const dropdownItems = useMemo(() => { const createMenuItems = (onAction: (fileType?: FileUploadType) => void) => { @@ -247,6 +256,7 @@ const AttachFileMenu = ({ endpointType, capabilities, useResponsesApi, + handleUploadClick, setToolResource, setEphemeralAgent, sharePointEnabled, diff --git a/packages/data-provider/src/file-config.spec.ts b/packages/data-provider/src/file-config.spec.ts index 96f48621cc..ee894712ff 100644 --- a/packages/data-provider/src/file-config.spec.ts +++ b/packages/data-provider/src/file-config.spec.ts @@ -1,6 +1,8 @@ import type { FileConfig } from './types/files'; import { fileConfig as baseFileConfig, + isPermissiveMimeConfig, + convertStringsToRegex, documentParserMimeTypes, getEndpointFileConfig, applicationMimeTypes, @@ -1246,3 +1248,62 @@ describe('getEndpointFileConfig', () => { }); }); }); + +describe('isPermissiveMimeConfig', () => { + it('returns true for wildcard .* pattern', () => { + expect(isPermissiveMimeConfig([/.*/])).toBe(true); + }); + + it('returns true for .+ pattern', () => { + expect(isPermissiveMimeConfig([/.+/])).toBe(true); + }); + + it('returns true for anchored ^.*$ pattern', () => { + expect(isPermissiveMimeConfig([/^.*$/])).toBe(true); + }); + + it('returns true for anchored ^.+$ pattern', () => { + expect(isPermissiveMimeConfig([/^.+$/])).toBe(true); + }); + + it('returns true when at least one pattern is permissive', () => { + expect(isPermissiveMimeConfig([/^image\/.*$/, /.*/])).toBe(true); + }); + + it('returns false for image-only patterns', () => { + expect(isPermissiveMimeConfig([/^image\/(jpeg|png)$/])).toBe(false); + }); + + it('returns false for category patterns', () => { + expect(isPermissiveMimeConfig([/^image\/.*$/, /^text\/.*$/])).toBe(false); + }); + + it('returns false for specific MIME type patterns', () => { + expect(isPermissiveMimeConfig([/^application\/pdf$/])).toBe(false); + }); + + it('returns false for broad application category pattern', () => { + expect(isPermissiveMimeConfig([/^application\/.*$/])).toBe(false); + }); + + it('returns false for multi-category pattern', () => { + expect(isPermissiveMimeConfig([/^(application|text)\/.*$/])).toBe(false); + }); + + it('returns false for default supportedMimeTypes', () => { + expect(isPermissiveMimeConfig(supportedMimeTypes)).toBe(false); + }); + + it('returns false for undefined', () => { + expect(isPermissiveMimeConfig(undefined)).toBe(false); + }); + + it('returns false for empty array', () => { + expect(isPermissiveMimeConfig([])).toBe(false); + }); + + it('returns true for regex produced by convertStringsToRegex with .*', () => { + const converted = convertStringsToRegex(['.*']); + expect(isPermissiveMimeConfig(converted)).toBe(true); + }); +}); diff --git a/packages/data-provider/src/file-config.ts b/packages/data-provider/src/file-config.ts index 7ec184755d..a93daf79e2 100644 --- a/packages/data-provider/src/file-config.ts +++ b/packages/data-provider/src/file-config.ts @@ -497,6 +497,14 @@ export const convertStringsToRegex = (patterns: string[]): RegExp[] => return acc; }, []); +/** Detects whether the given MIME type patterns accept all file types (e.g., `.*` or `.+`). */ +export const isPermissiveMimeConfig = (types?: RegExp[]): boolean => { + if (!types || types.length === 0) { + return false; + } + return types.some((regex) => regex.test('x-librechat/x-probe')); +}; + /** * Gets the appropriate endpoint file configuration with standardized lookup logic. *