saitejatirunagari Claude Sonnet 4.6 commited on
Commit
a008453
·
1 Parent(s): 76afa36

fix: HF Space proxy startup — CORS flags, port conflict, playwright deps

Browse files

Three bugs prevented the FastAPI+Streamlit reverse proxy from working on HF:

1. Remove STREAMLIT_SERVER_PORT=7860 + STREAMLIT_SERVER_ADDRESS=0.0.0.0 from
Dockerfile ENV — these were left over from the old single-process architecture
and conflict with the proxy design (uvicorn on 7860, Streamlit on 8501).

2. Add --server.enableCORS false --server.enableXsrfProtection false to the
Streamlit subprocess in start_streamlit() — Streamlit's default CORS middleware
rejects requests whose Origin doesn't match 127.0.0.1:8501, which breaks every
request forwarded by the FastAPI proxy from the public HF Space domain.

3. Remove --with-deps from _ensure_playwright() in ui.py (timeout 120s→30s) —
apt-get requires root; failing silently as uid 1000 on HF added wasted startup
latency on every cold boot. Chromium is pre-installed in the Dockerfile.

4. Add company_ats to PIPELINE_STEPS so it appears in the run progress UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (4) hide show
  1. Dockerfile +5 -4
  2. HISTORY.md +30 -0
  3. api_server.py +88 -3
  4. ui.py +6 -2
Dockerfile CHANGED
@@ -85,10 +85,11 @@ RUN chmod +x start.sh
85
  # Expose Streamlit port
86
  EXPOSE 7860
87
 
88
- ENV STREAMLIT_SERVER_PORT=7860 \
89
- STREAMLIT_SERVER_ADDRESS=0.0.0.0 \
90
- STREAMLIT_SERVER_HEADLESS=true \
 
 
91
  STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
92
 
93
- # start.sh boots ever-jobs on port 3001, then launches Streamlit on 7860
94
  CMD ["./start.sh"]
 
85
  # Expose Streamlit port
86
  EXPOSE 7860
87
 
88
+ # Only keep non-conflicting Streamlit env vars.
89
+ # STREAMLIT_SERVER_PORT and STREAMLIT_SERVER_ADDRESS are intentionally omitted:
90
+ # they would conflict with the proxy architecture where api_server.py runs
91
+ # uvicorn on 7860 and Streamlit is started on 127.0.0.1:8501 via CLI args.
92
+ ENV STREAMLIT_SERVER_HEADLESS=true \
93
  STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
94
 
 
95
  CMD ["./start.sh"]
HISTORY.md CHANGED
@@ -4,6 +4,36 @@ A running log of everything built, fixed, and changed. Most recent first.
4
 
5
  ---
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  ## 2026-06-23 (PM4) — ever-jobs sidecar disabled by default (clean/fast startup)
8
 
9
  User's HF log showed the ever-jobs Node sidecar crashing on boot
 
4
 
5
  ---
6
 
7
+ ## 2026-06-23 (PM5) — Fix HF Space startup: CORS proxy flags + port conflict + playwright
8
+
9
+ Three bugs prevented the FastAPI + Streamlit proxy from working on HF Spaces:
10
+
11
+ 1. **`STREAMLIT_SERVER_PORT=7860` removed from Dockerfile ENV** — this env var
12
+ conflicted with the proxy architecture: `api_server.py` runs uvicorn on 7860
13
+ and Streamlit on 8501 (via CLI `--server.port 8501`). If Streamlit used the
14
+ env var instead of the CLI arg, it would try to bind to 7860 while uvicorn
15
+ already held that port, causing one of them to crash.
16
+
17
+ 2. **`--server.enableCORS false --server.enableXsrfProtection false` added to
18
+ `start_streamlit()`** — Streamlit's default CORS middleware rejects requests
19
+ whose `Origin` header doesn't match `127.0.0.1:8501`. The FastAPI proxy
20
+ forwards the browser's `Origin` (`https://*.hf.space`) to Streamlit, which
21
+ then rejects it. Disabling these checks is the standard practice for running
22
+ Streamlit behind a reverse proxy.
23
+
24
+ 3. **`_ensure_playwright()` in `ui.py`: removed `--with-deps` + reduced timeout
25
+ from 120s → 30s** — `playwright install chromium --with-deps` runs
26
+ `apt-get install` for system deps, which requires root. Running as `user`
27
+ (uid 1000 on HF) it fails silently, but the command would attempt downloads
28
+ before failing, adding pointless latency on every cold start. Browser is
29
+ pre-installed in the Dockerfile so `--with-deps` is never needed on HF.
30
+
31
+ 4. **`company_ats` added to `PIPELINE_STEPS`** — added in the Scrapling commit
32
+ as a scraper but missing from the step-tracker list, so it never showed up
33
+ in the run progress UI.
34
+
35
+ ---
36
+
37
  ## 2026-06-23 (PM4) — ever-jobs sidecar disabled by default (clean/fast startup)
