edulab / app.py
bagaseptian's picture
after debug
a406c08
import os
import httpx # Library HTTP client modern, async-friendly
import asyncio
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse, RedirectResponse, PlainTextResponse
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv # Untuk membaca .env jika perlu (opsional)
import logging
import time
# --- Konfigurasi Logging ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Konfigurasi Aplikasi ---
load_dotenv() # Muat variabel dari .env jika ada (untuk pengembangan lokal)
# URL ke file backend_url.txt RAW di GitHub
# Ganti YOUR_GITHUB_USERNAME dan YOUR_REPO_NAME
GITHUB_RAW_URL_FILE = os.getenv(
"GITHUB_RAW_URL_FILE",
"https://raw.githubusercontent.com/BagaSept26/education-lab/refs/heads/main/backend_url.txt"
)
# Token GitHub PAT jika repo Anda private (tidak direkomendasikan untuk file URL ini)
# Jika repo publik, token tidak diperlukan untuk membaca file raw.
# GITHUB_TOKEN = os.getenv("GITHUB_PAT_FOR_REDIRECTOR") # Simpan di Secrets HF Space
# Interval cache untuk URL backend (dalam detik)
BACKEND_URL_CACHE_TTL = int(os.getenv("BACKEND_URL_CACHE_TTL", "60")) # Cache 60 detik
# --- State Aplikasi ---
app = FastAPI(title="Edication Lab Redirector", version="0.1.0")
current_backend_url = None
last_url_fetch_time = 0
url_fetch_lock = asyncio.Lock()
# --- Konfigurasi CORS untuk HF Space ---
# Izinkan request dari Vercel frontend Anda
# Ganti dengan URL Vercel Anda yang sebenarnya atau gunakan wildcard yang lebih spesifik
origins = [
"http://localhost:5173", # Untuk dev lokal frontend
"https://education-lab-bagaseptian.vercel.app", # GANTI INI
"https://*.vercel.app", # Lebih umum
"https://bagaseptian-edu_lab.hf.space" # URL space ini sendiri jika perlu
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
async def get_latest_backend_url_from_github():
"""Mengambil URL backend terbaru dari file di GitHub."""
global current_backend_url, last_url_fetch_time
async with url_fetch_lock: # Pastikan hanya satu coroutine yang fetch pada satu waktu
# Cek apakah cache masih valid
if current_backend_url and (time.time() - last_url_fetch_time) < BACKEND_URL_CACHE_TTL:
logger.info(f"Menggunakan URL backend dari cache: {current_backend_url}")
return current_backend_url
logger.info(f"Mengambil URL backend terbaru dari GitHub: {GITHUB_RAW_URL_FILE}")
headers = {}
# if GITHUB_TOKEN: # Jika file di repo private
# headers["Authorization"] = f"token {GITHUB_TOKEN}"
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(GITHUB_RAW_URL_FILE, headers=headers)
response.raise_for_status() # Akan raise error untuk status 4xx/5xx
new_url = response.text.strip()
if new_url and (new_url.startswith("http://") or new_url.startswith("https://")):
if new_url != current_backend_url:
logger.info(f"URL backend baru ditemukan: {new_url}")
current_backend_url = new_url
else:
logger.info(f"URL backend tidak berubah: {new_url}")
last_url_fetch_time = time.time()
return current_backend_url
else:
logger.error(f"Format URL tidak valid dari GitHub: '{new_url}'")
# Jangan update cache jika URL tidak valid, tetap gunakan yang lama (jika ada)
return current_backend_url # Atau None jika belum pernah ada yg valid
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error saat mengambil URL dari GitHub: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
logger.error(f"Request error saat mengambil URL dari GitHub: {e}")
except Exception as e:
logger.error(f"Error tidak terduga saat mengambil URL: {e}")
# Jika fetch gagal, kembalikan URL lama yang valid (jika ada) untuk sementara waktu
# atau jika URL lama sudah terlalu lama, mungkin lebih baik return None
if current_backend_url and (time.time() - last_url_fetch_time) < (BACKEND_URL_CACHE_TTL * 5): # Toleransi 5x TTL
logger.warning("Gagal mengambil URL baru, menggunakan URL lama dari cache.")
return current_backend_url
else: # Jika URL lama juga sudah terlalu basi atau tidak ada
current_backend_url = None # Reset jika gagal fetch terlalu lama
last_url_fetch_time = 0
return None
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
async def proxy_to_backend(request: Request, path: str):
"""
Menerima semua request dan mem-proxy-nya ke backend Colab.
"""
backend_url = await get_latest_backend_url_from_github()
if not backend_url:
logger.error("Tidak ada URL backend yang valid tersedia.")
raise HTTPException(status_code=503, detail="Layanan backend sementara tidak tersedia. URL tidak ditemukan.")
target_url_path = f"{backend_url}/{path}"
if request.url.query:
target_url_path += f"?{request.url.query}"
logger.info(f"Meneruskan request {request.method} ke: {target_url_path}")
# Baca body request jika ada
body_bytes = await request.body()
headers = dict(request.headers)
# Hapus atau modifikasi header tertentu jika perlu (misal, 'host')
headers.pop("host", None)
headers.pop("Host", None)
# Tambahkan header yang mungkin dibutuhkan oleh backend Anda
# headers["X-Forwarded-For"] = request.client.host
async with httpx.AsyncClient(timeout=300.0) as client: # Timeout panjang untuk model AI
try:
# Kirim request ke backend
rp = await client.request(
method=request.method,
url=target_url_path,
headers=headers,
content=body_bytes,
# Untuk mengizinkan redirect jika backend mengembalikan 3xx
# follow_redirects=False # Kita akan handle redirect secara manual jika perlu
)
# Buat respons streaming agar efisien
# Beberapa header tidak boleh di-proxy langsung
excluded_headers = [
"content-encoding", "Content-Encoding", # Biarkan httpx/FastAPI handle de/compression
"transfer-encoding", "Transfer-Encoding",
"connection", "Connection"
]
response_headers = {
k: v for k, v in rp.headers.items() if k.lower() not in excluded_headers
}
# Pastikan CORS header dari proxy juga benar
response_headers["Access-Control-Allow-Origin"] = request.headers.get("origin") or "*" # Atau lebih spesifik
response_headers["Access-Control-Allow-Credentials"] = "true"
# Jika backend mengembalikan redirect, kita bisa meneruskannya
if 300 <= rp.status_code < 400 and "location" in rp.headers:
return RedirectResponse(url=rp.headers["location"], status_code=rp.status_code, headers=response_headers)
return StreamingResponse(
rp.aiter_bytes(), # Stream content dari backend
status_code=rp.status_code,
media_type=rp.headers.get("content-type"),
headers=response_headers
)
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error dari backend: {e.response.status_code} - {e.response.text}")
# Meneruskan status error dari backend
return PlainTextResponse(content=e.response.text, status_code=e.response.status_code)
except httpx.ConnectError as e:
logger.error(f"Tidak dapat terhubung ke backend: {backend_url} - {e}")
raise HTTPException(status_code=504, detail=f"Gateway Timeout: Tidak dapat terhubung ke layanan backend Colab di {backend_url}.")
except httpx.TimeoutException as e:
logger.error(f"Timeout saat menghubungi backend: {backend_url} - {e}")
raise HTTPException(status_code=504, detail=f"Gateway Timeout: Waktu habis saat menghubungi layanan backend Colab.")
except Exception as e:
logger.error(f"Error tidak terduga saat proxying: {e}")
raise HTTPException(status_code=500, detail="Internal server error pada proxy.")
@app.get("/redirector-status", include_in_schema=False) # Endpoint untuk cek status redirector
async def status():
latest_url = await get_latest_backend_url_from_github() # Ambil URL terbaru
return {
"status": "Redirector Aktif",
"pesan": "Proxy bot untuk Edication Lab Backend.",
"target_github_url_file": GITHUB_RAW_URL_FILE,
"current_resolved_backend_url": latest_url if latest_url else "Belum ada URL backend yang valid ditemukan.",
"cache_ttl_seconds": BACKEND_URL_CACHE_TTL,
"last_url_fetch_attempt_timestamp": last_url_fetch_time if last_url_fetch_time > 0 else "Belum pernah fetch"
}
# Jika dijalankan secara lokal (python app.py)
# if __name__ == "__main__":
# import uvicorn
# logger.info("Menjalankan redirector secara lokal di port 8001...")
# # Ganti nilai di GITHUB_RAW_URL_FILE dengan URL raw GitHub Anda yang benar sebelum menjalankan lokal
# # Contoh: GITHUB_RAW_URL_FILE = "https://raw.githubusercontent.com/NAMA_USER_ANDA/edication-lab/main/backend_url.txt"
# uvicorn.run(app, host="0.0.0.0", port=8001)