From 81c44f0c71ee46f1cee9c01932981ccc419a94b7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 14:53:29 +0000 Subject: [PATCH] feat: add support for Hugging Face API operations This change enables full support for Hugging Face API requests, including repository creation, by: 1. Identifying HF API requests via path patterns. 2. Allowing POST, PUT, PATCH, and DELETE methods for these requests. 3. Correctly forwarding request bodies and headers. 4. Skipping caching for API operations. Fixes: huggingface_hub.errors.HfHubHTTPError: Client error '405 Method Not Allowed' --- src/index.js | 27 ++++++++++------ src/protocols/huggingface.js | 63 ++++++++++++++++++++++++++++++++++++ src/utils/validation.js | 10 +++--- 3 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 src/protocols/huggingface.js diff --git a/src/index.js b/src/index.js index 5e4a1a5..8dec3e1 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,7 @@ import { CONFIG, createConfig } from './config/index.js'; import { SORTED_PLATFORMS, transformPath } from './config/platforms.js'; import { configureAIHeaders, isAIInferenceRequest } from './protocols/ai.js'; +import { configureHuggingFaceHeaders, isHuggingFaceAPIRequest } from './protocols/huggingface.js'; import { fetchToken, getScopeFromUrl, @@ -126,7 +127,10 @@ async function handleRequest(request, env, ctx) { // Check if this is an AI inference request const isAI = isAIInferenceRequest(request, url); - // Check cache first (skip cache for Git, Git LFS, Docker, and AI inference operations) + // Check if this is a Hugging Face API request + const isHF = isHuggingFaceAPIRequest(request, url); + + // Check cache first (skip cache for Git, Git LFS, Docker, AI inference, and HF API operations) /** @type {Cache | null} */ // @ts-ignore - Cloudflare Workers cache API const cache = @@ -134,7 +138,7 @@ async function handleRequest(request, env, ctx) { ? /** @type {any} */ (caches).default // eslint-disable-line jsdoc/reject-any-type : null; - if (cache && !isGit && !isGitLFS && !isDocker && !isAI) { + if (cache && !isGit && !isGitLFS && !isDocker && !isAI && !isHF) { try { // For Range requests, try cache match first const cacheKey = new Request(targetUrl, { @@ -177,10 +181,10 @@ async function handleRequest(request, env, ctx) { redirect: 'follow' }; - // Add body for POST/PUT/PATCH requests (Git/Docker/AI inference operations) + // Add body for POST/PUT/PATCH/DELETE requests (Git/Docker/AI/HF operations) if ( - ['POST', 'PUT', 'PATCH'].includes(request.method) && - (isGit || isGitLFS || isDocker || isAI) + ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) && + (isGit || isGitLFS || isDocker || isAI || isHF) ) { fetchOptions.body = request.body; } @@ -188,9 +192,9 @@ async function handleRequest(request, env, ctx) { // Cast headers to Headers for proper typing const requestHeaders = /** @type {Headers} */ (fetchOptions.headers); - // Set appropriate headers for Git/Docker/AI vs regular requests - if (isGit || isGitLFS || isDocker || isAI) { - // For Git/Docker/AI operations, copy all headers from the original request + // Set appropriate headers for Git/Docker/AI/HF vs regular requests + if (isGit || isGitLFS || isDocker || isAI || isHF) { + // For Git/Docker/AI/HF operations, copy all headers from the original request // This ensures protocol compliance for (const [key, value] of request.headers.entries()) { // Skip headers that might cause issues with proxying @@ -209,6 +213,10 @@ async function handleRequest(request, env, ctx) { if (isAI) { configureAIHeaders(requestHeaders, request); } + + if (isHF) { + configureHuggingFaceHeaders(requestHeaders, request); + } } else { // Regular file download headers Object.assign(fetchOptions, { @@ -617,8 +625,9 @@ async function handleRequest(request, env, ctx) { const isDocker = isDockerRequest(request, new URL(request.url)); const isAI = isAIInferenceRequest(request, new URL(request.url)); const isGitLFS = isGitLFSRequest(request, new URL(request.url)); + const isHF = isHuggingFaceAPIRequest(request, new URL(request.url)); - return isGit || isGitLFS || isDocker || isAI + return isGit || isGitLFS || isDocker || isAI || isHF ? response : addPerformanceHeaders(response, monitor); } diff --git a/src/protocols/huggingface.js b/src/protocols/huggingface.js new file mode 100644 index 0000000..078fbf1 --- /dev/null +++ b/src/protocols/huggingface.js @@ -0,0 +1,63 @@ +/** + * Xget - High-performance acceleration engine for developer resources + * Copyright (C) 2025 Xi Xu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * Hugging Face protocol handler for Xget + */ + +/** + * Detects if a request is a Hugging Face API operation. + * + * Identifies Hugging Face API requests by checking for: + * - Hugging Face platform prefix (/hf/) + * - API path segment (/api/) + * + * @param {Request} request - The incoming request object + * @param {URL} url - Parsed URL object + * @returns {boolean} True if this is a Hugging Face API operation + */ +export function isHuggingFaceAPIRequest(request, url) { + // Check for Hugging Face API endpoints + if (url.pathname.startsWith('/hf/api/')) { + return true; + } + + // Also check for token endpoint which is often used + if (url.pathname.startsWith('/hf/token')) { + return true; + } + + return false; +} + +/** + * Configures headers for Hugging Face API requests. + * + * @param {Headers} headers - The headers object to modify + * @param {Request} request - The original request + */ +export function configureHuggingFaceHeaders(headers, request) { + // Pass through Authorization header if present + if (request.headers.has('Authorization')) { + headers.set('Authorization', request.headers.get('Authorization')); + } + + if (request.method === 'POST' && !headers.has('Content-Type')) { + headers.set('Content-Type', 'application/json'); + } +} diff --git a/src/utils/validation.js b/src/utils/validation.js index 13745ab..3932721 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -25,6 +25,7 @@ import { CONFIG } from '../config/index.js'; // Imported protocol checks import { isAIInferenceRequest } from '../protocols/ai.js'; import { isGitLFSRequest, isGitRequest } from '../protocols/git.js'; +import { isHuggingFaceAPIRequest } from '../protocols/huggingface.js'; /** * Detects if a request is a container registry operation (Docker/OCI). @@ -72,7 +73,7 @@ export function isDockerRequest(request, url) { } // Re-export for standard usage -export { isAIInferenceRequest, isGitLFSRequest, isGitRequest }; +export { isAIInferenceRequest, isGitLFSRequest, isGitRequest, isHuggingFaceAPIRequest }; /** * Validates incoming requests against security rules. @@ -90,15 +91,16 @@ export { isAIInferenceRequest, isGitLFSRequest, isGitRequest }; * @returns {{valid: boolean, error?: string, status?: number}} Validation result object */ export function validateRequest(request, url, config = CONFIG) { - // Allow POST method for Git, Git LFS, Docker, and AI inference operations + // Allow POST method for Git, Git LFS, Docker, AI inference, and HF API operations const isGit = isGitRequest(request, url); const isGitLFS = isGitLFSRequest(request, url); const isDocker = isDockerRequest(request, url); const isAI = isAIInferenceRequest(request, url); + const isHF = isHuggingFaceAPIRequest(request, url); const allowedMethods = - isGit || isGitLFS || isDocker || isAI - ? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH'] + isGit || isGitLFS || isDocker || isAI || isHF + ? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'] : config.SECURITY.ALLOWED_METHODS; if (!allowedMethods.includes(request.method)) {