38
 
39
  User's HF log showed the ever-jobs Node sidecar crashing on boot
api_server.py CHANGED
@@ -16,12 +16,14 @@ import base64
16
  import hashlib
17
  import hmac
18
  import io
 
19
  import os
20
  import re
21
  import shutil
22
  import subprocess
23
  import sys
24
  import tempfile
 
25
 
26
  import httpx
27
  import uvicorn
@@ -747,6 +749,25 @@ async def repair_with_feedback(
747
  # ── Streamlit subprocess ──────────────────────────────────────────────────────
748
  _streamlit_proc: subprocess.Popen | None = None
749
  _STREAMLIT_PORT = 8501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
 
751
 
752
  def start_streamlit() -> None:
@@ -765,8 +786,24 @@ def start_streamlit() -> None:
765
  "127.0.0.1",
766
  "--server.headless",
767
  "true",
 
 
 
 
 
 
 
768
  ]
769
  )
 
 
 
 
 
 
 
 
 
770
 
771
 
772
  # ── WebSocket proxy for /_stcore/stream (Streamlit live updates) ──────────────
@@ -780,6 +817,15 @@ async def ws_proxy(websocket: WebSocket):
780
  import websockets.exceptions as _ws_exc
781
 
782
  await websocket.accept()
 
 
 
 
 
 
 
 
 
