🪣 fix: Proper Key Extraction from S3 URL (#11241)

*  feat: Enhance S3 URL handling and add comprehensive tests for CRUD operations

* 🔒 fix: Improve S3 URL key extraction with enhanced logging and additional test cases

* chore: removed some duplicate testcases and fixed incorrect apostrophes

* fix: Log error for malformed URLs

* test: Add additional test case for extracting keys from S3 URLs

* fix: Enhance S3 URL key extraction logic and improve error handling with additional test cases

* test: Add test case for stripping bucket from custom endpoint URLs with forcePathStyle enabled

* refactor: Update S3 path style handling and enhance environment configuration for S3-compatible services

* refactor: Remove S3_FORCE_PATH_STYLE dependency and streamline S3 URL key extraction logic

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Rene Heijdens 2026-02-21 21:07:16 +01:00 committed by GitHub
parent 59717f5f50
commit 5d2b7fa4d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 896 additions and 2 deletions

View file

@ -252,15 +252,63 @@ function extractKeyFromS3Url(fileUrlOrKey) {
try {
const url = new URL(fileUrlOrKey);
return url.pathname.substring(1);
const hostname = url.hostname;
const pathname = url.pathname.substring(1); // Remove leading slash
if (
hostname === 's3.amazonaws.com' ||
hostname.match(/^s3[-.][a-z0-9-]+\.amazonaws\.com$/) ||
(bucketName && pathname.startsWith(`${bucketName}/`))
) {
// Path-style: https://s3.amazonaws.com/bucket-name/key or custom endpoint (MinIO, R2, etc.)
// Strip the bucket name (first path segment)
const firstSlashIndex = pathname.indexOf('/');
if (firstSlashIndex > 0) {
const key = pathname.substring(firstSlashIndex + 1);
if (key === '') {
logger.warn(
`[extractKeyFromS3Url] Extracted key is empty after removing bucket name from URL: ${fileUrlOrKey}`,
);
} else {
logger.debug(
`[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`,
);
}
return key;
} else {
logger.warn(
`[extractKeyFromS3Url] Unable to extract key from path-style URL: ${fileUrlOrKey}`,
);
return '';
}
}
// Virtual-hosted-style or other: https://bucket-name.s3.amazonaws.com/key
// Just return the pathname without leading slash
logger.debug(`[extractKeyFromS3Url] fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${pathname}`);
return pathname;
} catch (error) {
if (fileUrlOrKey.startsWith('http://') || fileUrlOrKey.startsWith('https://')) {
logger.error(
`[extractKeyFromS3Url] Error parsing URL: ${fileUrlOrKey}, Error: ${error.message}`,
);
} else {
logger.debug(`[extractKeyFromS3Url] Non-URL input, using fallback: ${fileUrlOrKey}`);
}
const parts = fileUrlOrKey.split('/');
if (parts.length >= 3 && !fileUrlOrKey.startsWith('http') && !fileUrlOrKey.startsWith('/')) {
return fileUrlOrKey;
}
return fileUrlOrKey.startsWith('/') ? fileUrlOrKey.substring(1) : fileUrlOrKey;
const key = fileUrlOrKey.startsWith('/') ? fileUrlOrKey.substring(1) : fileUrlOrKey;
logger.debug(
`[extractKeyFromS3Url] FALLBACK. fileUrlOrKey: ${fileUrlOrKey}, Extracted key: ${key}`,
);
return key;
}
}
@ -482,4 +530,5 @@ module.exports = {
refreshS3Url,
needsRefresh,
getNewS3URL,
extractKeyFromS3Url,
};