| import { NextRequest } from "next/server"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const HF_HOST = "https://huggingface.co"; |
| const COOKIE_NAME = "hf_access_token"; |
| const ALLOWED_PREFIXES = ["datasets/", "buckets/"]; |
|
|
| export const runtime = "nodejs"; |
| export const dynamic = "force-dynamic"; |
|
|
| const FORWARD_REQUEST_HEADERS = [ |
| "range", |
| "if-modified-since", |
| "if-none-match", |
| "accept", |
| "accept-encoding", |
| ]; |
|
|
| const FORWARD_RESPONSE_HEADERS = [ |
| "content-type", |
| "content-length", |
| "content-range", |
| "accept-ranges", |
| "etag", |
| "last-modified", |
| "cache-control", |
| ]; |
|
|
| |
| |
| const UPSTREAM_TIMEOUT_MS = 30_000; |
|
|
| |
| |
| |
| function upstreamSignal(req: NextRequest): AbortSignal { |
| return AbortSignal.any([ |
| req.signal, |
| AbortSignal.timeout(UPSTREAM_TIMEOUT_MS), |
| ]); |
| } |
|
|
| |
| |
| |
| |
| function buildUpstreamHeaders(req: NextRequest): Headers { |
| const headers = new Headers(); |
| const token = req.cookies.get(COOKIE_NAME)?.value; |
| if (token) headers.set("authorization", `Bearer ${token}`); |
| for (const h of FORWARD_REQUEST_HEADERS) { |
| const v = req.headers.get(h); |
| if (v) headers.set(h, v); |
| } |
| return headers; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function resolveUpstreamUrl( |
| subPath: string, |
| searchParams: URLSearchParams, |
| ): URL | null { |
| let upstreamUrl: URL; |
| try { |
| upstreamUrl = new URL(`${HF_HOST}/${subPath}`); |
| } catch { |
| return null; |
| } |
|
|
| if (upstreamUrl.origin !== HF_HOST) return null; |
|
|
| const normalized = upstreamUrl.pathname.replace(/^\/+/, ""); |
| if (!ALLOWED_PREFIXES.some((p) => normalized.startsWith(p))) return null; |
|
|
| for (const [k, v] of searchParams) { |
| upstreamUrl.searchParams.set(k, v); |
| } |
| return upstreamUrl; |
| } |
|
|
| export async function GET( |
| req: NextRequest, |
| ctx: { params: Promise<{ path: string[] }> }, |
| ) { |
| const { path } = await ctx.params; |
| const upstreamUrl = resolveUpstreamUrl( |
| path.join("/"), |
| req.nextUrl.searchParams, |
| ); |
| if (!upstreamUrl) return new Response("Forbidden", { status: 403 }); |
|
|
| const headers = buildUpstreamHeaders(req); |
|
|
| let upstream: Response; |
| try { |
| upstream = await fetch(upstreamUrl, { |
| method: "GET", |
| headers, |
| redirect: "follow", |
| cache: "no-store", |
| signal: upstreamSignal(req), |
| }); |
| } catch (err) { |
| |
| |
| |
| |
| const isTimeout = err instanceof Error && err.name === "TimeoutError"; |
| console.error("[proxy] upstream fetch failed", err); |
| return new Response( |
| isTimeout |
| ? "Gateway timeout: upstream took too long" |
| : "Bad gateway: upstream fetch failed", |
| { status: isTimeout ? 504 : 502 }, |
| ); |
| } |
|
|
| const respHeaders = new Headers(); |
| for (const h of FORWARD_RESPONSE_HEADERS) { |
| const v = upstream.headers.get(h); |
| if (v) respHeaders.set(h, v); |
| } |
|
|
| return new Response(upstream.body, { |
| status: upstream.status, |
| statusText: upstream.statusText, |
| headers: respHeaders, |
| }); |
| } |
|
|
| export async function HEAD( |
| req: NextRequest, |
| ctx: { params: Promise<{ path: string[] }> }, |
| ) { |
| const { path } = await ctx.params; |
| const upstreamUrl = resolveUpstreamUrl( |
| path.join("/"), |
| req.nextUrl.searchParams, |
| ); |
| if (!upstreamUrl) return new Response(null, { status: 403 }); |
|
|
| const headers = buildUpstreamHeaders(req); |
|
|
| let upstream: Response; |
| try { |
| upstream = await fetch(upstreamUrl, { |
| method: "HEAD", |
| headers, |
| redirect: "follow", |
| cache: "no-store", |
| signal: upstreamSignal(req), |
| }); |
| } catch (err) { |
| const isTimeout = err instanceof Error && err.name === "TimeoutError"; |
| console.error("[proxy] upstream HEAD failed", err); |
| return new Response(null, { status: isTimeout ? 504 : 502 }); |
| } |
|
|
| const respHeaders = new Headers(); |
| for (const h of FORWARD_RESPONSE_HEADERS) { |
| const v = upstream.headers.get(h); |
| if (v) respHeaders.set(h, v); |
| } |
|
|
| return new Response(null, { |
| status: upstream.status, |
| statusText: upstream.statusText, |
| headers: respHeaders, |
| }); |
| } |
|
|