783
  upstream_url = (
784
  f"ws://127.0.0.1:{_STREAMLIT_PORT}/_stcore/stream"
785
  f"?{websocket.scope.get('query_string', b'').decode()}"
@@ -815,8 +861,16 @@ async def ws_proxy(websocket: WebSocket):
815
 
816
  await asyncio.gather(client_to_upstream(), upstream_to_client())
817
 
818
- except Exception:
819
- pass
 
 
 
 
 
 
 
 
820
  finally:
821
  try:
822
  await websocket.close()
@@ -837,6 +891,15 @@ async def proxy(request: Request, path: str):
837
  if path == "api" or path.startswith("api/"):
838
  return JSONResponse({"error": "not_found", "path": "/" + path}, status_code=404)
839
 
 
 
 
 
 
 
 
 
 
840
  url = f"http://127.0.0.1:{_STREAMLIT_PORT}/{path}"
841
  params = dict(request.query_params)
842
  headers = {
@@ -860,13 +923,35 @@ async def proxy(request: Request, path: str):
860
  "connection", "keep-alive"}
861
  clean_headers = {k: v for k, v in resp.headers.items()
862
  if k.lower() not in _DROP}
 
 
 
 
 
 
 
 
 
 
 
 
 
863
  return StarletteResponse(
864
  content=resp.content,
865
  status_code=resp.status_code,
866
  headers=clean_headers,
867
  media_type=resp.headers.get("content-type"),
868
  )
869
- except httpx.ConnectError:
 
 
 
 
 
 
 
 
 
870
  # Streamlit may still be starting up — return a friendly retry message.
871
  return StarletteResponse(
872
  content=b"Streamlit is starting, please wait...",
 
16
  import hashlib
17
  import hmac
18
  import io
19
+ import json
20
  import os
21
  import re
22
  import shutil
23
  import subprocess
24
  import sys
25
  import tempfile
26
+ import time
27
 
28
  import httpx
29
  import uvicorn
 
749
  # ── Streamlit subprocess ──────────────────────────────────────────────────────
750
  _streamlit_proc: subprocess.Popen | None = None
751
  _STREAMLIT_PORT = 8501
752
+ _DEBUG_LOG_PATH = "debug-03623a.log"
753
+
754
+
755
+ def _debug_log(run_id: str, hypothesis_id: str, location: str, message: str, data: dict) -> None:
756
+ """Append one NDJSON runtime debug event for this session."""
757
+ payload = {
758
+ "sessionId": "03623a",
759
+ "runId": run_id,
760
+ "hypothesisId": hypothesis_id,
761
+ "location": location,
762
+ "message": message,
763
+ "data": data,
764
+ "timestamp": int(time.time() * 1000),
765
+ }
766
+ try:
767
+ with open(_DEBUG_LOG_PATH, "a", encoding="utf-8") as f:
768
+ f.write(json.dumps(payload, separators=(",", ":")) + "\n")
769
+ except Exception:
770
+ pass
771
 
772
 
773
  def start_streamlit() -> None:
 
786
  "127.0.0.1",
787
  "--server.headless",
788
  "true",
789
+ # Required for reverse-proxy operation: without these, Streamlit's
790
+ # CORS/XSRF middleware rejects requests whose Origin header comes
791
+ # from the public HF Space domain rather than 127.0.0.1:8501.
792
+ "--server.enableCORS",
793
+ "false",
794
+ "--server.enableXsrfProtection",
795
+ "false",
796
  ]
797
  )
798
+ # #region agent log
799
+ _debug_log(
800
+ run_id="open-check-1",
801
+ hypothesis_id="H4",
802
+ location="api_server.py:start_streamlit",
803
+ message="streamlit_process_started",
804
+ data={"pid": _streamlit_proc.pid, "port": _STREAMLIT_PORT},
805
+ )
806
+ # #endregion
807
 
808
 
809
  # ── WebSocket proxy for /_stcore/stream (Streamlit live updates) ──────────────
 
817
  import websockets.exceptions as _ws_exc
818
 
819
  await websocket.accept()
820
+ # #region agent log
821
+ _debug_log(
822
+ run_id="open-check-1",
823
+ hypothesis_id="H3",
824
+ location="api_server.py:ws_proxy",
825
+ message="ws_client_connected",
826
+ data={"client": str(websocket.client)},
827
+ )
828
+ # #endregion
829
  upstream_url = (
830
  f"ws://127.0.0.1:{_STREAMLIT_PORT}/_stcore/stream"
831
  f"?{websocket.scope.get('query_string', b'').decode()}"
 
861
 
862
  await asyncio.gather(client_to_upstream(), upstream_to_client())
863
 
864
+ except Exception as exc:
865
+ # #region agent log
866
+ _debug_log(
867
+ run_id="open-check-1",
868
+ hypothesis_id="H3",
869
+ location="api_server.py:ws_proxy",
870
+ message="ws_upstream_connect_or_proxy_failed",
871
+ data={"error": str(exc)[:200]},
872
+ )
873
+ # #endregion
874
  finally:
875
  try:
876
  await websocket.close()
 
891
  if path == "api" or path.startswith("api/"):
892
  return JSONResponse({"error": "not_found", "path": "/" + path}, status_code=404)
893
 
894
+ # #region agent log
895
+ _debug_log(
896
+ run_id="open-check-1",
897
+ hypothesis_id="H2",
898
+ location="api_server.py:proxy_entry",
899
+ message="proxy_request_received",
900
+ data={"method": request.method, "path": path},
901
+ )
902
+ # #endregion
903
  url = f"http://127.0.0.1:{_STREAMLIT_PORT}/{path}"
904
  params = dict(request.query_params)
905
  headers = {
 
923
  "connection", "keep-alive"}
924
  clean_headers = {k: v for k, v in resp.headers.items()
925
  if k.lower() not in _DROP}
926
+ # #region agent log
927
+ _debug_log(
928
+ run_id="open-check-1",
929
+ hypothesis_id="H2",
930
+ location="api_server.py:proxy_response",
931
+ message="proxy_upstream_response",
932
+ data={
933
+ "path": path,
934
+ "status_code": resp.status_code,
935
+ "content_type": resp.headers.get("content-type", ""),
936
+ },
937
+ )
938
+ # #endregion
939
  return StarletteResponse(
940
  content=resp.content,
941
  status_code=resp.status_code,
942
  headers=clean_headers,
943
  media_type=resp.headers.get("content-type"),
944
  )
945
+ except httpx.ConnectError as exc:
946
+ # #region agent log
947
+ _debug_log(
948
+ run_id="open-check-1",
949
+ hypothesis_id="H4",
950
+ location="api_server.py:proxy_connect_error",
951
+ message="streamlit_connect_error",
952
+ data={"path": path, "error": str(exc)[:200]},
953
+ )
954
+ # #endregion
955
  # Streamlit may still be starting up — return a friendly retry message.
956
  return StarletteResponse(
957
  content=b"Streamlit is starting, please wait...",
ui.py CHANGED
@@ -616,12 +616,15 @@ hr { border-color: #E2E8F0 !important; opacity: 0.5 !important; }
616
  """, unsafe_allow_html=True)
617
 
618
  # ── Playwright install (runs once per server lifetime on HF Spaces) ──────────
 
 
 
619
  @st.cache_resource(show_spinner=False)
620
  def _ensure_playwright():
621
  import subprocess, sys as _sys
622
  result = subprocess.run(
623
- [_sys.executable, "-m", "playwright", "install", "chromium", "--with-deps"],
624
- capture_output=True, text=True, timeout=120,
625
  )
626
  return result.returncode == 0
627
 
@@ -669,6 +672,7 @@ PIPELINE_STEPS = [
669
  {"id": "remotive", "icon": "🌍", "title": "Remotive"},
670
  {"id": "weworkremotely", "icon": "💻", "title": "WeWorkRemotely"},
671
  {"id": "naukri", "icon": "🇮🇳", "title": "Naukri"},
 
672
  {"id": "ever_jobs", "icon": "🌐", "title": "EverJobs (160+)"},
673
  {"id": "assess", "icon": "🤖", "title": "AI Assessment"},
674
  {"id": "resumes", "icon": "📝", "title": "Generate Resumes"},
 
616
  """, unsafe_allow_html=True)
617
 
618
  # ── Playwright install (runs once per server lifetime on HF Spaces) ──────────
619
+ # On HF Spaces, Chromium is pre-installed in the Dockerfile so this is a fast
620
+ # no-op check. We omit --with-deps because system-package installs require root
621
+ # and would fail silently, adding ~10s of pointless startup latency every boot.
622
  @st.cache_resource(show_spinner=False)
623
  def _ensure_playwright():
624
  import subprocess, sys as _sys
625
  result = subprocess.run(
626
+ [_sys.executable, "-m", "playwright", "install", "chromium"],
627
+ capture_output=True, text=True, timeout=30,
628
  )
629
  return result.returncode == 0
630
 
 
672
  {"id": "remotive", "icon": "🌍", "title": "Remotive"},
673
  {"id": "weworkremotely", "icon": "💻", "title": "WeWorkRemotely"},
674
  {"id": "naukri", "icon": "🇮🇳", "title": "Naukri"},
675
+ {"id": "company_ats", "icon": "🏢", "title": "Company ATS"},
676
  {"id": "ever_jobs", "icon": "🌐", "title": "EverJobs (160+)"},
677
  {"id": "assess", "icon": "🤖", "title": "AI Assessment"},
678
  {"id": "resumes", "icon": "📝", "title": "Generate Resumes"},